- Set up Docker, MySQL, and Nginx on host machine
- MySQL data directory should be in the bind-mounted directory specified in deployment/docker/compose.yaml
- Recover database data from backups
- Make sure default key for SSH and for GitHub pushing has no passcode if planning to use automatic db/image backup scripts. No hack pls
- MySQL data directory should be in the bind-mounted directory specified in deployment/docker/compose.yaml
git clone- Make sure all files and folders in the entire project are owned by the user that Docker runs its containers as (see deployment/systemd_reference/personal_website.service)
- Install packages:
- Install Python modules from requirements.txt by running
pip install -r requirements.txt(ideally within a virtualenv) - Install JS modules from app/static/package.json by running
npm installin the app/static/ directory
- Install Python modules from requirements.txt by running
- Add back gitignored files:
.env: randomly generatedSECRET_KEYand SQLAlchemyDATABASE_URLfor connecting to MySQL from hostDATABASE_URLshould be something likemysql+pymysql://[db username]:[db password]@[hostname]:[port]/[database name]?charset=utf8mb4
deployment/docker/flask/envs/.env: same as.envbut withDATABASE_URLmodified to connect to the MySQL Docker container (i.e. thehostnamepart of the URL is the name of the MySQL container service in deployment/docker/compose.yaml, in this casemysql)deployment/docker/mysql/envs/.mysqlenv: nothing yet (no environment variables if bind-mounting existing MySQL data directory)deployment/backup_scripts/db_backup_config.sh: set the variables referenced in deployment/backup_scripts/db_backup.shapp/static/css/custom_bootstrap.cssandapp/static/css/custom_bootstrap.css.map: runnpm compile_bootstrapfrom within the app/static/ folder
- Navigate to deployment/docker/ and run
deploy.sh(or use asystemdservice, for example deployment/systemd_reference/personal_website.service)
Keep up-to-date:
- .gitignore
- config.py
- Server-side access control
- README
- Backup scripts in deployment/backup_scripts/
- Cloudflare WAF rules etc.
- Sync/keep up-to-date according to comments and common sense:
- deployment/docker/compose.yaml
- Dockerfiles
- Docker entrypoint scripts
- Docker environment variables
deployment/backup_scripts/db_backup_config.shconfigs- Backup scripts
systemdservices
- To connect to the MySQL instance running in Docker from the host:
- Make sure the MySQL port (default 3306) is exposed from Docker and there is a
.envfile on the host withDATABASE_URLpointing tolocalhost - Use
mysql --protocol=tcpto connect so it doesn't try to use a Unix socket; make sure to use the MySQL user that has%as its host (because that means it can connect from any host, whereaslocalhostwould mean that it can only connect from within the Docker container)
- Make sure the MySQL port (default 3306) is exposed from Docker and there is a
- To edit database schema:
- Edit app/models.py on the host
- IMPORTANT: currently MySQL-specific!!!
- Run
flask db migrateon the host in the Python venv; this requires MySQL connectivity from the host - CHECK MIGRATION SCRIPT IN migrations/versions/!!!
-
If renaming columns, you will probably have to edit the Alembic script in migrations/versions/ to use
alter_column()!existing_typeis a required argument:batch_op.alter_column(column_name='[]', new_column_name='[]', existing_type=[])Reference migrations/versions/79665802aa08_rename_blogpage_title_and_subtitle_to_.py for examples.
-
If changing
uniqueconstraint, you will needbatch_op.create_unique_constraint("[constraint_name]", ['[column_name]'])and
batch_op.drop_constraint("[constraint_name]", type_='unique')
-
- Run
flask db upgradeon the host in the Python venv or restart the Docker containers
- Edit app/models.py on the host
- Assume the user can reach all endpoints, so access-control must be perfect server-side
- Use the functions defined in app/utils.py for access control
- It doesn't matter as much if client-side is lax on updating hidden HTML links etc. on session expiry. This is good because my client-side is an absolute dumpster fire.
- Add to database (reference current database entries)
- Add a developer/backrooms blogpage too with its
blogpage_idbeing the negative of the public one blogpage_idis always an integer except for the commented cases in config.py, where they must be strings to avoid confusion with negative values and list/dictionary accessing
- Add a developer/backrooms blogpage too with its
- Update config.py:
- Update
BLOGPAGE_ID_TO_PATHwith the same paths that you gave the new blogpage and its developer blogpage in the database; this is used for blueprint initialization (we can't access database before app context is fully created) - Update
URLS_LOGIN_REQUIREDwith the backrooms blogpage
- Update
- Create new static directories for it in app/blog/static/blogpage/ from the template, and update other static directory names if necessary
- Remember that since HTML templates are the same for every blogpage, things like font or background image customizations must be done through static files like CSS, which are imported individually per blogpage
- If overriding default background image, change
backgroundImgOverrideNamein a fileapp/blog/static/blogpage/[blueprint]/js/override_background_img.js
- Change the static directories, obviously
- Update paths in config.py
- Update Markdown expansion/collapse regex in app/models.py
- Update image paths in app/admin/routes.py. This is important to make sure we don't accidentally delete/move important files!
- Update static paths for all linked JS/CSS in templates
- Update image paths for all existing images in db
- GET forms:
- These should not modify server-side state!
- Usage guidelines:
- Do not implement a CSRF Token hidden field to avoid leaking token in the URL (per OWASP guidelines). This means that we shouldn't use the
boostrap_wtf.quick_form()macro for GET forms!
- Do not implement a CSRF Token hidden field to avoid leaking token in the URL (per OWASP guidelines). This means that we shouldn't use the
- Refer to app/blog/static/blogpage/js/goto_page_form.js and its associated app/blog/templates/blog/blogpage/index.html for an example of a GET form
- POST forms:
- All other forms
- Usage guidelines:
- Must be Ajax, using
fetchWrapper()in app/static/js/util_ajax.js and sending FormData (since the CSRF error handling is designed only for FormData). SeedoAjaxResponseBase()in the same file for documentation on the basic, always-supported JSON keys that the backend can return.
- Must be Ajax, using
- Refer to app/static/js/util_session.js, app/static/js/main_form_submit.js, and app/blog/static/blogpage/js/post_comments.js for examples of POST forms
- Always add HTML classes
auth-true/auth-false(for showing/hiding elements) when needed
- Update
CUSTOM_ERRORSin config.py - Update
fetchWrapper()in app/static/js/util_ajax.js - Update app/routes.py error handlers if necessary
url_for()to a blueprint (trusted destination!) should always be used with_external=Truein both HTML templates and Flask to simplify the cross-origin nature of having a blog subdomain- Try not to modify any of the
forms.pys, as some JS might rely on hardcoded values of the form fields. I don't do frontend, ok?
- Make sure to check out the documentation for Python-Markdown's official extensions
- Check code in app/blog/blogpage/routes.py to see which ones are used
- Attribute Lists allows you to take advantage of the many util CSS classes in app/static/css/util.css
- Custom Markdown syntax:
- Check source code for detailed documentation and usages
- Inline:
~~<text>~~: strikethrough TODO: update once finalized
- Blocks (all delimiters must be surrounded by a blank line on both sides; not allowed in comments due to potential bugs):
\begin{<block type>}andend{<block type>}, surrounded by a blank line on both sides, puts everything in between in the specified<block type>- Available
<block type>s:captioned_figure: a figure with a caption underneath- Requires nested
captionblock inside
- Requires nested
cited_blockquote: a blockquote with a citation underneath- Requires nested
citationblock inside
- Requires nested
dropdown: an expandable/collapsible dropdown- Alternative
<block type>s:exer: exercisepf: proofrmk: remark
- Requires nested
summaryblock inside, except for the following<block type>s that get defaults:pfrmk
- Alternative
textbox: a textbox- Alternative
<block type>s:coro: corollarydefn: definitionimpt: importantnotat: notationprop: propositionthm: theorem
- Alternative
- Images:
- Only give the filename for images in Markdown; the full path will be automatically expanded (won't work if you put in full path because I'm bad at regex!!!)
- Raw HTML (including with attributes!) will be rendered, which is useful for additional styling or in environments where Markdown equivalents may not always work (footnotes, tables, blockquotes etc.). Examples:
<span></span>with pretty much any custom CSS styling you want (or with existing styling classes, once CSP is able to block inlinestyleattributes)<pre><code></code></pre>with<br>newlines for multiline code blocks in a table, as raw newlines would interfere with the table syntax<small></small>for small text<p></p>for paragraphs and line breaks (note: not supported in footnotes; use<br><br>instead)- E.g. lists, which have had the space between it and the previous paragraph removed by default
<br>for line breaks that aren't new paragraphs and don't leave extra space, like between lines in a stanza, and<br>surrounded by two empty lines for more space than a normal paragraph, like between stanzas
- Use
debugTestSelfLinks()in the browser console to test for dead self-links on the current page
- Uses Markdown tables with "Compact mode" and "Line breaks as <br>" checked
- For merged cells, use the Attribute Lists extension to set
colspan. To keep valid table syntax, put<span></span> {: hidden }in cells that have been merged into other ones. - To specify column
widthattributes (in HTML, not CSS) for example with Attribute Lists, either specify in pixels or percentages. Pixels are absolute while percentages are relative to the width of the table. If percentages are used, or if no width specified at all, table will havemin-width: 100%of parent div.
Comparing Flask's built-in session cookie with PERMANENT_SESSION_LIFETIME config vs. Flask-Login's remember me cookie with REMEMBER_COOKIE_DURATION config:
session.permanentdoes not actually affect if a cookie is invalidated byPERMANENT_SESSION_LIFETIME; cookies will always adhere to this lifetime (including the non-signed-in, default cookie for storing Flask'ssession):session.permanent=Falsemeans the session cookie is invalidated by Flask but not deleted when this lifetime is up, whilesession.permanent=Trueactually gives it an expiration time.rememberfrom Flask-Login only affects how the cookies are handled when the browser is closed (although it seems many browsers nowadays will persist even session (non-remembered) cookies as well on close).
| Session cookie stored in: | Remember cookie stored in: | PERMANENT_SESSION_LIFETIME effect on session cookie |
REMEMBER_COOKIE_DURATION effect on remember cookie |
User experience when PERMANENT_SESSION_LIFETIME reached |
User experience when REMEMBER_COOKIE_DURATION reached |
|
|---|---|---|---|---|---|---|
session.permanent=False, remember=False |
Memory (non-persistent) | - | Invalidated by Flask (docs) | - | Logged out | - |
session.permanent=False, remember=True |
Memory (non-persistent) | Disk (persistent) | Invalidated by Flask | Expires & is deleted | Logged out | Logged out if browser closed |
session.permanent=True, remember=False |
Disk (persistent) | - | Expires & is deleted | - | Logged out | - |
session.permanent=True, remember=True |
Disk (persistent) | Disk (persistent) | Expires & is deleted | Expires & is deleted | Logged out | Logged out if browser closed |