From fed4454a0531395ad581538020bc9e4cbd1d2b90 Mon Sep 17 00:00:00 2001 From: finn Date: Thu, 1 Aug 2024 02:07:49 -0700 Subject: [PATCH 01/25] mg tut c2 --- backend/app/routes.py | 17 ++++++++++++++++- backend/app/templates/base.html | 16 ++++++++++++++++ backend/app/templates/index.html | 11 +++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 backend/app/templates/base.html create mode 100644 backend/app/templates/index.html diff --git a/backend/app/routes.py b/backend/app/routes.py index 05a0bd4..9a10401 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -1,7 +1,22 @@ +from flask import render_template from app import app @app.route('/') @app.route('/index') def index(): - return "Hello, World!" + + user = {'username': 'Finnaa'} + posts = [ + { + 'author': {'username': 'john'}, + 'body': 'Beautiful day 1' + }, + { + 'author': {'username': 'susie'}, + 'body': 'Movie is good.' + } + ] + #return posts; + return render_template('index.html', title='Home', user=user, posts=posts) + diff --git a/backend/app/templates/base.html b/backend/app/templates/base.html new file mode 100644 index 0000000..920f44b --- /dev/null +++ b/backend/app/templates/base.html @@ -0,0 +1,16 @@ + + + + {% if title %} + {{ title }} - blog + {% else %} + Welcome to blog. + {% endif %} + + +
blgo: home
+
+ {% block content %}{% endblock %} + + + diff --git a/backend/app/templates/index.html b/backend/app/templates/index.html new file mode 100644 index 0000000..ab4fd46 --- /dev/null +++ b/backend/app/templates/index.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block content %} +

Helloo, {{ user.username }}!

+ {% for post in posts %} +

{{ post.author.username }} says: {{ post.body }}

+ {% endfor %} +{% endblock %} + + + From 426e917df96279b8ed178a039dc6236c47487b85 Mon Sep 17 00:00:00 2001 From: finn Date: Thu, 1 Aug 2024 03:33:45 -0700 Subject: [PATCH 02/25] mg tut c3 --- backend/app/__init__.py | 2 + backend/app/forms.py | 10 + backend/app/routes.py | 11 +- backend/app/static/simple.css | 713 ++++++++++++++++++++++++++++++ backend/app/static/simple.min.css | 1 + backend/app/templates/base.html | 19 +- backend/app/templates/login.html | 25 ++ backend/config.py | 7 + backend/requirements.txt | 2 + backend/templates/about.html | 11 - backend/templates/basictemp.html | 9 - backend/templates/home.html | 11 - 12 files changed, 788 insertions(+), 33 deletions(-) create mode 100644 backend/app/forms.py create mode 100644 backend/app/static/simple.css create mode 100644 backend/app/static/simple.min.css create mode 100644 backend/app/templates/login.html create mode 100644 backend/config.py delete mode 100644 backend/templates/about.html delete mode 100644 backend/templates/basictemp.html delete mode 100644 backend/templates/home.html diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 5bdbcd0..14ca068 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -1,6 +1,8 @@ from flask import Flask +from config import Config app = Flask(__name__) +app.config.from_object(Config) from app import routes diff --git a/backend/app/forms.py b/backend/app/forms.py new file mode 100644 index 0000000..7462ea6 --- /dev/null +++ b/backend/app/forms.py @@ -0,0 +1,10 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, BooleanField, SubmitField +from wtforms.validators import DataRequired + +class LoginForm(FlaskForm): + username = StringField('Username', validators=[DataRequired()]) + password = PasswordField('Pasword', validators=[DataRequired()]) + remember_me = BooleanField('Remember Me') + submit = SubmitField('Sign In') + diff --git a/backend/app/routes.py b/backend/app/routes.py index 9a10401..5e2a53f 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -1,5 +1,6 @@ -from flask import render_template +from flask import render_template, flash, redirect, url_for from app import app +from app.forms import LoginForm @app.route('/') @app.route('/index') @@ -19,4 +20,12 @@ def index(): #return posts; return render_template('index.html', title='Home', user=user, posts=posts) +@app.route('/login', methods=['GET', 'POST']) +def login(): + form = LoginForm() + if form.validate_on_submit(): + flash('Login requested for user {}, remember_me={}'.format(form.username.data, form.remember_me.data)) + return redirect(url_for('index')) + return render_template('login.html', title='Sign In', form=form) + diff --git a/backend/app/static/simple.css b/backend/app/static/simple.css new file mode 100644 index 0000000..efe643b --- /dev/null +++ b/backend/app/static/simple.css @@ -0,0 +1,713 @@ +/* Global variables. */ +:root, +::backdrop { + /* Set sans-serif & mono fonts */ + --sans-font: -apple-system, BlinkMacSystemFont, "Avenir Next", Avenir, + "Nimbus Sans L", Roboto, "Noto Sans", "Segoe UI", Arial, Helvetica, + "Helvetica Neue", sans-serif; + --mono-font: Consolas, Menlo, Monaco, "Andale Mono", "Ubuntu Mono", monospace; + --standard-border-radius: 5px; + + /* Default (light) theme */ + --bg: #fff; + --accent-bg: #f5f7ff; + --text: #212121; + --text-light: #585858; + --border: #898EA4; + --accent: #0d47a1; + --accent-hover: #1266e2; + --accent-text: var(--bg); + --code: #d81b60; + --preformatted: #444; + --marked: #ffdd33; + --disabled: #efefef; +} + +/* Dark theme */ +@media (prefers-color-scheme: dark) { + :root, + ::backdrop { + color-scheme: dark; + --bg: #212121; + --accent-bg: #2b2b2b; + --text: #dcdcdc; + --text-light: #ababab; + /* --accent: #ffb300; */ + --accent: #ace1af; + /* --accent-hover: #ffe099; */ + --accent-hover: #99dabe; + --accent-text: var(--bg); + --code: #f06292; + --preformatted: #ccc; + --disabled: #111; + } + /* Add a bit of transparency so light media isn't so glaring in dark mode */ + img, + video { + opacity: 0.8; + } +} + +/* Reset box-sizing */ +*, *::before, *::after { + box-sizing: border-box; +} + +/* Reset default appearance */ +textarea, +select, +input, +progress { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; +} + +html { + /* Set the font globally */ + font-family: var(--sans-font); + scroll-behavior: smooth; +} + +/* Make the body a nice central block */ +body { + color: var(--text); + background-color: var(--bg); + font-size: 1.15rem; + line-height: 1.5; + display: grid; + grid-template-columns: 1fr min(45rem, 90%) 1fr; + margin: 0; +} +body > * { + grid-column: 2; +} + +/* Make the header bg full width, but the content inline with body */ +body > header { + background-color: var(--accent-bg); + border-bottom: 1px solid var(--border); + text-align: center; + padding: 0 0.5rem 2rem 0.5rem; + grid-column: 1 / -1; +} + +body > header > *:only-child { + margin-block-start: 2rem; +} + +body > header h1 { + max-width: 1200px; + margin: 1rem auto; +} + +body > header p { + max-width: 40rem; + margin: 1rem auto; +} + +/* Add a little padding to ensure spacing is correct between content and header > nav */ +main { + padding-top: 1.5rem; +} + +body > footer { + margin-top: 4rem; + padding: 2rem 1rem 1.5rem 1rem; + color: var(--text-light); + font-size: 0.9rem; + text-align: center; + border-top: 1px solid var(--border); +} + +/* Format headers */ +h1 { + font-size: 3rem; +} + +h2 { + font-size: 2.6rem; + margin-top: 3rem; +} + +h3 { + font-size: 2rem; + margin-top: 3rem; +} + +h4 { + font-size: 1.44rem; +} + +h5 { + font-size: 1.15rem; +} + +h6 { + font-size: 0.96rem; +} + +p { + margin: 1.5rem 0; +} + +/* Prevent long strings from overflowing container */ +p, h1, h2, h3, h4, h5, h6 { + overflow-wrap: break-word; +} + +/* Fix line height when title wraps */ +h1, +h2, +h3 { + line-height: 1.1; +} + +/* Reduce header size on mobile */ +@media only screen and (max-width: 720px) { + h1 { + font-size: 2.5rem; + } + + h2 { + font-size: 2.1rem; + } + + h3 { + font-size: 1.75rem; + } + + h4 { + font-size: 1.25rem; + } +} + +/* Format links & buttons */ +a, +a:visited { + color: var(--accent); +} + +a:hover { + text-decoration: none; +} + +button, +.button, +a.button, /* extra specificity to override a */ +input[type="submit"], +input[type="reset"], +input[type="button"], +label[type="button"] { + border: 1px solid var(--accent); + background-color: var(--accent); + color: var(--accent-text); + padding: 0.5rem 0.9rem; + text-decoration: none; + line-height: normal; +} + +.button[aria-disabled="true"], +input:disabled, +textarea:disabled, +select:disabled, +button[disabled] { + cursor: not-allowed; + background-color: var(--disabled); + border-color: var(--disabled); + color: var(--text-light); +} + +input[type="range"] { + padding: 0; +} + +/* Set the cursor to '?' on an abbreviation and style the abbreviation to show that there is more information underneath */ +abbr[title] { + cursor: help; + text-decoration-line: underline; + text-decoration-style: dotted; +} + +button:enabled:hover, +.button:not([aria-disabled="true"]):hover, +input[type="submit"]:enabled:hover, +input[type="reset"]:enabled:hover, +input[type="button"]:enabled:hover, +label[type="button"]:hover { + background-color: var(--accent-hover); + border-color: var(--accent-hover); + cursor: pointer; +} + +.button:focus-visible, +button:focus-visible:where(:enabled), +input:enabled:focus-visible:where( + [type="submit"], + [type="reset"], + [type="button"] +) { + outline: 2px solid var(--accent); + outline-offset: 1px; +} + +/* Format navigation */ +header > nav { + font-size: 1rem; + line-height: 2; + padding: 1rem 0 0 0; +} + +/* Use flexbox to allow items to wrap, as needed */ +header > nav ul, +header > nav ol { + align-content: space-around; + align-items: center; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + list-style-type: none; + margin: 0; + padding: 0; +} + +/* List items are inline elements, make them behave more like blocks */ +header > nav ul li, +header > nav ol li { + display: inline-block; +} + +header > nav a, +header > nav a:visited { + margin: 0 0.5rem 1rem 0.5rem; + border: 1px solid var(--border); + border-radius: var(--standard-border-radius); + color: var(--text); + display: inline-block; + padding: 0.1rem 1rem; + text-decoration: none; +} + +header > nav a:hover, +header > nav a.current, +header > nav a[aria-current="page"] { + border-color: var(--accent); + color: var(--accent); + cursor: pointer; +} + +/* Reduce nav side on mobile */ +@media only screen and (max-width: 720px) { + header > nav a { + border: none; + padding: 0; + text-decoration: underline; + line-height: 1; + } +} + +/* Consolidate box styling */ +aside, details, pre, progress { + background-color: var(--accent-bg); + border: 1px solid var(--border); + border-radius: var(--standard-border-radius); + margin-bottom: 1rem; +} + +aside { + font-size: 1rem; + width: 30%; + padding: 0 15px; + margin-inline-start: 15px; + float: right; +} +*[dir="rtl"] aside { + float: left; +} + +/* Make aside full-width on mobile */ +@media only screen and (max-width: 720px) { + aside { + width: 100%; + float: none; + margin-inline-start: 0; + } +} + +article, fieldset, dialog { + border: 1px solid var(--border); + padding: 1rem; + border-radius: var(--standard-border-radius); + margin-bottom: 1rem; +} + +article h2:first-child, +section h2:first-child, +article h3:first-child, +section h3:first-child { + margin-top: 1rem; +} + +section { + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + padding: 2rem 1rem; + margin: 3rem 0; +} + +/* Don't double separators when chaining sections */ +section + section, +section:first-child { + border-top: 0; + padding-top: 0; +} + +section + section { + margin-top: 0; +} + +section:last-child { + border-bottom: 0; + padding-bottom: 0; +} + +details { + padding: 0.7rem 1rem; +} + +summary { + cursor: pointer; + font-weight: bold; + padding: 0.7rem 1rem; + margin: -0.7rem -1rem; + word-break: break-all; +} + +details[open] > summary + * { + margin-top: 0; +} + +details[open] > summary { + margin-bottom: 0.5rem; +} + +details[open] > :last-child { + margin-bottom: 0; +} + +/* Format tables */ +table { + border-collapse: collapse; + margin: 1.5rem 0; +} + +figure > table { + width: max-content; + margin: 0; +} + +td, +th { + border: 1px solid var(--border); + text-align: start; + padding: 0.5rem; +} + +th { + background-color: var(--accent-bg); + font-weight: bold; +} + +tr:nth-child(even) { + /* Set every other cell slightly darker. Improves readability. */ + background-color: var(--accent-bg); +} + +table caption { + font-weight: bold; + margin-bottom: 0.5rem; +} + +/* Format forms */ +textarea, +select, +input, +button, +.button { + font-size: inherit; + font-family: inherit; + padding: 0.5rem; + margin-bottom: 0.5rem; + border-radius: var(--standard-border-radius); + box-shadow: none; + max-width: 100%; + display: inline-block; +} +textarea, +select, +input { + color: var(--text); + background-color: var(--bg); + border: 1px solid var(--border); +} +label { + display: block; +} +textarea:not([cols]) { + width: 100%; +} + +/* Add arrow to drop-down */ +select:not([multiple]) { + background-image: linear-gradient(45deg, transparent 49%, var(--text) 51%), + linear-gradient(135deg, var(--text) 51%, transparent 49%); + background-position: calc(100% - 15px), calc(100% - 10px); + background-size: 5px 5px, 5px 5px; + background-repeat: no-repeat; + padding-inline-end: 25px; +} +*[dir="rtl"] select:not([multiple]) { + background-position: 10px, 15px; +} + +/* checkbox and radio button style */ +input[type="checkbox"], +input[type="radio"] { + vertical-align: middle; + position: relative; + width: min-content; +} + +input[type="checkbox"] + label, +input[type="radio"] + label { + display: inline-block; +} + +input[type="radio"] { + border-radius: 100%; +} + +input[type="checkbox"]:checked, +input[type="radio"]:checked { + background-color: var(--accent); +} + +input[type="checkbox"]:checked::after { + /* Creates a rectangle with colored right and bottom borders which is rotated to look like a check mark */ + content: " "; + width: 0.18em; + height: 0.32em; + border-radius: 0; + position: absolute; + top: 0.05em; + left: 0.17em; + background-color: transparent; + border-right: solid var(--bg) 0.08em; + border-bottom: solid var(--bg) 0.08em; + font-size: 1.8em; + transform: rotate(45deg); +} +input[type="radio"]:checked::after { + /* creates a colored circle for the checked radio button */ + content: " "; + width: 0.25em; + height: 0.25em; + border-radius: 100%; + position: absolute; + top: 0.125em; + background-color: var(--bg); + left: 0.125em; + font-size: 32px; +} + +/* Makes input fields wider on smaller screens */ +@media only screen and (max-width: 720px) { + textarea, + select, + input { + width: 100%; + } +} + +/* Set a height for color input */ +input[type="color"] { + height: 2.5rem; + padding: 0.2rem; +} + +/* do not show border around file selector button */ +input[type="file"] { + border: 0; +} + +/* Misc body elements */ +hr { + border: none; + height: 1px; + background: var(--border); + margin: 1rem auto; +} + +mark { + padding: 2px 5px; + border-radius: var(--standard-border-radius); + background-color: var(--marked); + color: black; +} + +mark a { + color: #0d47a1; +} + +img, +video { + max-width: 100%; + height: auto; + border-radius: var(--standard-border-radius); +} + +figure { + margin: 0; + display: block; + overflow-x: auto; +} + +figure > img, +figure > picture > img { + display: block; + margin-inline: auto; +} + +figcaption { + text-align: center; + font-size: 0.9rem; + color: var(--text-light); + margin-block: 1rem; +} + +blockquote { + margin-inline-start: 2rem; + margin-inline-end: 0; + margin-block: 2rem; + padding: 0.4rem 0.8rem; + border-inline-start: 0.35rem solid var(--accent); + color: var(--text-light); + font-style: italic; +} + +cite { + font-size: 0.9rem; + color: var(--text-light); + font-style: normal; +} + +dt { + color: var(--text-light); +} + +/* Use mono font for code elements */ +code, +pre, +pre span, +kbd, +samp { + font-family: var(--mono-font); + color: var(--code); +} + +kbd { + color: var(--preformatted); + border: 1px solid var(--preformatted); + border-bottom: 3px solid var(--preformatted); + border-radius: var(--standard-border-radius); + padding: 0.1rem 0.4rem; +} + +pre { + padding: 1rem 1.4rem; + max-width: 100%; + overflow: auto; + color: var(--preformatted); +} + +/* Fix embedded code within pre */ +pre code { + color: var(--preformatted); + background: none; + margin: 0; + padding: 0; +} + +/* Progress bars */ +/* Declarations are repeated because you */ +/* cannot combine vendor-specific selectors */ +progress { + width: 100%; +} + +progress:indeterminate { + background-color: var(--accent-bg); +} + +progress::-webkit-progress-bar { + border-radius: var(--standard-border-radius); + background-color: var(--accent-bg); +} + +progress::-webkit-progress-value { + border-radius: var(--standard-border-radius); + background-color: var(--accent); +} + +progress::-moz-progress-bar { + border-radius: var(--standard-border-radius); + background-color: var(--accent); + transition-property: width; + transition-duration: 0.3s; +} + +progress:indeterminate::-moz-progress-bar { + background-color: var(--accent-bg); +} + +dialog { + max-width: 40rem; + margin: auto; +} + +dialog::backdrop { + background-color: var(--bg); + opacity: 0.8; +} + +@media only screen and (max-width: 720px) { + dialog { + max-width: 100%; + margin: auto 1em; + } +} + +/* Superscript & Subscript */ +/* Prevent scripts from affecting line-height. */ +sup, sub { + vertical-align: baseline; + position: relative; +} + +sup { + top: -0.4em; +} + +sub { + top: 0.3em; +} + +/* Classes for notices */ +.notice { + background: var(--accent-bg); + border: 2px solid var(--border); + border-radius: var(--standard-border-radius); + padding: 1.5rem; + margin: 2rem 0; +} diff --git a/backend/app/static/simple.min.css b/backend/app/static/simple.min.css new file mode 100644 index 0000000..9db8fe6 --- /dev/null +++ b/backend/app/static/simple.min.css @@ -0,0 +1 @@ +:root,::backdrop{--sans-font:-apple-system,BlinkMacSystemFont,"Avenir Next",Avenir,"Nimbus Sans L",Roboto,"Noto Sans","Segoe UI",Arial,Helvetica,"Helvetica Neue",sans-serif;--mono-font:Consolas,Menlo,Monaco,"Andale Mono","Ubuntu Mono",monospace;--standard-border-radius:5px;--bg:#fff;--accent-bg:#f5f7ff;--text:#212121;--text-light:#585858;--border:#898ea4;--accent:#0d47a1;--accent-hover:#1266e2;--accent-text:var(--bg);--code:#d81b60;--preformatted:#444;--marked:#fd3;--disabled:#efefef}@media (prefers-color-scheme:dark){:root,::backdrop{color-scheme:dark;--bg:#212121;--accent-bg:#2b2b2b;--text:#dcdcdc;--text-light:#ababab;--accent:#ffb300;--accent-hover:#ffe099;--accent-text:var(--bg);--code:#f06292;--preformatted:#ccc;--disabled:#111}img,video{opacity:.8}}*,:before,:after{box-sizing:border-box}textarea,select,input,progress{-webkit-appearance:none;-moz-appearance:none;appearance:none}html{font-family:var(--sans-font);scroll-behavior:smooth}body{color:var(--text);background-color:var(--bg);grid-template-columns:1fr min(45rem,90%) 1fr;margin:0;font-size:1.15rem;line-height:1.5;display:grid}body>*{grid-column:2}body>header{background-color:var(--accent-bg);border-bottom:1px solid var(--border);text-align:center;grid-column:1/-1;padding:0 .5rem 2rem}body>header>:only-child{margin-block-start:2rem}body>header h1{max-width:1200px;margin:1rem auto}body>header p{max-width:40rem;margin:1rem auto}main{padding-top:1.5rem}body>footer{color:var(--text-light);text-align:center;border-top:1px solid var(--border);margin-top:4rem;padding:2rem 1rem 1.5rem;font-size:.9rem}h1{font-size:3rem}h2{margin-top:3rem;font-size:2.6rem}h3{margin-top:3rem;font-size:2rem}h4{font-size:1.44rem}h5{font-size:1.15rem}h6{font-size:.96rem}p{margin:1.5rem 0}p,h1,h2,h3,h4,h5,h6{overflow-wrap:break-word}h1,h2,h3{line-height:1.1}@media only screen and (width<=720px){h1{font-size:2.5rem}h2{font-size:2.1rem}h3{font-size:1.75rem}h4{font-size:1.25rem}}a,a:visited{color:var(--accent)}a:hover{text-decoration:none}button,.button,a.button,input[type=submit],input[type=reset],input[type=button],label[type=button]{border:1px solid var(--accent);background-color:var(--accent);color:var(--accent-text);padding:.5rem .9rem;line-height:normal;text-decoration:none}.button[aria-disabled=true],input:disabled,textarea:disabled,select:disabled,button[disabled]{cursor:not-allowed;background-color:var(--disabled);border-color:var(--disabled);color:var(--text-light)}input[type=range]{padding:0}abbr[title]{cursor:help;text-decoration-line:underline;text-decoration-style:dotted}button:enabled:hover,.button:not([aria-disabled=true]):hover,input[type=submit]:enabled:hover,input[type=reset]:enabled:hover,input[type=button]:enabled:hover,label[type=button]:hover{background-color:var(--accent-hover);border-color:var(--accent-hover);cursor:pointer}.button:focus-visible,button:focus-visible:where(:enabled),input:enabled:focus-visible:where([type=submit],[type=reset],[type=button]){outline:2px solid var(--accent);outline-offset:1px}header>nav{padding:1rem 0 0;font-size:1rem;line-height:2}header>nav ul,header>nav ol{flex-flow:wrap;place-content:space-around center;align-items:center;margin:0;padding:0;list-style-type:none;display:flex}header>nav ul li,header>nav ol li{display:inline-block}header>nav a,header>nav a:visited{border:1px solid var(--border);border-radius:var(--standard-border-radius);color:var(--text);margin:0 .5rem 1rem;padding:.1rem 1rem;text-decoration:none;display:inline-block}header>nav a:hover,header>nav a.current,header>nav a[aria-current=page]{border-color:var(--accent);color:var(--accent);cursor:pointer}@media only screen and (width<=720px){header>nav a{border:none;padding:0;line-height:1;text-decoration:underline}}aside,details,pre,progress{background-color:var(--accent-bg);border:1px solid var(--border);border-radius:var(--standard-border-radius);margin-bottom:1rem}aside{float:right;width:30%;margin-inline-start:15px;padding:0 15px;font-size:1rem}[dir=rtl] aside{float:left}@media only screen and (width<=720px){aside{float:none;width:100%;margin-inline-start:0}}article,fieldset,dialog{border:1px solid var(--border);border-radius:var(--standard-border-radius);margin-bottom:1rem;padding:1rem}article h2:first-child,section h2:first-child,article h3:first-child,section h3:first-child{margin-top:1rem}section{border-top:1px solid var(--border);border-bottom:1px solid var(--border);margin:3rem 0;padding:2rem 1rem}section+section,section:first-child{border-top:0;padding-top:0}section+section{margin-top:0}section:last-child{border-bottom:0;padding-bottom:0}details{padding:.7rem 1rem}summary{cursor:pointer;word-break:break-all;margin:-.7rem -1rem;padding:.7rem 1rem;font-weight:700}details[open]>summary+*{margin-top:0}details[open]>summary{margin-bottom:.5rem}details[open]>:last-child{margin-bottom:0}table{border-collapse:collapse;margin:1.5rem 0}figure>table{width:max-content;margin:0}td,th{border:1px solid var(--border);text-align:start;padding:.5rem}th{background-color:var(--accent-bg);font-weight:700}tr:nth-child(2n){background-color:var(--accent-bg)}table caption{margin-bottom:.5rem;font-weight:700}textarea,select,input,button,.button{font-size:inherit;border-radius:var(--standard-border-radius);box-shadow:none;max-width:100%;margin-bottom:.5rem;padding:.5rem;font-family:inherit;display:inline-block}textarea,select,input{color:var(--text);background-color:var(--bg);border:1px solid var(--border)}label{display:block}textarea:not([cols]){width:100%}select:not([multiple]){background-image:linear-gradient(45deg,transparent 49%,var(--text)51%),linear-gradient(135deg,var(--text)51%,transparent 49%);background-position:calc(100% - 15px),calc(100% - 10px);background-repeat:no-repeat;background-size:5px 5px,5px 5px;padding-inline-end:25px}[dir=rtl] select:not([multiple]){background-position:10px,15px}input[type=checkbox],input[type=radio]{vertical-align:middle;width:min-content;position:relative}input[type=checkbox]+label,input[type=radio]+label{display:inline-block}input[type=radio]{border-radius:100%}input[type=checkbox]:checked,input[type=radio]:checked{background-color:var(--accent)}input[type=checkbox]:checked:after{content:" ";border-right:solid var(--bg).08em;border-bottom:solid var(--bg).08em;background-color:#0000;border-radius:0;width:.18em;height:.32em;font-size:1.8em;position:absolute;top:.05em;left:.17em;transform:rotate(45deg)}input[type=radio]:checked:after{content:" ";background-color:var(--bg);border-radius:100%;width:.25em;height:.25em;font-size:32px;position:absolute;top:.125em;left:.125em}@media only screen and (width<=720px){textarea,select,input{width:100%}}input[type=color]{height:2.5rem;padding:.2rem}input[type=file]{border:0}hr{background:var(--border);border:none;height:1px;margin:1rem auto}mark{border-radius:var(--standard-border-radius);background-color:var(--marked);color:#000;padding:2px 5px}mark a{color:#0d47a1}img,video{border-radius:var(--standard-border-radius);max-width:100%;height:auto}figure{margin:0;display:block;overflow-x:auto}figure>img,figure>picture>img{margin-inline:auto;display:block}figcaption{text-align:center;color:var(--text-light);margin-block:1rem;font-size:.9rem}blockquote{border-inline-start:.35rem solid var(--accent);color:var(--text-light);margin-block:2rem;margin-inline:2rem 0;padding:.4rem .8rem;font-style:italic}cite{color:var(--text-light);font-size:.9rem;font-style:normal}dt{color:var(--text-light)}code,pre,pre span,kbd,samp{font-family:var(--mono-font);color:var(--code)}kbd{color:var(--preformatted);border:1px solid var(--preformatted);border-bottom:3px solid var(--preformatted);border-radius:var(--standard-border-radius);padding:.1rem .4rem}pre{color:var(--preformatted);max-width:100%;padding:1rem 1.4rem;overflow:auto}pre code{color:var(--preformatted);background:0 0;margin:0;padding:0}progress{width:100%}progress:indeterminate{background-color:var(--accent-bg)}progress::-webkit-progress-bar{border-radius:var(--standard-border-radius);background-color:var(--accent-bg)}progress::-webkit-progress-value{border-radius:var(--standard-border-radius);background-color:var(--accent)}progress::-moz-progress-bar{border-radius:var(--standard-border-radius);background-color:var(--accent);transition-property:width;transition-duration:.3s}progress:indeterminate::-moz-progress-bar{background-color:var(--accent-bg)}dialog{max-width:40rem;margin:auto}dialog::backdrop{background-color:var(--bg);opacity:.8}@media only screen and (width<=720px){dialog{max-width:100%;margin:auto 1em}}sup,sub{vertical-align:baseline;position:relative}sup{top:-.4em}sub{top:.3em}.notice{background:var(--accent-bg);border:2px solid var(--border);border-radius:var(--standard-border-radius);margin:2rem 0;padding:1.5rem} \ No newline at end of file diff --git a/backend/app/templates/base.html b/backend/app/templates/base.html index 920f44b..8ecf3bb 100644 --- a/backend/app/templates/base.html +++ b/backend/app/templates/base.html @@ -1,6 +1,7 @@ + {% if title %} {{ title }} - blog {% else %} @@ -8,8 +9,24 @@ {% endif %} -
blgo: home
+
+ blgo: + home + login + +

+ {% with messages = get_flashed_messages() %} + {% if messages %} + + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} diff --git a/backend/app/templates/login.html b/backend/app/templates/login.html new file mode 100644 index 0000000..e159759 --- /dev/null +++ b/backend/app/templates/login.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block content %} +

Sign In

+
+ {{ form.hidden_tag() }} +

+ {{ form.username.label }}
+ {{ form.username(size=32) }}
+ {% for error in form.username.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.password.label }}
+ {{ form.password(size=32) }}
+ {% for error in form.password.errors %} + [{{ error }}] + {% endfor %} +

+

{{ form.remember_me() }} {{ form.remember_me.label }}

+

{{ form.submit }}

+
+ +{% endblock %} diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..6e8209e --- /dev/null +++ b/backend/config.py @@ -0,0 +1,7 @@ +import os + +# Remove or fallbacks for prod + +class Config: + SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') or 'flasksk' + diff --git a/backend/requirements.txt b/backend/requirements.txt index c1e99d5..2517dd8 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,8 +1,10 @@ blinker==1.8.2 click==8.1.7 Flask==3.0.3 +Flask-WTF==1.2.1 itsdangerous==2.2.0 Jinja2==3.1.4 MarkupSafe==2.1.5 python-dotenv==1.0.1 Werkzeug==3.0.3 +WTForms==3.1.2 diff --git a/backend/templates/about.html b/backend/templates/about.html deleted file mode 100644 index 7318c07..0000000 --- a/backend/templates/about.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - -

About

- - - diff --git a/backend/templates/basictemp.html b/backend/templates/basictemp.html deleted file mode 100644 index cbbcf44..0000000 --- a/backend/templates/basictemp.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/backend/templates/home.html b/backend/templates/home.html deleted file mode 100644 index fa70d65..0000000 --- a/backend/templates/home.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - -

Home

- - - From 76b20a3b384a94ffda17522913697ddd16a99319 Mon Sep 17 00:00:00 2001 From: finn Date: Thu, 1 Aug 2024 03:53:33 -0700 Subject: [PATCH 03/25] scss work --- backend/README.md | 6 +++- backend/app/static/simple.css | 52 ++++++++++++++++--------------- backend/app/static/simple.min.css | 1 - 3 files changed, 32 insertions(+), 27 deletions(-) delete mode 100644 backend/app/static/simple.min.css diff --git a/backend/README.md b/backend/README.md index c3fdd6b..bfc2438 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,4 +1,8 @@ -pip: +*pip:\ + +``` pip install flask pip install python-dotenv +pip install flask-wtf +``` diff --git a/backend/app/static/simple.css b/backend/app/static/simple.css index efe643b..8874b1a 100644 --- a/backend/app/static/simple.css +++ b/backend/app/static/simple.css @@ -8,39 +8,41 @@ --mono-font: Consolas, Menlo, Monaco, "Andale Mono", "Ubuntu Mono", monospace; --standard-border-radius: 5px; - /* Default (light) theme */ - --bg: #fff; - --accent-bg: #f5f7ff; - --text: #212121; - --text-light: #585858; + --bg: #212121; + --accent-bg: #2b2b2b; + --text: #dcdcdc; + --text-light: #ababab; --border: #898EA4; - --accent: #0d47a1; - --accent-hover: #1266e2; + /* --accent: #ffb300; */ + --accent: #ace1af; + /* --accent-hover: #ffe099; */ + --accent-hover: #99dabe; --accent-text: var(--bg); - --code: #d81b60; - --preformatted: #444; + --code: #f06292; + --preformatted: #ccc; --marked: #ffdd33; - --disabled: #efefef; + --disabled: #111; + } -/* Dark theme */ -@media (prefers-color-scheme: dark) { +/* Light theme */ +@media (prefers-color-scheme: light) { :root, ::backdrop { - color-scheme: dark; - --bg: #212121; - --accent-bg: #2b2b2b; - --text: #dcdcdc; - --text-light: #ababab; - /* --accent: #ffb300; */ - --accent: #ace1af; - /* --accent-hover: #ffe099; */ - --accent-hover: #99dabe; + color-scheme: light; + /* Default (light) theme */ + --bg: #fff; + --accent-bg: #f5f7ff; + --text: #212121; + --text-light: #585858; + --accent: #0d47a1; + --accent-hover: #1266e2; --accent-text: var(--bg); - --code: #f06292; - --preformatted: #ccc; - --disabled: #111; - } + --code: #d81b60; + --preformatted: #444; + --disabled: #efefef; + + } /* Add a bit of transparency so light media isn't so glaring in dark mode */ img, video { diff --git a/backend/app/static/simple.min.css b/backend/app/static/simple.min.css deleted file mode 100644 index 9db8fe6..0000000 --- a/backend/app/static/simple.min.css +++ /dev/null @@ -1 +0,0 @@ -:root,::backdrop{--sans-font:-apple-system,BlinkMacSystemFont,"Avenir Next",Avenir,"Nimbus Sans L",Roboto,"Noto Sans","Segoe UI",Arial,Helvetica,"Helvetica Neue",sans-serif;--mono-font:Consolas,Menlo,Monaco,"Andale Mono","Ubuntu Mono",monospace;--standard-border-radius:5px;--bg:#fff;--accent-bg:#f5f7ff;--text:#212121;--text-light:#585858;--border:#898ea4;--accent:#0d47a1;--accent-hover:#1266e2;--accent-text:var(--bg);--code:#d81b60;--preformatted:#444;--marked:#fd3;--disabled:#efefef}@media (prefers-color-scheme:dark){:root,::backdrop{color-scheme:dark;--bg:#212121;--accent-bg:#2b2b2b;--text:#dcdcdc;--text-light:#ababab;--accent:#ffb300;--accent-hover:#ffe099;--accent-text:var(--bg);--code:#f06292;--preformatted:#ccc;--disabled:#111}img,video{opacity:.8}}*,:before,:after{box-sizing:border-box}textarea,select,input,progress{-webkit-appearance:none;-moz-appearance:none;appearance:none}html{font-family:var(--sans-font);scroll-behavior:smooth}body{color:var(--text);background-color:var(--bg);grid-template-columns:1fr min(45rem,90%) 1fr;margin:0;font-size:1.15rem;line-height:1.5;display:grid}body>*{grid-column:2}body>header{background-color:var(--accent-bg);border-bottom:1px solid var(--border);text-align:center;grid-column:1/-1;padding:0 .5rem 2rem}body>header>:only-child{margin-block-start:2rem}body>header h1{max-width:1200px;margin:1rem auto}body>header p{max-width:40rem;margin:1rem auto}main{padding-top:1.5rem}body>footer{color:var(--text-light);text-align:center;border-top:1px solid var(--border);margin-top:4rem;padding:2rem 1rem 1.5rem;font-size:.9rem}h1{font-size:3rem}h2{margin-top:3rem;font-size:2.6rem}h3{margin-top:3rem;font-size:2rem}h4{font-size:1.44rem}h5{font-size:1.15rem}h6{font-size:.96rem}p{margin:1.5rem 0}p,h1,h2,h3,h4,h5,h6{overflow-wrap:break-word}h1,h2,h3{line-height:1.1}@media only screen and (width<=720px){h1{font-size:2.5rem}h2{font-size:2.1rem}h3{font-size:1.75rem}h4{font-size:1.25rem}}a,a:visited{color:var(--accent)}a:hover{text-decoration:none}button,.button,a.button,input[type=submit],input[type=reset],input[type=button],label[type=button]{border:1px solid var(--accent);background-color:var(--accent);color:var(--accent-text);padding:.5rem .9rem;line-height:normal;text-decoration:none}.button[aria-disabled=true],input:disabled,textarea:disabled,select:disabled,button[disabled]{cursor:not-allowed;background-color:var(--disabled);border-color:var(--disabled);color:var(--text-light)}input[type=range]{padding:0}abbr[title]{cursor:help;text-decoration-line:underline;text-decoration-style:dotted}button:enabled:hover,.button:not([aria-disabled=true]):hover,input[type=submit]:enabled:hover,input[type=reset]:enabled:hover,input[type=button]:enabled:hover,label[type=button]:hover{background-color:var(--accent-hover);border-color:var(--accent-hover);cursor:pointer}.button:focus-visible,button:focus-visible:where(:enabled),input:enabled:focus-visible:where([type=submit],[type=reset],[type=button]){outline:2px solid var(--accent);outline-offset:1px}header>nav{padding:1rem 0 0;font-size:1rem;line-height:2}header>nav ul,header>nav ol{flex-flow:wrap;place-content:space-around center;align-items:center;margin:0;padding:0;list-style-type:none;display:flex}header>nav ul li,header>nav ol li{display:inline-block}header>nav a,header>nav a:visited{border:1px solid var(--border);border-radius:var(--standard-border-radius);color:var(--text);margin:0 .5rem 1rem;padding:.1rem 1rem;text-decoration:none;display:inline-block}header>nav a:hover,header>nav a.current,header>nav a[aria-current=page]{border-color:var(--accent);color:var(--accent);cursor:pointer}@media only screen and (width<=720px){header>nav a{border:none;padding:0;line-height:1;text-decoration:underline}}aside,details,pre,progress{background-color:var(--accent-bg);border:1px solid var(--border);border-radius:var(--standard-border-radius);margin-bottom:1rem}aside{float:right;width:30%;margin-inline-start:15px;padding:0 15px;font-size:1rem}[dir=rtl] aside{float:left}@media only screen and (width<=720px){aside{float:none;width:100%;margin-inline-start:0}}article,fieldset,dialog{border:1px solid var(--border);border-radius:var(--standard-border-radius);margin-bottom:1rem;padding:1rem}article h2:first-child,section h2:first-child,article h3:first-child,section h3:first-child{margin-top:1rem}section{border-top:1px solid var(--border);border-bottom:1px solid var(--border);margin:3rem 0;padding:2rem 1rem}section+section,section:first-child{border-top:0;padding-top:0}section+section{margin-top:0}section:last-child{border-bottom:0;padding-bottom:0}details{padding:.7rem 1rem}summary{cursor:pointer;word-break:break-all;margin:-.7rem -1rem;padding:.7rem 1rem;font-weight:700}details[open]>summary+*{margin-top:0}details[open]>summary{margin-bottom:.5rem}details[open]>:last-child{margin-bottom:0}table{border-collapse:collapse;margin:1.5rem 0}figure>table{width:max-content;margin:0}td,th{border:1px solid var(--border);text-align:start;padding:.5rem}th{background-color:var(--accent-bg);font-weight:700}tr:nth-child(2n){background-color:var(--accent-bg)}table caption{margin-bottom:.5rem;font-weight:700}textarea,select,input,button,.button{font-size:inherit;border-radius:var(--standard-border-radius);box-shadow:none;max-width:100%;margin-bottom:.5rem;padding:.5rem;font-family:inherit;display:inline-block}textarea,select,input{color:var(--text);background-color:var(--bg);border:1px solid var(--border)}label{display:block}textarea:not([cols]){width:100%}select:not([multiple]){background-image:linear-gradient(45deg,transparent 49%,var(--text)51%),linear-gradient(135deg,var(--text)51%,transparent 49%);background-position:calc(100% - 15px),calc(100% - 10px);background-repeat:no-repeat;background-size:5px 5px,5px 5px;padding-inline-end:25px}[dir=rtl] select:not([multiple]){background-position:10px,15px}input[type=checkbox],input[type=radio]{vertical-align:middle;width:min-content;position:relative}input[type=checkbox]+label,input[type=radio]+label{display:inline-block}input[type=radio]{border-radius:100%}input[type=checkbox]:checked,input[type=radio]:checked{background-color:var(--accent)}input[type=checkbox]:checked:after{content:" ";border-right:solid var(--bg).08em;border-bottom:solid var(--bg).08em;background-color:#0000;border-radius:0;width:.18em;height:.32em;font-size:1.8em;position:absolute;top:.05em;left:.17em;transform:rotate(45deg)}input[type=radio]:checked:after{content:" ";background-color:var(--bg);border-radius:100%;width:.25em;height:.25em;font-size:32px;position:absolute;top:.125em;left:.125em}@media only screen and (width<=720px){textarea,select,input{width:100%}}input[type=color]{height:2.5rem;padding:.2rem}input[type=file]{border:0}hr{background:var(--border);border:none;height:1px;margin:1rem auto}mark{border-radius:var(--standard-border-radius);background-color:var(--marked);color:#000;padding:2px 5px}mark a{color:#0d47a1}img,video{border-radius:var(--standard-border-radius);max-width:100%;height:auto}figure{margin:0;display:block;overflow-x:auto}figure>img,figure>picture>img{margin-inline:auto;display:block}figcaption{text-align:center;color:var(--text-light);margin-block:1rem;font-size:.9rem}blockquote{border-inline-start:.35rem solid var(--accent);color:var(--text-light);margin-block:2rem;margin-inline:2rem 0;padding:.4rem .8rem;font-style:italic}cite{color:var(--text-light);font-size:.9rem;font-style:normal}dt{color:var(--text-light)}code,pre,pre span,kbd,samp{font-family:var(--mono-font);color:var(--code)}kbd{color:var(--preformatted);border:1px solid var(--preformatted);border-bottom:3px solid var(--preformatted);border-radius:var(--standard-border-radius);padding:.1rem .4rem}pre{color:var(--preformatted);max-width:100%;padding:1rem 1.4rem;overflow:auto}pre code{color:var(--preformatted);background:0 0;margin:0;padding:0}progress{width:100%}progress:indeterminate{background-color:var(--accent-bg)}progress::-webkit-progress-bar{border-radius:var(--standard-border-radius);background-color:var(--accent-bg)}progress::-webkit-progress-value{border-radius:var(--standard-border-radius);background-color:var(--accent)}progress::-moz-progress-bar{border-radius:var(--standard-border-radius);background-color:var(--accent);transition-property:width;transition-duration:.3s}progress:indeterminate::-moz-progress-bar{background-color:var(--accent-bg)}dialog{max-width:40rem;margin:auto}dialog::backdrop{background-color:var(--bg);opacity:.8}@media only screen and (width<=720px){dialog{max-width:100%;margin:auto 1em}}sup,sub{vertical-align:baseline;position:relative}sup{top:-.4em}sub{top:.3em}.notice{background:var(--accent-bg);border:2px solid var(--border);border-radius:var(--standard-border-radius);margin:2rem 0;padding:1.5rem} \ No newline at end of file From 3162b662b5254e78301d3dc2c246e30184476b17 Mon Sep 17 00:00:00 2001 From: finn Date: Thu, 1 Aug 2024 06:35:28 -0700 Subject: [PATCH 04/25] mgt c4 --- .gitignore | 2 +- backend/README.md | 17 ++- backend/app/__init__.py | 6 +- backend/app/models.py | 27 +++++ backend/config.py | 4 + backend/instance/app.db | Bin 0 -> 24576 bytes backend/microblog.py | 10 +- backend/migrations/README | 1 + backend/migrations/alembic.ini | 50 ++++++++ backend/migrations/env.py | 113 ++++++++++++++++++ backend/migrations/script.py.mako | 24 ++++ .../versions/938ae2fee021_posts_table.py | 43 +++++++ .../versions/e42cf202d424_users_table.py | 42 +++++++ backend/requirements.txt | 7 ++ 14 files changed, 342 insertions(+), 4 deletions(-) create mode 100644 backend/app/models.py create mode 100644 backend/instance/app.db create mode 100644 backend/migrations/README create mode 100644 backend/migrations/alembic.ini create mode 100644 backend/migrations/env.py create mode 100644 backend/migrations/script.py.mako create mode 100644 backend/migrations/versions/938ae2fee021_posts_table.py create mode 100644 backend/migrations/versions/e42cf202d424_users_table.py diff --git a/.gitignore b/.gitignore index bd1860d..850c508 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ gitea/ .env pmb-pf/ venv - +zapp.db diff --git a/backend/README.md b/backend/README.md index bfc2438..db2552a 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,8 +1,23 @@ -*pip:\ +pip: + ``` pip install flask pip install python-dotenv pip install flask-wtf +pip install flask-sqlalchemy +pip install flask-migrate +pip freeze > requirements.txt ``` + +db: + +``` +flask db migrate -m "users table" +flask db upgrade + +flask db downgrade [base] +flask db upgrade +``` + diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 14ca068..ecf426c 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -1,8 +1,12 @@ from flask import Flask from config import Config +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate app = Flask(__name__) app.config.from_object(Config) +db = SQLAlchemy(app) +migrate = Migrate(app, db) -from app import routes +from app import routes, models diff --git a/backend/app/models.py b/backend/app/models.py new file mode 100644 index 0000000..3f4e1f5 --- /dev/null +++ b/backend/app/models.py @@ -0,0 +1,27 @@ +from datetime import datetime, timezone +from typing import Optional +import sqlalchemy as sa +import sqlalchemy.orm as so +from app import db + +class User(db.Model): + id: so.Mapped[int] = so.mapped_column(primary_key=True) + username: so.Mapped[str] = so.mapped_column(sa.String(64), index=True, unique=True) + email: so.Mapped[str] = so.mapped_column(sa.String(120), index=True, unique=True) + password_hash: so.Mapped[Optional[str]] = so.mapped_column(sa.String(256)) + posts: so.WriteOnlyMapped['Post'] = so.relationship(back_populates='author') + + def __repr__(self): + return ''.format(self.username) + +class Post(db.Model): + id: so.Mapped[int] = so.mapped_column(primary_key=True) + body: so.Mapped[str] = so.mapped_column(sa.String(140)) + timestamp: so.Mapped[datetime] = so.mapped_column(index=True, default=lambda: datetime.now(timezone.utc)) + user_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id), index=True) + author: so.Mapped[User] = so.relationship(back_populates='posts') + + def __repr__(self): + return ''.format(self.body) + + diff --git a/backend/config.py b/backend/config.py index 6e8209e..6948de2 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,7 +1,11 @@ import os +basedir = os.path.abspath(os.path.dirname(__file__)) # Remove or fallbacks for prod class Config: SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') or 'flasksk' + SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') + #SQLALCHEMY_DATABASE_URI = 'db' + diff --git a/backend/instance/app.db b/backend/instance/app.db new file mode 100644 index 0000000000000000000000000000000000000000..75d140e0e176bf9161f74afe13d2c52fd0d2f19a GIT binary patch literal 24576 zcmeI&J#X4T7{GBKZ>f=hE{OG(s93aBa7eaFrPOLssVNS$&15;?C{}q%F|?67C7-Bk zzg5SMJv)%b9?~8CPn@~K=f}_e&dbT140^%Jm*OKmY**5I_I{1Q0*~f&U16$`@+S zs^Uj5pZHh7mAel8Wqg=>Ge7DSI*!#IT5@D}2O~>%?bp_4nI0(nwu}bLdUT8~HSAoW z_M|EniNOBM3#QSK{6kUWQU5~5UE{i$-K$lr;>KEeXVW;fC{S(_XxM($vo<*t^%qh*8rO!{t2G+pMvrs$rvB_K7`xy6Wf&~xsct4QlhjFB zFY24i-1&NTcke?pnPI1IpA4P0n!HR!a4)}%Wg^9U@l>uaGKJcpA+&hC!mm@ci+8>C z>URCybg*XXhpc+c9z~B@NmWt#t*H+M0R#|0009ILKmY**5I_I{1a?HAEHe9k%N(DZ z2j-+@wu)+A?EiOUahV1J2q1s}0tg_000IagfB*s;fl78?zrX*F-v6sV{ZkM?009IL zKmY**5I_I{1Q0-AhXtbd|Lp&Fc!8N30tg_000IagfB*srAb;&MW`` literal 0 HcmV?d00001 diff --git a/backend/microblog.py b/backend/microblog.py index f099917..3bf8aa5 100644 --- a/backend/microblog.py +++ b/backend/microblog.py @@ -1,2 +1,10 @@ -from app import app +import sqlalchemy as sa +import sqlalchemy.orm as so +from app import app, db +from app.models import User, Post + +@app.shell_context_processor +def make_shell_context(): + return {'sa': sa, 'so': so, 'db': db, 'User': User, 'Post': Post} + diff --git a/backend/migrations/README b/backend/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/backend/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/backend/migrations/alembic.ini b/backend/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/backend/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/migrations/env.py b/backend/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/backend/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/migrations/script.py.mako b/backend/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/backend/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/backend/migrations/versions/938ae2fee021_posts_table.py b/backend/migrations/versions/938ae2fee021_posts_table.py new file mode 100644 index 0000000..8d70d1e --- /dev/null +++ b/backend/migrations/versions/938ae2fee021_posts_table.py @@ -0,0 +1,43 @@ +"""posts table + +Revision ID: 938ae2fee021 +Revises: e42cf202d424 +Create Date: 2024-08-01 06:11:07.414657 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '938ae2fee021' +down_revision = 'e42cf202d424' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('post', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('body', sa.String(length=140), nullable=False), + sa.Column('timestamp', sa.DateTime(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('post', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_post_timestamp'), ['timestamp'], unique=False) + batch_op.create_index(batch_op.f('ix_post_user_id'), ['user_id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('post', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_post_user_id')) + batch_op.drop_index(batch_op.f('ix_post_timestamp')) + + op.drop_table('post') + # ### end Alembic commands ### diff --git a/backend/migrations/versions/e42cf202d424_users_table.py b/backend/migrations/versions/e42cf202d424_users_table.py new file mode 100644 index 0000000..057534c --- /dev/null +++ b/backend/migrations/versions/e42cf202d424_users_table.py @@ -0,0 +1,42 @@ +"""users table + +Revision ID: e42cf202d424 +Revises: +Create Date: 2024-08-01 05:46:32.176166 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'e42cf202d424' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=64), nullable=False), + sa.Column('email', sa.String(length=120), nullable=False), + sa.Column('password_hash', sa.String(length=256), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_user_email'), ['email'], unique=True) + batch_op.create_index(batch_op.f('ix_user_username'), ['username'], unique=True) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_user_username')) + batch_op.drop_index(batch_op.f('ix_user_email')) + + op.drop_table('user') + # ### end Alembic commands ### diff --git a/backend/requirements.txt b/backend/requirements.txt index 2517dd8..77e38d7 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,10 +1,17 @@ +alembic==1.13.2 blinker==1.8.2 click==8.1.7 Flask==3.0.3 +Flask-Migrate==4.0.7 +Flask-SQLAlchemy==3.1.1 Flask-WTF==1.2.1 +greenlet==3.0.3 itsdangerous==2.2.0 Jinja2==3.1.4 +Mako==1.3.5 MarkupSafe==2.1.5 python-dotenv==1.0.1 +SQLAlchemy==2.0.31 +typing_extensions==4.12.2 Werkzeug==3.0.3 WTForms==3.1.2 From 9db540fc1ce062178c4231d36688206575079159 Mon Sep 17 00:00:00 2001 From: finn Date: Thu, 1 Aug 2024 11:10:49 -0700 Subject: [PATCH 05/25] login and register --- backend/README.md | 3 ++ backend/app/__init__.py | 3 ++ backend/app/forms.py | 21 +++++++++++++- backend/app/models.py | 13 +++++++-- backend/app/routes.py | 45 +++++++++++++++++++++++++---- backend/app/templates/base.html | 6 +++- backend/app/templates/index.html | 2 +- backend/app/templates/login.html | 3 ++ backend/app/templates/register.html | 40 +++++++++++++++++++++++++ backend/requirements.txt | 4 +++ 10 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 backend/app/templates/register.html diff --git a/backend/README.md b/backend/README.md index db2552a..fcc71d9 100644 --- a/backend/README.md +++ b/backend/README.md @@ -8,6 +8,9 @@ pip install python-dotenv pip install flask-wtf pip install flask-sqlalchemy pip install flask-migrate +pip install flask-login +pip install email-validator + pip freeze > requirements.txt ``` diff --git a/backend/app/__init__.py b/backend/app/__init__.py index ecf426c..d57d604 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -2,11 +2,14 @@ from flask import Flask from config import Config from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate +from flask_login import LoginManager app = Flask(__name__) app.config.from_object(Config) db = SQLAlchemy(app) migrate = Migrate(app, db) +login = LoginManager(app) +login.login_view = 'login' from app import routes, models diff --git a/backend/app/forms.py b/backend/app/forms.py index 7462ea6..fe31c0e 100644 --- a/backend/app/forms.py +++ b/backend/app/forms.py @@ -1,6 +1,9 @@ from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, BooleanField, SubmitField -from wtforms.validators import DataRequired +from wtforms.validators import DataRequired, ValidationError, Email, EqualTo +import sqlalchemy as sa +from app import db +from app.models import User class LoginForm(FlaskForm): username = StringField('Username', validators=[DataRequired()]) @@ -8,3 +11,19 @@ class LoginForm(FlaskForm): remember_me = BooleanField('Remember Me') submit = SubmitField('Sign In') +class RegistrationForm(FlaskForm): + username = StringField('Username', validators=[DataRequired()]) + email = StringField('Email', validators=[DataRequired(), Email()]) + password = PasswordField('Password', validators=[DataRequired()]) + password2 = PasswordField('Repeat Password', validators=[DataRequired()]) + submit = SubmitField('Register') + + def validate_username(self, username): + user = db.session.scalar(sa.select(User).where(User.username == username.data)) + if user is not None: + raise ValidationError('Please use a different username.') + + def validate_email(self, email): + user = db.session.scalar(sa.select(User).where(User.email == email.data)) + if user is not None: + raise ValidationError('Please use a different email address.') diff --git a/backend/app/models.py b/backend/app/models.py index 3f4e1f5..36cbb11 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -2,15 +2,21 @@ from datetime import datetime, timezone from typing import Optional import sqlalchemy as sa import sqlalchemy.orm as so -from app import db +from app import db, login +from werkzeug.security import generate_password_hash, check_password_hash +from flask_login import UserMixin -class User(db.Model): +class User(UserMixin, db.Model): id: so.Mapped[int] = so.mapped_column(primary_key=True) username: so.Mapped[str] = so.mapped_column(sa.String(64), index=True, unique=True) email: so.Mapped[str] = so.mapped_column(sa.String(120), index=True, unique=True) password_hash: so.Mapped[Optional[str]] = so.mapped_column(sa.String(256)) posts: so.WriteOnlyMapped['Post'] = so.relationship(back_populates='author') + def set_password(self, password): + self.password_hash = generate_password_hash(password) + def check_password(self, password): + return check_password_hash(self.password_hash, password) def __repr__(self): return ''.format(self.username) @@ -25,3 +31,6 @@ class Post(db.Model): return ''.format(self.body) +@login.user_loader +def load_user(id): + return db.session.get(User, int(id)) diff --git a/backend/app/routes.py b/backend/app/routes.py index 5e2a53f..72d750a 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -1,9 +1,14 @@ -from flask import render_template, flash, redirect, url_for -from app import app -from app.forms import LoginForm +from flask import render_template, flash, redirect, url_for, request +from urllib.parse import urlsplit +from app import app, db +from app.forms import LoginForm, RegistrationForm +from flask_login import current_user, login_user, logout_user, login_required +import sqlalchemy as sa +from app.models import User @app.route('/') @app.route('/index') +@login_required def index(): user = {'username': 'Finnaa'} @@ -18,14 +23,42 @@ def index(): } ] #return posts; - return render_template('index.html', title='Home', user=user, posts=posts) + return render_template('index.html', title='Home', posts=posts) @app.route('/login', methods=['GET', 'POST']) def login(): + if current_user.is_authenticated: + return redirect(url_for('index')) form = LoginForm() if form.validate_on_submit(): - flash('Login requested for user {}, remember_me={}'.format(form.username.data, form.remember_me.data)) - return redirect(url_for('index')) + user = db.session.scalar(sa.select(User).where(User.username == form.username.data)) + if user is None or not user.check_password(form.password.data): + flash('Invalid u or p') + return redirect(url_for('login')) + login_user(user, remember=form.remember_me.data) + next_page = request.args.get('next') + if not next_page or urlsplit(next_page).netloc != '': + next_page = url_for('index') + return redirect(next_page) return render_template('login.html', title='Sign In', form=form) +@app.route('/logout') +def logout(): + logout_user() + return redirect(url_for('index')) + +@app.route('/register', methods=['GET', 'POST']) +def register(): + if current_user.is_authenticated: + return redirect(url_for('index')) + form = RegistrationForm() + if form.validate_on_submit(): + user = User(username=form.username.data, email=form.email.data) + user.set_password(form.password.data) + db.session.add(user) + db.session.commit() + flash('User has been created.') + return redirect(url_for('login')) + return render_template('register.html', title='Register', form=form) + diff --git a/backend/app/templates/base.html b/backend/app/templates/base.html index 8ecf3bb..e83424e 100644 --- a/backend/app/templates/base.html +++ b/backend/app/templates/base.html @@ -12,7 +12,11 @@
blgo: home + {% if current_user.is_anonymous %} login + {% else %} + logout + {% endif %}

@@ -20,7 +24,7 @@ {% if messages %}
    {% for message in messages %} -
  • {{ message }}
  • +

    {{ message }}

    {% endfor %}
{% endif %} diff --git a/backend/app/templates/index.html b/backend/app/templates/index.html index ab4fd46..e2b6d0b 100644 --- a/backend/app/templates/index.html +++ b/backend/app/templates/index.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block content %} -

Helloo, {{ user.username }}!

+

Hello, {{ current_user.username }}!

{% for post in posts %}

{{ post.author.username }} says: {{ post.body }}

{% endfor %} diff --git a/backend/app/templates/login.html b/backend/app/templates/login.html index e159759..c604808 100644 --- a/backend/app/templates/login.html +++ b/backend/app/templates/login.html @@ -22,4 +22,7 @@

{{ form.submit }}

+

Register Here

+ + {% endblock %} diff --git a/backend/app/templates/register.html b/backend/app/templates/register.html new file mode 100644 index 0000000..02b4226 --- /dev/null +++ b/backend/app/templates/register.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{% block content %} + +

Register

+
+ {{ form.hidden_tag() }} +

+ {{ form.username.label }}
+ {{ form.username(size=32) }}
+ {% for error in form.username.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.email.label }}
+ {{ form.email(size=64) }}
+ {% for error in form.email.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.password.label }}
+ {{ form.password(size=64) }}
+ {% for error in form.password.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.password2.label }}
+ {{ form.password2(size=64) }}
+ {% for error in form.password2.errors %} + [{{ error }}] + {% endfor %} +

+

{{ form.submit }}

+
+ + +{% endblock %} diff --git a/backend/requirements.txt b/backend/requirements.txt index 77e38d7..98e3753 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,11 +1,15 @@ alembic==1.13.2 blinker==1.8.2 click==8.1.7 +dnspython==2.6.1 +email_validator==2.2.0 Flask==3.0.3 +Flask-Login==0.6.3 Flask-Migrate==4.0.7 Flask-SQLAlchemy==3.1.1 Flask-WTF==1.2.1 greenlet==3.0.3 +idna==3.7 itsdangerous==2.2.0 Jinja2==3.1.4 Mako==1.3.5 From 52c1e056c1237ede2e2d1e2126a7fe41355fc703 Mon Sep 17 00:00:00 2001 From: finn Date: Thu, 1 Aug 2024 17:48:00 -0700 Subject: [PATCH 06/25] minor db config --- backend/config.py | 2 +- backend/instance/app.db | Bin 24576 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 backend/instance/app.db diff --git a/backend/config.py b/backend/config.py index 6948de2..8de6d47 100644 --- a/backend/config.py +++ b/backend/config.py @@ -6,6 +6,6 @@ basedir = os.path.abspath(os.path.dirname(__file__)) class Config: SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') or 'flasksk' SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') - #SQLALCHEMY_DATABASE_URI = 'db' + #SQLALCHEMY_DATABASE_URI = 'mysql://flasku:' + os.environ.get('MYSQL_PASSWORD') + '@db:3306/flask' diff --git a/backend/instance/app.db b/backend/instance/app.db deleted file mode 100644 index 75d140e0e176bf9161f74afe13d2c52fd0d2f19a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24576 zcmeI&J#X4T7{GBKZ>f=hE{OG(s93aBa7eaFrPOLssVNS$&15;?C{}q%F|?67C7-Bk zzg5SMJv)%b9?~8CPn@~K=f}_e&dbT140^%Jm*OKmY**5I_I{1Q0*~f&U16$`@+S zs^Uj5pZHh7mAel8Wqg=>Ge7DSI*!#IT5@D}2O~>%?bp_4nI0(nwu}bLdUT8~HSAoW z_M|EniNOBM3#QSK{6kUWQU5~5UE{i$-K$lr;>KEeXVW;fC{S(_XxM($vo<*t^%qh*8rO!{t2G+pMvrs$rvB_K7`xy6Wf&~xsct4QlhjFB zFY24i-1&NTcke?pnPI1IpA4P0n!HR!a4)}%Wg^9U@l>uaGKJcpA+&hC!mm@ci+8>C z>URCybg*XXhpc+c9z~B@NmWt#t*H+M0R#|0009ILKmY**5I_I{1a?HAEHe9k%N(DZ z2j-+@wu)+A?EiOUahV1J2q1s}0tg_000IagfB*s;fl78?zrX*F-v6sV{ZkM?009IL zKmY**5I_I{1Q0-AhXtbd|Lp&Fc!8N30tg_000IagfB*srAb;&MW`` From a8068c85788a661e68f2361cd07fc14dab1a2bb2 Mon Sep 17 00:00:00 2001 From: finn Date: Thu, 1 Aug 2024 22:21:07 -0700 Subject: [PATCH 07/25] mgt doc updates --- backend/Dockerfile | 1 + backend/README.md | 28 ++++++++++++++++++++++----- backend/README.md.backup | 42 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 backend/README.md.backup diff --git a/backend/Dockerfile b/backend/Dockerfile index 6e581ef..36d6f5c 100755 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -6,6 +6,7 @@ COPY requirements.txt /code RUN target=/root/.cache/pip \ pip3 install -r requirements.txt +# Need to make this explicit as part of expansion, no migrations or venv COPY . . ENV FLASK_APP app.py diff --git a/backend/README.md b/backend/README.md index fcc71d9..0fc2bd1 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,7 +1,13 @@ - -pip: +## Workflow: +- should work with flask run locally and dockerfile build +- local dev +- local / venv pip install +- record versionless pips manually here +- pip freeze snapshots into project requirements +- docker build then copies frozen requirements +## pip: ``` pip install flask pip install python-dotenv @@ -10,12 +16,11 @@ pip install flask-sqlalchemy pip install flask-migrate pip install flask-login pip install email-validator - +... pip freeze > requirements.txt ``` -db: - +## db cheat: ``` flask db migrate -m "users table" flask db upgrade @@ -23,4 +28,17 @@ flask db upgrade flask db downgrade [base] flask db upgrade ``` +## build notes: +Dockerfile needs dockerignore or preferably explicitly defined copies for: +- app +- config +- project dir +- requirements +- not dotflaskenv, vars set with dockerfile + +## notes: +- compose has entry that overrides flask with uwsgi for prod +- miminal environment vars come through project env, pass through compose +- no dotenv here, dotflaskenv goes into image +- keep env untracked but templated, dotflaskenv is tracked and public diff --git a/backend/README.md.backup b/backend/README.md.backup new file mode 100644 index 0000000..480b4e2 --- /dev/null +++ b/backend/README.md.backup @@ -0,0 +1,42 @@ +## Workflow: +- should work with flask run and dockerfile build +- local dev +- local pip install +- record versionless pips manually here +- pip freeze snapshots into project requirements +- docker build copies frozen requirements + + +## pip: +``` +pip install flask +pip install python-dotenv +pip install flask-wtf +pip install flask-sqlalchemy +pip install flask-migrate +pip install flask-login +pip install email-validator + +pip freeze > requirements.txt +``` + +## db cheat: +``` +flask db migrate -m "users table" +flask db upgrade + +flask db downgrade [base] +flask db upgrade +``` +## build: + +Dockerfile needs explicitly defined copies for: +- app +- config +- project dir +- requirements + +## notes: +- environment comes through project env passes through compose +- keep env untracked but templated +- no dotenv here, dotflaskenv goes into image From cdebe081c3fcd25b21e4d84c2b298fd0b442a025 Mon Sep 17 00:00:00 2001 From: finn Date: Sat, 3 Aug 2024 02:14:20 -0700 Subject: [PATCH 08/25] mgt c6 checkpoint --- backend/README.md | 1 + backend/app/models.py | 32 +++++++++++++++++- backend/app/routes.py | 11 +++++- backend/app/templates/_post.html | 6 ++++ backend/app/templates/base.html | 1 + backend/app/templates/user.html | 15 ++++++++ .../39466ff4e782fc398fed2c3b21d53ba2.png | Bin 0 -> 384 bytes .../686fed96bd36dc9ba0c0434036031942.png | Bin 0 -> 397 bytes .../7a651a7bab949a73092e21ad0bd2f4a8.png | Bin 0 -> 378 bytes backend/requirements.txt | 2 ++ 10 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 backend/app/templates/_post.html create mode 100644 backend/app/templates/user.html create mode 100644 backend/app/usercontent/identicon/39466ff4e782fc398fed2c3b21d53ba2.png create mode 100644 backend/app/usercontent/identicon/686fed96bd36dc9ba0c0434036031942.png create mode 100644 backend/app/usercontent/identicon/7a651a7bab949a73092e21ad0bd2f4a8.png diff --git a/backend/README.md b/backend/README.md index 0fc2bd1..07ea60b 100644 --- a/backend/README.md +++ b/backend/README.md @@ -16,6 +16,7 @@ pip install flask-sqlalchemy pip install flask-migrate pip install flask-login pip install email-validator +pip install pydenticon ... pip freeze > requirements.txt ``` diff --git a/backend/app/models.py b/backend/app/models.py index 36cbb11..36c70a5 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -1,9 +1,12 @@ +import os from datetime import datetime, timezone from typing import Optional import sqlalchemy as sa import sqlalchemy.orm as so -from app import db, login from werkzeug.security import generate_password_hash, check_password_hash +import pydenticon, hashlib, base64 + +from app import db, login from flask_login import UserMixin class User(UserMixin, db.Model): @@ -17,6 +20,33 @@ class User(UserMixin, db.Model): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) + def gen_avatar(self, write_png=True): + foreground = [ "rgb(45,79,255)", + "rgb(254,180,44)", + "rgb(226,121,234)", + "rgb(30,179,253)", + "rgb(232,77,65)", + "rgb(49,203,115)", + "rgb(141,69,170)" ] + background = "rgb(22,22,22)" + + digest = hashlib.md5(self.email.lower().encode('utf-8')).hexdigest() + basedir = os.path.abspath(os.path.dirname(__file__)) + pngloc = os.path.join(basedir, 'usercontent', 'identicon', str(digest) + '.png') + icongen = pydenticon.Generator(5, 5, digest=hashlib.md5, foreground=foreground, background=background) + pngicon = icongen.generate(self.email, 120, 120, padding=(10, 10, 10, 10), inverted=False, output_format="png") + if write_png: + pngfile = open(pngloc, "wb") + pngfile.write(pngicon) + pngfile.close() + else: + return str(base64.b64encode(pngicon))[2:-1] + def avatar(self): + digest = hashlib.md5(self.email.lower().encode('utf-8')).hexdigest() + basedir = os.path.abspath(os.path.dirname(__file__)) + pngloc = os.path.join(basedir, 'usercontent', 'identicon', digest + '.png') + return pngloc + def __repr__(self): return ''.format(self.username) diff --git a/backend/app/routes.py b/backend/app/routes.py index 72d750a..e301aaf 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -57,8 +57,17 @@ def register(): user.set_password(form.password.data) db.session.add(user) db.session.commit() + user.gen_avatar() flash('User has been created.') return redirect(url_for('login')) return render_template('register.html', title='Register', form=form) - +@app.route('/user/') +@login_required +def user(username): + user = db.first_or_404(sa.select(User).where(User.username == username)) + posts = [ + {'author': user, 'body': 'Test1'}, + {'author': user, 'body': 'Test2?'} + ] + return render_template('user.html', user=user, posts=posts) diff --git a/backend/app/templates/_post.html b/backend/app/templates/_post.html new file mode 100644 index 0000000..424bfc2 --- /dev/null +++ b/backend/app/templates/_post.html @@ -0,0 +1,6 @@ + + + + + +
{{ post.author.username }} says:
{{ post.body }}
diff --git a/backend/app/templates/base.html b/backend/app/templates/base.html index e83424e..1c5890a 100644 --- a/backend/app/templates/base.html +++ b/backend/app/templates/base.html @@ -15,6 +15,7 @@ {% if current_user.is_anonymous %} login {% else %} + Profile logout {% endif %} diff --git a/backend/app/templates/user.html b/backend/app/templates/user.html new file mode 100644 index 0000000..508579a --- /dev/null +++ b/backend/app/templates/user.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block content %} + + + + + +

User: {{ user.username }}

+
+ {% for post in posts %} + {% include '_post.html' %} + {% endfor %} +{% endblock %} + diff --git a/backend/app/usercontent/identicon/39466ff4e782fc398fed2c3b21d53ba2.png b/backend/app/usercontent/identicon/39466ff4e782fc398fed2c3b21d53ba2.png new file mode 100644 index 0000000000000000000000000000000000000000..0e771b051fbeddbf1dcb239e5d92f644de153806 GIT binary patch literal 384 zcmeAS@N?(olHy`uVBq!ia0vp^Js`}%1|-)VaI|A!V08C%aSW-r_4e*T&ISVk*Mlx= z6eH%hM$W&{Ql_BAQdh8y>EiKcTb6CRc8-bPH;%E%(SU>Ppn`+|50Ld_X0Tk!{aNb^ zlOy?s+1!Pi7=bb@%?UtBEG)3TpexJk7f99SulOE!KK@=mXR|UVM2`c+T%c}D7W31i zahG0Hz2{weCq3m5*a}QV2#w4zN3vjHfgO(U0!!CU{gvNq-4=WA>_6E8^&B>R2r&)E nYR?%U*EWDW3ev)I*j?PnQ7pQoqGly9WEnhN{an^LB{Ts5e}ZwQ literal 0 HcmV?d00001 diff --git a/backend/app/usercontent/identicon/686fed96bd36dc9ba0c0434036031942.png b/backend/app/usercontent/identicon/686fed96bd36dc9ba0c0434036031942.png new file mode 100644 index 0000000000000000000000000000000000000000..88c2f586d2c7f0899eff624bcf733920f3d98936 GIT binary patch literal 397 zcmeAS@N?(olHy`uVBq!ia0vp^Js`}%1|-)VaI|A!U<~neaSW-r_4e*T&Q=2fhl4I_ z6mxIPD9`?ommIc4)v5+U_0250Awl7H$2TB zx$Nxz!o<7j!ffF}O^gQ>Bv_h3a#&bEZ3+^bR_u*m@axyTfG=_9l?A1qsjt%uNp$3hyhFnqF(q=*9p9p00i_>zopr0A(YCf&c&j literal 0 HcmV?d00001 diff --git a/backend/app/usercontent/identicon/7a651a7bab949a73092e21ad0bd2f4a8.png b/backend/app/usercontent/identicon/7a651a7bab949a73092e21ad0bd2f4a8.png new file mode 100644 index 0000000000000000000000000000000000000000..20f3c735219733f4aea39ce0da4441423ab69c17 GIT binary patch literal 378 zcmeAS@N?(olHy`uVBq!ia0vp^Js`}%1|-)VaI|A!V083!aSW-r_4e*Y&ISVkhd^b` zIS&r}G@AZ-r_-(*77b1<9`~dj{C>^}_2}x@cEEYHe?x)*Pm`ko2OCQ>koEZFVxCR1 z&zGNHpK89J7bF6dQ;-15H8CE<#=3C)ak1w)-kJB?4(fA4R5$>Q!^L9Wp83w(h*-0I zyE>j3Cme>_3o})K2ODe8g!Iab>)#u%O5L7_*9Bnj0^OJPaGy#Js^5O>=6 Date: Sat, 3 Aug 2024 04:59:05 -0700 Subject: [PATCH 09/25] mgt c6 checkpoint 2.2 --- backend/README.md | 7 +++ backend/app/forms.py | 9 +++- backend/app/models.py | 4 +- backend/app/routes.py | 30 +++++++++++-- backend/app/templates/edit_profile.html | 24 ++++++++++ backend/app/templates/user.html | 9 +++- .../39466ff4e782fc398fed2c3b21d53ba2.png | Bin 384 -> 0 bytes .../686fed96bd36dc9ba0c0434036031942.png | Bin 397 -> 0 bytes .../7a651a7bab949a73092e21ad0bd2f4a8.png | Bin 378 -> 0 bytes ...fee021_posts_table.py => 1a0e4f823e90_.py} | 30 ++++++++++--- .../versions/e42cf202d424_users_table.py | 42 ------------------ 11 files changed, 100 insertions(+), 55 deletions(-) create mode 100644 backend/app/templates/edit_profile.html delete mode 100644 backend/app/usercontent/identicon/39466ff4e782fc398fed2c3b21d53ba2.png delete mode 100644 backend/app/usercontent/identicon/686fed96bd36dc9ba0c0434036031942.png delete mode 100644 backend/app/usercontent/identicon/7a651a7bab949a73092e21ad0bd2f4a8.png rename backend/migrations/versions/{938ae2fee021_posts_table.py => 1a0e4f823e90_.py} (53%) delete mode 100644 backend/migrations/versions/e42cf202d424_users_table.py diff --git a/backend/README.md b/backend/README.md index 07ea60b..f4b48ea 100644 --- a/backend/README.md +++ b/backend/README.md @@ -28,6 +28,13 @@ flask db upgrade flask db downgrade [base] flask db upgrade + +full reset? +rm app.db +rm -r migrations +flask db init +flask db migrate +flask db upgrade ``` ## build notes: diff --git a/backend/app/forms.py b/backend/app/forms.py index fe31c0e..bcd5720 100644 --- a/backend/app/forms.py +++ b/backend/app/forms.py @@ -1,6 +1,6 @@ from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, BooleanField, SubmitField -from wtforms.validators import DataRequired, ValidationError, Email, EqualTo +from wtforms import StringField, PasswordField, BooleanField, SubmitField, TextAreaField +from wtforms.validators import DataRequired, ValidationError, Email, EqualTo, Length import sqlalchemy as sa from app import db from app.models import User @@ -27,3 +27,8 @@ class RegistrationForm(FlaskForm): user = db.session.scalar(sa.select(User).where(User.email == email.data)) if user is not None: raise ValidationError('Please use a different email address.') + +class EditProfileForm(FlaskForm): + username = StringField('Username', validators=[DataRequired()]) + about_me = TextAreaField('About me', validators=[Length(min=0, max=140)]) + submit = SubmitField('Update') diff --git a/backend/app/models.py b/backend/app/models.py index 36c70a5..dc5a1a3 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -15,6 +15,8 @@ class User(UserMixin, db.Model): email: so.Mapped[str] = so.mapped_column(sa.String(120), index=True, unique=True) password_hash: so.Mapped[Optional[str]] = so.mapped_column(sa.String(256)) posts: so.WriteOnlyMapped['Post'] = so.relationship(back_populates='author') + about_me: so.Mapped[Optional[str]] = so.mapped_column(sa.String(140)) + last_seen: so.Mapped[Optional[datetime]] = so.mapped_column(default=lambda: datetime.now(timezone.utc)) def set_password(self, password): self.password_hash = generate_password_hash(password) @@ -41,7 +43,7 @@ class User(UserMixin, db.Model): pngfile.close() else: return str(base64.b64encode(pngicon))[2:-1] - def avatar(self): + def avatar_path(self): digest = hashlib.md5(self.email.lower().encode('utf-8')).hexdigest() basedir = os.path.abspath(os.path.dirname(__file__)) pngloc = os.path.join(basedir, 'usercontent', 'identicon', digest + '.png') diff --git a/backend/app/routes.py b/backend/app/routes.py index e301aaf..b11bcde 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -1,9 +1,11 @@ from flask import render_template, flash, redirect, url_for, request from urllib.parse import urlsplit +from datetime import datetime, timezone + from app import app, db -from app.forms import LoginForm, RegistrationForm -from flask_login import current_user, login_user, logout_user, login_required +from app.forms import LoginForm, RegistrationForm, EditProfileForm import sqlalchemy as sa +from flask_login import current_user, login_user, logout_user, login_required from app.models import User @app.route('/') @@ -25,6 +27,12 @@ def index(): #return posts; return render_template('index.html', title='Home', posts=posts) +@app.before_request +def before_request(): + if current_user.is_authenticated: + current_user.last_seen = datetime.now(timezone.utc) + db.session.commit() + @app.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: @@ -57,7 +65,7 @@ def register(): user.set_password(form.password.data) db.session.add(user) db.session.commit() - user.gen_avatar() + #user.gen_avatar() flash('User has been created.') return redirect(url_for('login')) return render_template('register.html', title='Register', form=form) @@ -71,3 +79,19 @@ def user(username): {'author': user, 'body': 'Test2?'} ] return render_template('user.html', user=user, posts=posts) + +@app.route('/edit_profile', methods=['GET', 'POST']) +@login_required +def edit_profile(): + form = EditProfileForm() + if form.validate_on_submit(): + current_user.username = form.username.data + current_user.about_me = form.about_me.data + db.session.commit() + flash('Profile changes have been saved.') + return redirect(url_for('edit_profile')) + elif request.method == 'GET': + form.username.data = current_user.username + form.about_me.data = current_user.about_me() + return render_template('edit_profile.html', title='Edit Profile', form=form) + diff --git a/backend/app/templates/edit_profile.html b/backend/app/templates/edit_profile.html new file mode 100644 index 0000000..e347134 --- /dev/null +++ b/backend/app/templates/edit_profile.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +

Edit Profile

+
+ {{ form.hidden_tag() }} +

+ {{ form.username.label }} + {{ form.username(size=32) }} + {% for error in form.username.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.about_me.label }} + {{ form.about_me(cols=50, rows=4) }} + {% for error in form.about_me.errors %} + [{{ error }}] + {% endfor %} +

+

{{ form.submit }}

+
+ +{% endblock %} diff --git a/backend/app/templates/user.html b/backend/app/templates/user.html index 508579a..beac920 100644 --- a/backend/app/templates/user.html +++ b/backend/app/templates/user.html @@ -4,9 +4,16 @@ - +

User: {{ user.username }}

+

User: {{ user.username }}

+ {% if user.about_me %}

{{ user.about_me }}

{% endif %} + {% if user.last_seen %}

Last activity:{{ user.last_seen }}

{% endif %} +
+ {% if user == current_user() %} +

Edit Profile

+ {% endif %}
{% for post in posts %} {% include '_post.html' %} diff --git a/backend/app/usercontent/identicon/39466ff4e782fc398fed2c3b21d53ba2.png b/backend/app/usercontent/identicon/39466ff4e782fc398fed2c3b21d53ba2.png deleted file mode 100644 index 0e771b051fbeddbf1dcb239e5d92f644de153806..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 384 zcmeAS@N?(olHy`uVBq!ia0vp^Js`}%1|-)VaI|A!V08C%aSW-r_4e*T&ISVk*Mlx= z6eH%hM$W&{Ql_BAQdh8y>EiKcTb6CRc8-bPH;%E%(SU>Ppn`+|50Ld_X0Tk!{aNb^ zlOy?s+1!Pi7=bb@%?UtBEG)3TpexJk7f99SulOE!KK@=mXR|UVM2`c+T%c}D7W31i zahG0Hz2{weCq3m5*a}QV2#w4zN3vjHfgO(U0!!CU{gvNq-4=WA>_6E8^&B>R2r&)E nYR?%U*EWDW3ev)I*j?PnQ7pQoqGly9WEnhN{an^LB{Ts5e}ZwQ diff --git a/backend/app/usercontent/identicon/686fed96bd36dc9ba0c0434036031942.png b/backend/app/usercontent/identicon/686fed96bd36dc9ba0c0434036031942.png deleted file mode 100644 index 88c2f586d2c7f0899eff624bcf733920f3d98936..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 397 zcmeAS@N?(olHy`uVBq!ia0vp^Js`}%1|-)VaI|A!U<~neaSW-r_4e*T&Q=2fhl4I_ z6mxIPD9`?ommIc4)v5+U_0250Awl7H$2TB zx$Nxz!o<7j!ffF}O^gQ>Bv_h3a#&bEZ3+^bR_u*m@axyTfG=_9l?A1qsjt%uNp$3hyhFnqF(q=*9p9p00i_>zopr0A(YCf&c&j diff --git a/backend/app/usercontent/identicon/7a651a7bab949a73092e21ad0bd2f4a8.png b/backend/app/usercontent/identicon/7a651a7bab949a73092e21ad0bd2f4a8.png deleted file mode 100644 index 20f3c735219733f4aea39ce0da4441423ab69c17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 378 zcmeAS@N?(olHy`uVBq!ia0vp^Js`}%1|-)VaI|A!V083!aSW-r_4e*Y&ISVkhd^b` zIS&r}G@AZ-r_-(*77b1<9`~dj{C>^}_2}x@cEEYHe?x)*Pm`ko2OCQ>koEZFVxCR1 z&zGNHpK89J7bF6dQ;-15H8CE<#=3C)ak1w)-kJB?4(fA4R5$>Q!^L9Wp83w(h*-0I zyE>j3Cme>_3o})K2ODe8g!Iab>)#u%O5L7_*9Bnj0^OJPaGy#Js^5O>=6 Date: Sat, 3 Aug 2024 05:34:12 -0700 Subject: [PATCH 10/25] mgt c6 done --- backend/app/routes.py | 18 +++++++++--------- backend/app/templates/user.html | 4 +++- .../{1a0e4f823e90_.py => 4a2c3a72038e_.py} | 6 +++--- 3 files changed, 15 insertions(+), 13 deletions(-) rename backend/migrations/versions/{1a0e4f823e90_.py => 4a2c3a72038e_.py} (95%) diff --git a/backend/app/routes.py b/backend/app/routes.py index b11bcde..666d7aa 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -1,13 +1,19 @@ from flask import render_template, flash, redirect, url_for, request +from flask_login import current_user, login_user, logout_user, login_required from urllib.parse import urlsplit from datetime import datetime, timezone +import sqlalchemy as sa from app import app, db from app.forms import LoginForm, RegistrationForm, EditProfileForm -import sqlalchemy as sa -from flask_login import current_user, login_user, logout_user, login_required from app.models import User +@app.before_request +def before_request(): + if current_user.is_authenticated: + current_user.last_seen = datetime.now(timezone.utc) + db.session.commit() + @app.route('/') @app.route('/index') @login_required @@ -27,12 +33,6 @@ def index(): #return posts; return render_template('index.html', title='Home', posts=posts) -@app.before_request -def before_request(): - if current_user.is_authenticated: - current_user.last_seen = datetime.now(timezone.utc) - db.session.commit() - @app.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: @@ -92,6 +92,6 @@ def edit_profile(): return redirect(url_for('edit_profile')) elif request.method == 'GET': form.username.data = current_user.username - form.about_me.data = current_user.about_me() + form.about_me.data = current_user.about_me return render_template('edit_profile.html', title='Edit Profile', form=form) diff --git a/backend/app/templates/user.html b/backend/app/templates/user.html index beac920..fbfe27e 100644 --- a/backend/app/templates/user.html +++ b/backend/app/templates/user.html @@ -6,12 +6,14 @@

User: {{ user.username }}

+

CUdebug: {{ current_user }}

+

Udebug: {{ user }}

{% if user.about_me %}

{{ user.about_me }}

{% endif %} {% if user.last_seen %}

Last activity:{{ user.last_seen }}

{% endif %} - {% if user == current_user() %} + {% if user == current_user %}

Edit Profile

{% endif %}
diff --git a/backend/migrations/versions/1a0e4f823e90_.py b/backend/migrations/versions/4a2c3a72038e_.py similarity index 95% rename from backend/migrations/versions/1a0e4f823e90_.py rename to backend/migrations/versions/4a2c3a72038e_.py index 87ac3e8..582776d 100644 --- a/backend/migrations/versions/1a0e4f823e90_.py +++ b/backend/migrations/versions/4a2c3a72038e_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 1a0e4f823e90 +Revision ID: 4a2c3a72038e Revises: -Create Date: 2024-08-03 04:56:53.822820 +Create Date: 2024-08-03 05:02:15.935738 """ from alembic import op @@ -10,7 +10,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '1a0e4f823e90' +revision = '4a2c3a72038e' down_revision = None branch_labels = None depends_on = None From af0978c4e81059796da66d094291b44fc6d7c14e Mon Sep 17 00:00:00 2001 From: finn Date: Sat, 3 Aug 2024 05:38:58 -0700 Subject: [PATCH 11/25] update readme --- backend/README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/README.md b/backend/README.md index f4b48ea..36f57c0 100644 --- a/backend/README.md +++ b/backend/README.md @@ -22,20 +22,27 @@ pip freeze > requirements.txt ``` ## db cheat: +After db schema change: ``` -flask db migrate -m "users table" +flask db migrate -m "add users table" flask db upgrade +``` -flask db downgrade [base] +Dump data if db in good state: +``` +flask db downgrade base flask db upgrade +``` -full reset? +Full reset: +``` rm app.db rm -r migrations flask db init flask db migrate flask db upgrade ``` + ## build notes: Dockerfile needs dockerignore or preferably explicitly defined copies for: From fe2dcd23f13ba273f97cc22c82a628028ad28d45 Mon Sep 17 00:00:00 2001 From: finn Date: Sat, 3 Aug 2024 06:31:19 -0700 Subject: [PATCH 12/25] mgt c7 checkpoint 1 --- backend/.flaskenv | 1 + backend/app/__init__.py | 2 +- backend/app/errors.py | 13 +++++++++++++ backend/app/templates/404.html | 8 ++++++++ backend/app/templates/500.html | 9 +++++++++ 5 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 backend/app/errors.py create mode 100644 backend/app/templates/404.html create mode 100644 backend/app/templates/500.html diff --git a/backend/.flaskenv b/backend/.flaskenv index 046b50c..0366019 100644 --- a/backend/.flaskenv +++ b/backend/.flaskenv @@ -1,2 +1,3 @@ FLASK_APP=microblog.py +FLASK_DEBUG=0 diff --git a/backend/app/__init__.py b/backend/app/__init__.py index d57d604..2ea3d48 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -11,5 +11,5 @@ migrate = Migrate(app, db) login = LoginManager(app) login.login_view = 'login' -from app import routes, models +from app import routes, models, errors diff --git a/backend/app/errors.py b/backend/app/errors.py new file mode 100644 index 0000000..4428603 --- /dev/null +++ b/backend/app/errors.py @@ -0,0 +1,13 @@ +from flask import render_template + +from app import app, db + +@app.errorhandler(404) +def not_found_error(error): + return render_template('404.html'), 404 + +@app.errorhandler(500) +def internal_error(error): + db.session.rollback() + return render_template('500.html'), 500 + diff --git a/backend/app/templates/404.html b/backend/app/templates/404.html new file mode 100644 index 0000000..3c74661 --- /dev/null +++ b/backend/app/templates/404.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block content %} + +

File Not Found

+

Back

+ +{% endblock %} diff --git a/backend/app/templates/500.html b/backend/app/templates/500.html new file mode 100644 index 0000000..699d215 --- /dev/null +++ b/backend/app/templates/500.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block content %} + +

An unexpected error has occurred.

+

Administrator has been notified.

+

Back

+ +{% endblock %} From cdeb87184a77d8fc5d222642449a680a54840088 Mon Sep 17 00:00:00 2001 From: finn Date: Sat, 3 Aug 2024 08:37:27 -0700 Subject: [PATCH 13/25] dockerized newsite mariadb setup --- backend/.dockerignore | 3 + backend/Dockerfile | 6 +- backend/README.md | 2 + backend/README.md.backup | 42 ------------ backend/config.py | 4 +- backend/requirements.txt | 1 + compose.yaml | 18 +++--- compose.yaml.local | 126 ++++++++++++++++++++++++++++++++++++ compose.yaml.prod | 126 ++++++++++++++++++++++++++++++++++++ proxy/{oldconf => baseconf} | 0 proxy/conf | 56 ++-------------- proxy/sslconf | 52 +++++++++++++++ 12 files changed, 332 insertions(+), 104 deletions(-) create mode 100644 backend/.dockerignore delete mode 100644 backend/README.md.backup create mode 100644 compose.yaml.local create mode 100644 compose.yaml.prod rename proxy/{oldconf => baseconf} (100%) create mode 100755 proxy/sslconf diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..80b0b8c --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,3 @@ +venv +migrations + diff --git a/backend/Dockerfile b/backend/Dockerfile index 36d6f5c..515a508 100755 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,5 +1,7 @@ # syntax=docker/dockerfile:1.4 -FROM python:3-alpine AS builder +FROM python:3-slim-bookworm AS builder + +RUN apt update && apt install -y libmariadb-dev gcc WORKDIR /code COPY requirements.txt /code @@ -9,7 +11,7 @@ RUN target=/root/.cache/pip \ # Need to make this explicit as part of expansion, no migrations or venv COPY . . -ENV FLASK_APP app.py +#ENV FLASK_APP app.py # This might be scary to leave on #ENV FLASK_ENV development diff --git a/backend/README.md b/backend/README.md index 36f57c0..4d452c0 100644 --- a/backend/README.md +++ b/backend/README.md @@ -8,6 +8,7 @@ ## pip: +mariadb may take extra work: gcc, libmariadb-dev ``` pip install flask pip install python-dotenv @@ -17,6 +18,7 @@ pip install flask-migrate pip install flask-login pip install email-validator pip install pydenticon +pip install mariadb ... pip freeze > requirements.txt ``` diff --git a/backend/README.md.backup b/backend/README.md.backup deleted file mode 100644 index 480b4e2..0000000 --- a/backend/README.md.backup +++ /dev/null @@ -1,42 +0,0 @@ -## Workflow: -- should work with flask run and dockerfile build -- local dev -- local pip install -- record versionless pips manually here -- pip freeze snapshots into project requirements -- docker build copies frozen requirements - - -## pip: -``` -pip install flask -pip install python-dotenv -pip install flask-wtf -pip install flask-sqlalchemy -pip install flask-migrate -pip install flask-login -pip install email-validator - -pip freeze > requirements.txt -``` - -## db cheat: -``` -flask db migrate -m "users table" -flask db upgrade - -flask db downgrade [base] -flask db upgrade -``` -## build: - -Dockerfile needs explicitly defined copies for: -- app -- config -- project dir -- requirements - -## notes: -- environment comes through project env passes through compose -- keep env untracked but templated -- no dotenv here, dotflaskenv goes into image diff --git a/backend/config.py b/backend/config.py index 8de6d47..8bcfc52 100644 --- a/backend/config.py +++ b/backend/config.py @@ -5,7 +5,7 @@ basedir = os.path.abspath(os.path.dirname(__file__)) class Config: SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') or 'flasksk' - SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') - #SQLALCHEMY_DATABASE_URI = 'mysql://flasku:' + os.environ.get('MYSQL_PASSWORD') + '@db:3306/flask' + #SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') + SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('MYSQL_PASSWORD') + '@db:3306/flask' diff --git a/backend/requirements.txt b/backend/requirements.txt index 5020423..1576f58 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -21,3 +21,4 @@ SQLAlchemy==2.0.31 typing_extensions==4.12.2 Werkzeug==3.0.3 WTForms==3.1.2 +mariadb diff --git a/compose.yaml b/compose.yaml index 27b9454..7ceb7ff 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,12 +1,14 @@ services: db: - image: mariadb:10-focal + image: mariadb:lts restart: always healthcheck: - test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 --password="${DOTENV_MYSQL_ROOT_PASSWORD}" --silent'] - interval: 3s + #test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 --password="${DOTENV_MYSQL_ROOT_PASSWORD}" --silent'] + test: ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'] + interval: 10s retries: 5 - start_period: 30s + timeout: 5s + start_period: 10s volumes: - db-data:/var/lib/mysql - ./db/init:/docker-entrypoint-initdb.d/ @@ -28,7 +30,7 @@ services: target: builder restart: always # Comment following line to use flask (1worker, dev), uncomment to use uwsgi (wsgi) - command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "app:server"] + #command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "app:server"] environment: - MYSQL_USER=flasku #- MYSQL_PASSWORD=flaskp @@ -86,9 +88,9 @@ services: proxy: build: proxy restart: always - volumes: - - /home/finn/d/cert/var/lib/letsencrypt:/var/lib/letsencrypt - - /home/finn/d/cert/etc/letsencrypt:/etc/letsencrypt + #volumes: + # - /home/finn/d/cert/var/lib/letsencrypt:/var/lib/letsencrypt + # - /home/finn/d/cert/etc/letsencrypt:/etc/letsencrypt ports: - 80:80 - 443:443 diff --git a/compose.yaml.local b/compose.yaml.local new file mode 100644 index 0000000..7ceb7ff --- /dev/null +++ b/compose.yaml.local @@ -0,0 +1,126 @@ +services: + db: + image: mariadb:lts + restart: always + healthcheck: + #test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 --password="${DOTENV_MYSQL_ROOT_PASSWORD}" --silent'] + test: ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'] + interval: 10s + retries: 5 + timeout: 5s + start_period: 10s + volumes: + - db-data:/var/lib/mysql + - ./db/init:/docker-entrypoint-initdb.d/ + networks: + - backnet + environment: + #- MYSQL_DATABASE=gitea + #- MYSQL_USER=gitea + #- MYSQL_PASSWORD=gitea + #- MYSQL_ROOT_PASSWORD=rootpass + - MYSQL_ROOT_PASSWORD=${DOTENV_MYSQL_ROOT_PASSWORD} + expose: + - 3306 + - 33060 + + backend: + build: + context: backend + target: builder + restart: always + # Comment following line to use flask (1worker, dev), uncomment to use uwsgi (wsgi) + #command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "app:server"] + environment: + - MYSQL_USER=flasku + #- MYSQL_PASSWORD=flaskp + - MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD} + - TOKEN_I=${DOTENV_TOKEN_I} + - TOKEN_C=${DOTENV_TOKEN_C} + #ports: + # - 8000:8000 + expose: + - 8000 + networks: + - backnet + - frontnet + depends_on: + db: + condition: service_healthy + + gutsub: + image: gitea/gitea:latest + container_name: gitea + restart: always + environment: + - USER_UID=1000 + - USER_GID=1000 + - GITEA__database__DB_TYPE=mysql + - GITEA__database__HOST=db:3306 + - GITEA__database__NAME=gitea + - GITEA__database__USER=gitea + - GITEA__database__PASSWD=${DOTENV_MYSQL_GITEA_PASSWORD} + - GITEA__repository__DEFAULT_BRANCH=master + - GITEA__mailer__ENABLED=true + - GITEA__mailer__FROM=${GITEA_MAIL_FROM} + - GITEA__mailer__USER= + - GITEA__mailer__PROTOCOL=smtp + - GITEA__mailer__SMTP_ADDR=pmb + - GITEA__mailer__SMTP_PORT=25 + - GITEA__service__REGISTER_EMAIL_CONFIRM=true + - GITEA__service__ENABLE_NOTIFY_MAIL=true + # To disable new users after setup: + #- GITEA__service__DISABLE_REGISTRATION=false + networks: + - backnet + - frontnet + volumes: + - ./gitea:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + #ports: + # - "3000:3000" + # - "222:22" + depends_on: + db: + condition: service_healthy + + proxy: + build: proxy + restart: always + #volumes: + # - /home/finn/d/cert/var/lib/letsencrypt:/var/lib/letsencrypt + # - /home/finn/d/cert/etc/letsencrypt:/etc/letsencrypt + ports: + - 80:80 + - 443:443 + depends_on: + - backend + networks: + - frontnet + + pmb: + #build: + # args: + # GPG_PP: $BUILD_GPG_PP + # context: pmb-pf + # dockerfile: Dockerfile + image: site_pmb:latest + expose: + - "25" + env_file: + - ./pmb-pf/.env + restart: always + volumes: + - pmb-root:/root + - /etc/localtime:/etc/localtime:ro + networks: + - backnet + +volumes: + db-data: + pmb-root: + +networks: + backnet: + frontnet: diff --git a/compose.yaml.prod b/compose.yaml.prod new file mode 100644 index 0000000..e62cf90 --- /dev/null +++ b/compose.yaml.prod @@ -0,0 +1,126 @@ +services: + db: + image: mariadb:lts + restart: always + healthcheck: + #10-focal test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 --password="${DOTENV_MYSQL_ROOT_PASSWORD}" --silent'] + test: ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'] + interval: 10s + retries: 5 + timeout: 5s + start_period: 10s + volumes: + - db-data:/var/lib/mysql + - ./db/init:/docker-entrypoint-initdb.d/ + networks: + - backnet + environment: + #- MYSQL_DATABASE=gitea + #- MYSQL_USER=gitea + #- MYSQL_PASSWORD=gitea + #- MYSQL_ROOT_PASSWORD=rootpass + - MYSQL_ROOT_PASSWORD=${DOTENV_MYSQL_ROOT_PASSWORD} + expose: + - 3306 + - 33060 + + backend: + build: + context: backend + target: builder + restart: always + # Comment following line to use flask (1worker, dev), uncomment to use uwsgi (wsgi) + command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "app:server"] + environment: + - MYSQL_USER=flasku + #- MYSQL_PASSWORD=flaskp + - MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD} + - TOKEN_I=${DOTENV_TOKEN_I} + - TOKEN_C=${DOTENV_TOKEN_C} + #ports: + # - 8000:8000 + expose: + - 8000 + networks: + - backnet + - frontnet + depends_on: + db: + condition: service_healthy + + gutsub: + image: gitea/gitea:latest + container_name: gitea + restart: always + environment: + - USER_UID=1000 + - USER_GID=1000 + - GITEA__database__DB_TYPE=mysql + - GITEA__database__HOST=db:3306 + - GITEA__database__NAME=gitea + - GITEA__database__USER=gitea + - GITEA__database__PASSWD=${DOTENV_MYSQL_GITEA_PASSWORD} + - GITEA__repository__DEFAULT_BRANCH=master + - GITEA__mailer__ENABLED=true + - GITEA__mailer__FROM=${GITEA_MAIL_FROM} + - GITEA__mailer__USER= + - GITEA__mailer__PROTOCOL=smtp + - GITEA__mailer__SMTP_ADDR=pmb + - GITEA__mailer__SMTP_PORT=25 + - GITEA__service__REGISTER_EMAIL_CONFIRM=true + - GITEA__service__ENABLE_NOTIFY_MAIL=true + # To disable new users after setup: + #- GITEA__service__DISABLE_REGISTRATION=false + networks: + - backnet + - frontnet + volumes: + - ./gitea:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + #ports: + # - "3000:3000" + # - "222:22" + depends_on: + db: + condition: service_healthy + + proxy: + build: proxy + restart: always + volumes: + - /home/finn/d/cert/var/lib/letsencrypt:/var/lib/letsencrypt + - /home/finn/d/cert/etc/letsencrypt:/etc/letsencrypt + ports: + - 80:80 + - 443:443 + depends_on: + - backend + networks: + - frontnet + + pmb: + #build: + # args: + # GPG_PP: $BUILD_GPG_PP + # context: pmb-pf + # dockerfile: Dockerfile + image: site_pmb:latest + expose: + - "25" + env_file: + - ./pmb-pf/.env + restart: always + volumes: + - pmb-root:/root + - /etc/localtime:/etc/localtime:ro + networks: + - backnet + +volumes: + db-data: + pmb-root: + +networks: + backnet: + frontnet: diff --git a/proxy/oldconf b/proxy/baseconf similarity index 100% rename from proxy/oldconf rename to proxy/baseconf diff --git a/proxy/conf b/proxy/conf index 80f6015..f6d2195 100755 --- a/proxy/conf +++ b/proxy/conf @@ -1,52 +1,8 @@ -#server { -# listen 80; -# server_name localhost; -# location / { -# proxy_pass http://backend:8000; -# } - - -# always redirect to https server { - listen 80 default_server; - server_name _; - return 301 https://$host$request_uri; + listen 80; + server_name localhost; + location / { + proxy_pass http://backend:8000; + } + } - -server { - listen 443 ssl http2; - # use the certificates - ssl_certificate /etc/letsencrypt/live/oily.dad/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/oily.dad/privkey.pem; - server_name oily.dad www.oily.dad; - root /var/www/html; - index index.php index.html index.htm; - - - location / { - proxy_pass http://backend:8000/; - } -} - -server { - listen 443 ssl http2; - # use the certificates - ssl_certificate /etc/letsencrypt/live/oily.dad/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/oily.dad/privkey.pem; - server_name gut.oily.dad; - root /var/www/html; - index index.php index.html index.htm; - - location / { - client_max_body_size 512M; - #proxy_pass http://localhost:3000; - proxy_set_header Connection $http_connection; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_pass http://gitea:3000/; - } -} - diff --git a/proxy/sslconf b/proxy/sslconf new file mode 100755 index 0000000..80f6015 --- /dev/null +++ b/proxy/sslconf @@ -0,0 +1,52 @@ +#server { +# listen 80; +# server_name localhost; +# location / { +# proxy_pass http://backend:8000; +# } + + +# always redirect to https +server { + listen 80 default_server; + server_name _; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + # use the certificates + ssl_certificate /etc/letsencrypt/live/oily.dad/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/oily.dad/privkey.pem; + server_name oily.dad www.oily.dad; + root /var/www/html; + index index.php index.html index.htm; + + + location / { + proxy_pass http://backend:8000/; + } +} + +server { + listen 443 ssl http2; + # use the certificates + ssl_certificate /etc/letsencrypt/live/oily.dad/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/oily.dad/privkey.pem; + server_name gut.oily.dad; + root /var/www/html; + index index.php index.html index.htm; + + location / { + client_max_body_size 512M; + #proxy_pass http://localhost:3000; + proxy_set_header Connection $http_connection; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://gitea:3000/; + } +} + From 5b0cd1d22f805ec4db81a9080fc8f4e7c851330b Mon Sep 17 00:00:00 2001 From: finn Date: Sat, 3 Aug 2024 08:58:32 -0700 Subject: [PATCH 14/25] checkpoint before maillog attempt --- backend/app/__init__.py | 2 ++ backend/config.py | 7 +++++++ compose.yaml | 1 + proxy/baseconf | 4 ++++ proxy/conf | 4 ++++ 5 files changed, 18 insertions(+) diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 2ea3d48..52c9951 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -3,6 +3,8 @@ from config import Config from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_login import LoginManager +import logging +from logging.handlers import SMTPHandler app = Flask(__name__) app.config.from_object(Config) diff --git a/backend/config.py b/backend/config.py index 8bcfc52..b2449f9 100644 --- a/backend/config.py +++ b/backend/config.py @@ -8,4 +8,11 @@ class Config: #SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('MYSQL_PASSWORD') + '@db:3306/flask' + MAIL_SERVER = 'pmb' + MAIL_PORT = 25 + MAIL_USE_TLS = False + MAIL_USERNAME = '' + MAIL_PASSWORD = '' + ADMINS = [os.environ.get('ADMIN_EMAIL')] + diff --git a/compose.yaml b/compose.yaml index 7ceb7ff..f7b45ba 100644 --- a/compose.yaml +++ b/compose.yaml @@ -37,6 +37,7 @@ services: - MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD} - TOKEN_I=${DOTENV_TOKEN_I} - TOKEN_C=${DOTENV_TOKEN_C} + - ADMIN_EMAIL=${ADMIN_EMAIL} #ports: # - 8000:8000 expose: diff --git a/proxy/baseconf b/proxy/baseconf index f6d2195..04abcd7 100755 --- a/proxy/baseconf +++ b/proxy/baseconf @@ -4,5 +4,9 @@ server { location / { proxy_pass http://backend:8000; } + location /gutty{ + proxy_pass http://gitea:3000; + } + } diff --git a/proxy/conf b/proxy/conf index f6d2195..fe29397 100755 --- a/proxy/conf +++ b/proxy/conf @@ -4,5 +4,9 @@ server { location / { proxy_pass http://backend:8000; } + location /gutty { + proxy_pass http://gitea:3000; + } + } From d7a0167cd6148cfa4a46b4a562fd00112f74fbe2 Mon Sep 17 00:00:00 2001 From: finn Date: Sat, 3 Aug 2024 11:45:37 -0700 Subject: [PATCH 15/25] mgt c7 finish --- backend/.flaskenv | 2 +- backend/Dockerfile | 2 +- backend/README.md | 3 +++ backend/app/__init__.py | 22 +++++++++++++++++++++- backend/app/forms.py | 14 ++++++++++++++ backend/app/routes.py | 4 +++- backend/config.py | 10 +++++++--- backend/requirements.txt | 1 + compose.yaml | 1 + compose.yaml.local | 2 ++ compose.yaml.prod | 4 +++- 11 files changed, 57 insertions(+), 8 deletions(-) diff --git a/backend/.flaskenv b/backend/.flaskenv index 0366019..b85b65f 100644 --- a/backend/.flaskenv +++ b/backend/.flaskenv @@ -1,3 +1,3 @@ FLASK_APP=microblog.py -FLASK_DEBUG=0 +FLASK_DEBUG=1 diff --git a/backend/Dockerfile b/backend/Dockerfile index 515a508..fdeff17 100755 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -11,7 +11,7 @@ RUN target=/root/.cache/pip \ # Need to make this explicit as part of expansion, no migrations or venv COPY . . -#ENV FLASK_APP app.py +ENV FLASK_APP microblog.py # This might be scary to leave on #ENV FLASK_ENV development diff --git a/backend/README.md b/backend/README.md index 4d452c0..f2511f1 100644 --- a/backend/README.md +++ b/backend/README.md @@ -54,6 +54,9 @@ Dockerfile needs dockerignore or preferably explicitly defined copies for: - requirements - not dotflaskenv, vars set with dockerfile +## docker setup: +- py logger handler StreamHandler is buggy to the point of being useless. Use logfile and link to stdout in Docker build + ## notes: - compose has entry that overrides flask with uwsgi for prod - miminal environment vars come through project env, pass through compose diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 52c9951..3cac71a 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -3,7 +3,7 @@ from config import Config from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_login import LoginManager -import logging +import logging, sys from logging.handlers import SMTPHandler app = Flask(__name__) @@ -13,5 +13,25 @@ migrate = Migrate(app, db) login = LoginManager(app) login.login_view = 'login' +if not app.debug: + if app.config['MAIL_SERVER']: + auth = None + secure = None + mail_handler = SMTPHandler( + mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']), + fromaddr=app.config['FROM_ADDRESS'], + toaddrs=app.config['ADMINS'], subject='MB failure.', + credentials=auth, secure=secure) + mail_handler.setLevel(logging.ERROR) + app.logger.addHandler(mail_handler) + + if app.config['DC_LOGGING']: + print('#################### DEBUGHERE', file=sys.stderr) + dclog = logging.StreamHandler(stream=sys.stderr) + dclog.setLevel(logging.INFO) + dclog.propagate = False + app.logger.addHandler(dclog) + + from app import routes, models, errors diff --git a/backend/app/forms.py b/backend/app/forms.py index bcd5720..21dadf7 100644 --- a/backend/app/forms.py +++ b/backend/app/forms.py @@ -32,3 +32,17 @@ class EditProfileForm(FlaskForm): username = StringField('Username', validators=[DataRequired()]) about_me = TextAreaField('About me', validators=[Length(min=0, max=140)]) submit = SubmitField('Update') + + def __init__(self, original_username, *args, **kwargs): + super().__init__(*args, **kwargs) + self.original_username = original_username + + def validate_username(self, username): + if username.data != self.original_username: + user = db.session.scalar(sa.select(User).where(User.username == username.data)) + if user is not None: + raise ValidationError('Please use a different username.') + + + + diff --git a/backend/app/routes.py b/backend/app/routes.py index 666d7aa..e3723b1 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -18,6 +18,7 @@ def before_request(): @app.route('/index') @login_required def index(): + app.logger.info("AAAAAAAAAAAAAAAAAAAAA") user = {'username': 'Finnaa'} posts = [ @@ -74,6 +75,7 @@ def register(): @login_required def user(username): user = db.first_or_404(sa.select(User).where(User.username == username)) + #app.logger.info('PROFILE DEBUG ###############################') posts = [ {'author': user, 'body': 'Test1'}, {'author': user, 'body': 'Test2?'} @@ -83,7 +85,7 @@ def user(username): @app.route('/edit_profile', methods=['GET', 'POST']) @login_required def edit_profile(): - form = EditProfileForm() + form = EditProfileForm(current_user.username) if form.validate_on_submit(): current_user.username = form.username.data current_user.about_me = form.about_me.data diff --git a/backend/config.py b/backend/config.py index b2449f9..e1efeb1 100644 --- a/backend/config.py +++ b/backend/config.py @@ -5,14 +5,18 @@ basedir = os.path.abspath(os.path.dirname(__file__)) class Config: SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') or 'flasksk' - #SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') - SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('MYSQL_PASSWORD') + '@db:3306/flask' + SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') + #SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('MYSQL_PASSWORD') + '@db:3306/flask' - MAIL_SERVER = 'pmb' + #MAIL_SERVER = 'pmb' + MAIL_SERVER = '' MAIL_PORT = 25 MAIL_USE_TLS = False MAIL_USERNAME = '' MAIL_PASSWORD = '' ADMINS = [os.environ.get('ADMIN_EMAIL')] + FROM_ADDRESS = os.environ.get('FROM_ADDRESS') + DC_LOGGING = True + diff --git a/backend/requirements.txt b/backend/requirements.txt index 1576f58..5df5030 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -21,4 +21,5 @@ SQLAlchemy==2.0.31 typing_extensions==4.12.2 Werkzeug==3.0.3 WTForms==3.1.2 +uwsgi mariadb diff --git a/compose.yaml b/compose.yaml index f7b45ba..e0f2c61 100644 --- a/compose.yaml +++ b/compose.yaml @@ -38,6 +38,7 @@ services: - TOKEN_I=${DOTENV_TOKEN_I} - TOKEN_C=${DOTENV_TOKEN_C} - ADMIN_EMAIL=${ADMIN_EMAIL} + - FROM_ADDRESS=${GITEA_MAIL_FROM} #ports: # - 8000:8000 expose: diff --git a/compose.yaml.local b/compose.yaml.local index 7ceb7ff..e0f2c61 100644 --- a/compose.yaml.local +++ b/compose.yaml.local @@ -37,6 +37,8 @@ services: - MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD} - TOKEN_I=${DOTENV_TOKEN_I} - TOKEN_C=${DOTENV_TOKEN_C} + - ADMIN_EMAIL=${ADMIN_EMAIL} + - FROM_ADDRESS=${GITEA_MAIL_FROM} #ports: # - 8000:8000 expose: diff --git a/compose.yaml.prod b/compose.yaml.prod index e62cf90..f1ab81f 100644 --- a/compose.yaml.prod +++ b/compose.yaml.prod @@ -3,7 +3,7 @@ services: image: mariadb:lts restart: always healthcheck: - #10-focal test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 --password="${DOTENV_MYSQL_ROOT_PASSWORD}" --silent'] + #test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 --password="${DOTENV_MYSQL_ROOT_PASSWORD}" --silent'] test: ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'] interval: 10s retries: 5 @@ -37,6 +37,8 @@ services: - MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD} - TOKEN_I=${DOTENV_TOKEN_I} - TOKEN_C=${DOTENV_TOKEN_C} + - ADMIN_EMAIL=${ADMIN_EMAIL} + - FROM_ADDRESS=${GITEA_MAIL_FROM} #ports: # - 8000:8000 expose: From eb0f19b109b4ff0ef89c2506b9259296a06f9b27 Mon Sep 17 00:00:00 2001 From: finn Date: Sun, 4 Aug 2024 04:39:49 -0700 Subject: [PATCH 16/25] pre-c8 checkpoint --- README.md | 29 ++++++++++++++++++----------- backend/README.md | 7 ++++++- backend/app/__init__.py | 3 ++- backend/config.py | 4 ++-- compose.yaml | 2 +- dotenv | 6 +++++- 6 files changed, 34 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index cf790f4..5a0baa3 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,10 @@ ### Sec: -* This repo is public. Mind cred slip-ups. -* Please note changes to /etc/sshd/sshd_conf made by lll script. If different method is used, audit manually. -* Note app Dockerfile debug console, found at /console. Werkzeug/flask is WILDLY insecure if left in dev/dbg. -* Avoid docker socks stuff. - - +- This repo is public. Mind cred slip-ups. +- Please note changes to /etc/sshd/sshd_conf made by lll script. If different method is used, audit manually. +- Note app Dockerfile debug console, found at /console. Werkzeug/flask is WILDLY insecure if left in dev/dbg. +- Avoid docker socks stuff. ### Install: @@ -49,19 +47,28 @@ set up cron job for script pmb-pf - git clone of my mail thing other - ref and non-sensitive files for dns -### Timeline: +### Setup cheat: -set up certbot dns\ -see tar of cert dir with script +- set up certbot dns (prod) +- see tar of cert dir with script (prod) +- flask vs uwsgi in backend compose section (prod) +- build vs local image in pmb-pf compose section +- git clone pmb-pf +- copy example .env in root dir +- copy example .env in pmb-pf +- copy example conf in proxy +- do pmb-pf setup, and adjust root .env +- mind backend config db settings ### Notes: This repo is minimally-sensitive. Falling outside the repo dir structure are reference awesome-compose files used as baseline -- nginx-flask-mysql -- and certs, containing letsencrypt script. Script may be backed up into repo carefully, sanitizing any tkens. -TODO: gitea subdomain will require wildcard cert -- therefore "*.oily.dad" AND "oily.dad" DONE - ### Changing gitea subdomain: Find in proxy/conf.\ Find in gitea conf.\ Rebuild images. +### Todo: +- gitea subdomain will require wildcard cert -- therefore "*.oily.dad" AND "oily.dad" DONE +- move more stuff from backend config into root .env \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index f2511f1..a7b6c04 100644 --- a/backend/README.md +++ b/backend/README.md @@ -18,7 +18,9 @@ pip install flask-migrate pip install flask-login pip install email-validator pip install pydenticon +Prod only, require sys packages: pip install mariadb +pip install uwsgi ... pip freeze > requirements.txt ``` @@ -36,8 +38,11 @@ flask db downgrade base flask db upgrade ``` -Full reset: +Full reset or maria init: ``` +sql: +drop table users; +drop table posts; rm app.db rm -r migrations flask db init diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 3cac71a..8d36e49 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -26,11 +26,12 @@ if not app.debug: app.logger.addHandler(mail_handler) if app.config['DC_LOGGING']: - print('#################### DEBUGHERE', file=sys.stderr) + print('#################### TEST PRINT STDERR DEBUG', file=sys.stderr) dclog = logging.StreamHandler(stream=sys.stderr) dclog.setLevel(logging.INFO) dclog.propagate = False app.logger.addHandler(dclog) + app.logger.info('@@@@@@@@@@@@@@@@@@@@@ TEST LOGGER INFO MESSAGE') from app import routes, models, errors diff --git a/backend/config.py b/backend/config.py index e1efeb1..d05ad60 100644 --- a/backend/config.py +++ b/backend/config.py @@ -5,8 +5,8 @@ basedir = os.path.abspath(os.path.dirname(__file__)) class Config: SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') or 'flasksk' - SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') - #SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('MYSQL_PASSWORD') + '@db:3306/flask' + #SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') + SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('MYSQL_PASSWORD') + '@db:3306/flask' #MAIL_SERVER = 'pmb' MAIL_SERVER = '' diff --git a/compose.yaml b/compose.yaml index e0f2c61..5e45c24 100644 --- a/compose.yaml +++ b/compose.yaml @@ -30,7 +30,7 @@ services: target: builder restart: always # Comment following line to use flask (1worker, dev), uncomment to use uwsgi (wsgi) - #command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "app:server"] + #command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "microblog:app"] environment: - MYSQL_USER=flasku #- MYSQL_PASSWORD=flaskp diff --git a/dotenv b/dotenv index d5122f5..0f2a538 100644 --- a/dotenv +++ b/dotenv @@ -5,7 +5,7 @@ DOTENV_MYSQL_ROOT_PASSWORD=rootp DOTENV_MYSQL_GITEA_PASSWORD=giteap DOTENV_MYSQL_FLASK_PASSWORD=flaskp -GITEA_MAIL_FROM= +GITEA_MAIL_FROM=gitea@gitea.changeme # Build ARG GPG_PP. May still need to be empty to avoid breakage. BUILD_GPG_PP= @@ -18,3 +18,7 @@ DOTENV_TOKEN_I=dti # Consequential token: protect DOTENV_TOKEN_C=dtc + +# Destination address for handler mailer +ADMIN_EMAIL="email@email.changeme" + From 469785ee33a7714cc4f9d03fba57fd1b2ffd5c63 Mon Sep 17 00:00:00 2001 From: finn Date: Sun, 4 Aug 2024 05:44:20 -0700 Subject: [PATCH 17/25] mgt c8 checkpoint at tests --- backend/.dockerignore | 1 + backend/Dockerfile | 6 ++++- backend/README.md | 4 ++++ backend/app/models.py | 47 ++++++++++++++++++++++++++++++++++++++++ backend/dbdb.sh | 4 ++++ backend/requirements.txt | 8 +++++-- backend/tests.py | 5 +++++ compose.yaml | 5 ++++- 8 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 backend/dbdb.sh create mode 100644 backend/tests.py diff --git a/backend/.dockerignore b/backend/.dockerignore index 80b0b8c..a375c52 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -1,3 +1,4 @@ venv migrations +zapp.db diff --git a/backend/Dockerfile b/backend/Dockerfile index fdeff17..2145c54 100755 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,7 +1,11 @@ # syntax=docker/dockerfile:1.4 FROM python:3-slim-bookworm AS builder -RUN apt update && apt install -y libmariadb-dev gcc +# Second line optional/debug/qol +RUN apt update && apt install -y \ + libmariadb-dev gcc \ + mariadb-client + WORKDIR /code COPY requirements.txt /code diff --git a/backend/README.md b/backend/README.md index a7b6c04..e2b34d0 100644 --- a/backend/README.md +++ b/backend/README.md @@ -22,6 +22,10 @@ Prod only, require sys packages: pip install mariadb pip install uwsgi ... +``` + +Freeze/requirements.txt. Better to audit this inside python:3-bookworm-slim container. +``` pip freeze > requirements.txt ``` diff --git a/backend/app/models.py b/backend/app/models.py index dc5a1a3..9cace57 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -9,6 +9,13 @@ import pydenticon, hashlib, base64 from app import db, login from flask_login import UserMixin +followers = sa.Table( + 'followers', + db.metadata, + sa.Column('follower_id', sa.Integer, sa.ForeignKey('user.id'), primary_key=True), + sa.Column('followed_id', sa.Integer, sa.ForeignKey('user.id'), primary_key=True) +) + class User(UserMixin, db.Model): id: so.Mapped[int] = so.mapped_column(primary_key=True) username: so.Mapped[str] = so.mapped_column(sa.String(64), index=True, unique=True) @@ -17,6 +24,16 @@ class User(UserMixin, db.Model): posts: so.WriteOnlyMapped['Post'] = so.relationship(back_populates='author') about_me: so.Mapped[Optional[str]] = so.mapped_column(sa.String(140)) last_seen: so.Mapped[Optional[datetime]] = so.mapped_column(default=lambda: datetime.now(timezone.utc)) + following: so.WriteOnlyMapped['User'] = so.relationship( + secondary=followers, + primaryjoin=(followers.c.follower_id == id), + secondaryjoin=(followers.c.followed_id == id), + back_populates='followers') + followers: so.WriteOnlyMapped['User'] = so.relationship( + secondary=followers, + primaryjoin=(followers.c.followed_id == id), + secondaryjoin=(followers.c.follower_id == id), + back_populates='following') def set_password(self, password): self.password_hash = generate_password_hash(password) @@ -49,6 +66,36 @@ class User(UserMixin, db.Model): pngloc = os.path.join(basedir, 'usercontent', 'identicon', digest + '.png') return pngloc + def follow(self, user): + if not self.is_following(user): + self.following.add(user) + def unfollow(self, user): + if self.is_following(user): + self.following.remove(user) + def is_following(self, user): + query = self.following.select().where(User.id == user.id) + return db.session.scalar(query) is not None + def followers_count(self): + query = sa.select(sa.func.count()).select_from(self.followers.select().subquery()) + return db.session.scalar(query) + def following_count(self): + query = sa.select(sa.func.count()).select_from(self.following.select().subquery()) + return db.session.scalar(query) + def following_posts(self): + Author = so.aliased(User) + Follower = so.aliased(User) + return ( + sa.select(Post) + .join(Post.author.of_type(Author)) + .join(Author.followers.of_type(Follower), isouter=True) + .where(sa.or_( + Follower.id == self.id + Author.id == self.id + )) + .group_by(Post) + .order_by(Post.timestamp.desc()) + ) + def __repr__(self): return ''.format(self.username) diff --git a/backend/dbdb.sh b/backend/dbdb.sh new file mode 100644 index 0000000..4c3930b --- /dev/null +++ b/backend/dbdb.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# Okay to publish -- creds are local dev only + +mariadb -hdb -uflasku -pflaskp flask diff --git a/backend/requirements.txt b/backend/requirements.txt index 5df5030..2cdaf64 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -21,5 +21,9 @@ SQLAlchemy==2.0.31 typing_extensions==4.12.2 Werkzeug==3.0.3 WTForms==3.1.2 -uwsgi -mariadb +mariadb==1.1.10 +packaging==24.1 +setuptools==72.1.0 +uWSGI==2.0.26 +wheel==0.43.0 + diff --git a/backend/tests.py b/backend/tests.py new file mode 100644 index 0000000..1b31bb1 --- /dev/null +++ b/backend/tests.py @@ -0,0 +1,5 @@ +import os +os.environ['DATABASE_URL'] = 'sqlite://' + +from datetime import datetime, timezone, timedelta + diff --git a/compose.yaml b/compose.yaml index 5e45c24..a4ffbc3 100644 --- a/compose.yaml +++ b/compose.yaml @@ -28,7 +28,10 @@ services: build: context: backend target: builder - restart: always + # Next two are only debug, used without restart + stdin_open: true + tty: true + #restart: always # Comment following line to use flask (1worker, dev), uncomment to use uwsgi (wsgi) #command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "microblog:app"] environment: From 63bdf8d1645334a8ada6039f6303f9e1449fbcb0 Mon Sep 17 00:00:00 2001 From: finn Date: Sun, 4 Aug 2024 05:48:02 -0700 Subject: [PATCH 18/25] mgt c8 checkpoint after tests --- backend/app/models.py | 2 +- backend/config.py | 4 +- backend/tests.py | 90 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/backend/app/models.py b/backend/app/models.py index 9cace57..142d734 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -89,7 +89,7 @@ class User(UserMixin, db.Model): .join(Post.author.of_type(Author)) .join(Author.followers.of_type(Follower), isouter=True) .where(sa.or_( - Follower.id == self.id + Follower.id == self.id, Author.id == self.id )) .group_by(Post) diff --git a/backend/config.py b/backend/config.py index d05ad60..e1efeb1 100644 --- a/backend/config.py +++ b/backend/config.py @@ -5,8 +5,8 @@ basedir = os.path.abspath(os.path.dirname(__file__)) class Config: SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') or 'flasksk' - #SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') - SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('MYSQL_PASSWORD') + '@db:3306/flask' + SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') + #SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('MYSQL_PASSWORD') + '@db:3306/flask' #MAIL_SERVER = 'pmb' MAIL_SERVER = '' diff --git a/backend/tests.py b/backend/tests.py index 1b31bb1..305ed03 100644 --- a/backend/tests.py +++ b/backend/tests.py @@ -2,4 +2,94 @@ import os os.environ['DATABASE_URL'] = 'sqlite://' from datetime import datetime, timezone, timedelta +import unittest +from app import app, db +from app.models import User, Post + + +class UserModelCase(unittest.TestCase): + def setUp(self): + self.app_context = app.app_context() + self.app_context.push() + db.create_all() + + def tearDown(self): + db.session.remove() + db.drop_all() + self.app_context.pop() + + def test_password_hashing(self): + u = User(username='susan', email='susan@example.com') + u.set_password('cat') + self.assertFalse(u.check_password('dog')) + self.assertTrue(u.check_password('cat')) + + def test_follow(self): + u1 = User(username='john', email='john@example.com') + u2 = User(username='susan', email='susan@example.com') + db.session.add(u1) + db.session.add(u2) + db.session.commit() + following = db.session.scalars(u1.following.select()).all() + followers = db.session.scalars(u2.followers.select()).all() + self.assertEqual(following, []) + self.assertEqual(followers, []) + + u1.follow(u2) + db.session.commit() + self.assertTrue(u1.is_following(u2)) + self.assertEqual(u1.following_count(), 1) + self.assertEqual(u2.followers_count(), 1) + u1_following = db.session.scalars(u1.following.select()).all() + u2_followers = db.session.scalars(u2.followers.select()).all() + self.assertEqual(u1_following[0].username, 'susan') + self.assertEqual(u2_followers[0].username, 'john') + + u1.unfollow(u2) + db.session.commit() + self.assertFalse(u1.is_following(u2)) + self.assertEqual(u1.following_count(), 0) + self.assertEqual(u2.followers_count(), 0) + + def test_follow_posts(self): + # create four users + u1 = User(username='john', email='john@example.com') + u2 = User(username='susan', email='susan@example.com') + u3 = User(username='mary', email='mary@example.com') + u4 = User(username='david', email='david@example.com') + db.session.add_all([u1, u2, u3, u4]) + + # create four posts + now = datetime.now(timezone.utc) + p1 = Post(body="post from john", author=u1, + timestamp=now + timedelta(seconds=1)) + p2 = Post(body="post from susan", author=u2, + timestamp=now + timedelta(seconds=4)) + p3 = Post(body="post from mary", author=u3, + timestamp=now + timedelta(seconds=3)) + p4 = Post(body="post from david", author=u4, + timestamp=now + timedelta(seconds=2)) + db.session.add_all([p1, p2, p3, p4]) + db.session.commit() + + # setup the followers + u1.follow(u2) # john follows susan + u1.follow(u4) # john follows david + u2.follow(u3) # susan follows mary + u3.follow(u4) # mary follows david + db.session.commit() + + # check the following posts of each user + f1 = db.session.scalars(u1.following_posts()).all() + f2 = db.session.scalars(u2.following_posts()).all() + f3 = db.session.scalars(u3.following_posts()).all() + f4 = db.session.scalars(u4.following_posts()).all() + self.assertEqual(f1, [p2, p4, p1]) + self.assertEqual(f2, [p2, p3]) + self.assertEqual(f3, [p3, p4]) + self.assertEqual(f4, [p4]) + + +if __name__ == '__main__': + unittest.main(verbosity=2) From 2f920f3b6f39c47f6eed03486545250cc3314613 Mon Sep 17 00:00:00 2001 From: finn Date: Sun, 4 Aug 2024 09:40:09 -0700 Subject: [PATCH 19/25] mgt c8 finish --- backend/Dockerfile | 0 backend/app/forms.py | 3 ++- backend/app/routes.py | 47 ++++++++++++++++++++++++++++++--- backend/app/templates/user.html | 22 ++++++++++++--- backend/config.py | 4 +-- backend/dbdb.sh | 0 6 files changed, 66 insertions(+), 10 deletions(-) mode change 100755 => 100644 backend/Dockerfile mode change 100644 => 100755 backend/dbdb.sh diff --git a/backend/Dockerfile b/backend/Dockerfile old mode 100755 new mode 100644 diff --git a/backend/app/forms.py b/backend/app/forms.py index 21dadf7..4825c86 100644 --- a/backend/app/forms.py +++ b/backend/app/forms.py @@ -43,6 +43,7 @@ class EditProfileForm(FlaskForm): if user is not None: raise ValidationError('Please use a different username.') - +class EmptyForm(FlaskForm): + submit = SubmitField('Submit') diff --git a/backend/app/routes.py b/backend/app/routes.py index e3723b1..921db5a 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -5,7 +5,7 @@ from datetime import datetime, timezone import sqlalchemy as sa from app import app, db -from app.forms import LoginForm, RegistrationForm, EditProfileForm +from app.forms import LoginForm, RegistrationForm, EditProfileForm, EmptyForm from app.models import User @app.before_request @@ -18,7 +18,7 @@ def before_request(): @app.route('/index') @login_required def index(): - app.logger.info("AAAAAAAAAAAAAAAAAAAAA") + app.logger.info('@@@@@@@@@@@@@@@@@ INFO LOG TEST INDEX') user = {'username': 'Finnaa'} posts = [ @@ -75,12 +75,12 @@ def register(): @login_required def user(username): user = db.first_or_404(sa.select(User).where(User.username == username)) - #app.logger.info('PROFILE DEBUG ###############################') posts = [ {'author': user, 'body': 'Test1'}, {'author': user, 'body': 'Test2?'} ] - return render_template('user.html', user=user, posts=posts) + form = EmptyForm() + return render_template('user.html', user=user, posts=posts, form=form) @app.route('/edit_profile', methods=['GET', 'POST']) @login_required @@ -97,3 +97,42 @@ def edit_profile(): form.about_me.data = current_user.about_me return render_template('edit_profile.html', title='Edit Profile', form=form) +@app.route('/follow/', methods=['POST']) +@login_required +def follow(username): + form = EmptyForm() + if form.validate_on_submit(): + user = db.session.scalar(sa.select(User).where(User.username == username)) + if user is None: + flash(f'User {username} not found.') + return redirect(url_for('index')) + if user == current_user: + flash('You cannot follow yourself.') + return redirect(url_for('user', username=username)) + current_user.follow(user) + db.session.commit() + flash(f'You are now following {username}.') + return redirect(url_for('user', username=username)) + else: + return redirect(url_for('index')) + +@app.route('/unfollow/', methods=['POST']) +@login_required +def unfollow(username): + form = EmptyForm() + if form.validate_on_submit(): + user = db.session.scalar(sa.select(User).where(User.username == username)) + if user is None: + flash(f'User {username} not found.') + return redirect(url_for('index')) + if user == current_user: + flash('You cannot unfollow yourself.') + return redirect(url_for('user', username=username)) + current_user.unfollow(user) + db.session.commit() + flash(f'You have unfollowed {username}.') + return redirect(url_for('user', username=username)) + else: + return redirect(url_for('index')) + + diff --git a/backend/app/templates/user.html b/backend/app/templates/user.html index fbfe27e..8c3d26f 100644 --- a/backend/app/templates/user.html +++ b/backend/app/templates/user.html @@ -10,12 +10,28 @@

Udebug: {{ user }}

{% if user.about_me %}

{{ user.about_me }}

{% endif %} {% if user.last_seen %}

Last activity:{{ user.last_seen }}

{% endif %} + {% if user == current_user %} +

Edit Profile

+ {% elif not current_user.is_following(user) %} +

+

+ {{ form.hidden_tag() }} + {{ form.submit(value='Follow') }} +
+

+ {% else %} +

+

+ {{ form.hidden_tag() }} + {{ form.submit(value='Unfollow') }} +
+

+ {% endif %} + + - {% if user == current_user %} -

Edit Profile

- {% endif %}
{% for post in posts %} {% include '_post.html' %} diff --git a/backend/config.py b/backend/config.py index e1efeb1..d05ad60 100644 --- a/backend/config.py +++ b/backend/config.py @@ -5,8 +5,8 @@ basedir = os.path.abspath(os.path.dirname(__file__)) class Config: SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') or 'flasksk' - SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') - #SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('MYSQL_PASSWORD') + '@db:3306/flask' + #SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') + SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('MYSQL_PASSWORD') + '@db:3306/flask' #MAIL_SERVER = 'pmb' MAIL_SERVER = '' diff --git a/backend/dbdb.sh b/backend/dbdb.sh old mode 100644 new mode 100755 From 94434d4f8ee3e8097009b77f9eaa742c6b902cce Mon Sep 17 00:00:00 2001 From: finn Date: Sun, 4 Aug 2024 10:55:28 -0700 Subject: [PATCH 20/25] avatar tweaks --- backend/app/models.py | 4 ++-- backend/app/templates/_post.html | 2 +- backend/app/templates/user.html | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/app/models.py b/backend/app/models.py index 142d734..7131df2 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -52,8 +52,8 @@ class User(UserMixin, db.Model): digest = hashlib.md5(self.email.lower().encode('utf-8')).hexdigest() basedir = os.path.abspath(os.path.dirname(__file__)) pngloc = os.path.join(basedir, 'usercontent', 'identicon', str(digest) + '.png') - icongen = pydenticon.Generator(5, 5, digest=hashlib.md5, foreground=foreground, background=background) - pngicon = icongen.generate(self.email, 120, 120, padding=(10, 10, 10, 10), inverted=False, output_format="png") + icongen = pydenticon.Generator(8, 5, digest=hashlib.md5, foreground=foreground, background=background) + pngicon = icongen.generate(self.email, 150, 240, padding=(10, 10, 10, 10), inverted=False, output_format="png") if write_png: pngfile = open(pngloc, "wb") pngfile.write(pngicon) diff --git a/backend/app/templates/_post.html b/backend/app/templates/_post.html index 424bfc2..58f0f6c 100644 --- a/backend/app/templates/_post.html +++ b/backend/app/templates/_post.html @@ -1,6 +1,6 @@ - +
{{ post.author.username }} says:
{{ post.body }}
diff --git a/backend/app/templates/user.html b/backend/app/templates/user.html index 8c3d26f..d4c6918 100644 --- a/backend/app/templates/user.html +++ b/backend/app/templates/user.html @@ -3,7 +3,9 @@ {% block content %} - +
+ +

User: {{ user.username }}

CUdebug: {{ current_user }}

From 61c8cadb8710edb76ae991da74d2a28e98948b2b Mon Sep 17 00:00:00 2001 From: finn Date: Sun, 4 Aug 2024 13:14:58 -0700 Subject: [PATCH 21/25] mgt c9 complete --- backend/app/forms.py | 4 +++ backend/app/routes.py | 58 ++++++++++++++++++++------------ backend/app/templates/_post.html | 13 +++++-- backend/app/templates/base.html | 7 ++-- backend/app/templates/index.html | 18 +++++++++- backend/app/templates/user.html | 3 ++ backend/config.py | 1 + 7 files changed, 75 insertions(+), 29 deletions(-) diff --git a/backend/app/forms.py b/backend/app/forms.py index 4825c86..5d50a5c 100644 --- a/backend/app/forms.py +++ b/backend/app/forms.py @@ -43,6 +43,10 @@ class EditProfileForm(FlaskForm): if user is not None: raise ValidationError('Please use a different username.') +class PostForm(FlaskForm): + post = TextAreaField('Post:', validators=[DataRequired(), Length(min=1, max=140)]) + submit = SubmitField('Submit') + class EmptyForm(FlaskForm): submit = SubmitField('Submit') diff --git a/backend/app/routes.py b/backend/app/routes.py index 921db5a..d2ae9a9 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -5,8 +5,8 @@ from datetime import datetime, timezone import sqlalchemy as sa from app import app, db -from app.forms import LoginForm, RegistrationForm, EditProfileForm, EmptyForm -from app.models import User +from app.forms import LoginForm, RegistrationForm, EditProfileForm, EmptyForm, PostForm +from app.models import User, Post @app.before_request def before_request(): @@ -14,25 +14,37 @@ def before_request(): current_user.last_seen = datetime.now(timezone.utc) db.session.commit() -@app.route('/') -@app.route('/index') +@app.route('/', methods=['GET', 'POST']) +@app.route('/index', methods=['GET', 'POST']) @login_required def index(): app.logger.info('@@@@@@@@@@@@@@@@@ INFO LOG TEST INDEX') + form = PostForm() + #user = {'username': 'aaa', 'email': 'a@a.a'} + if form.validate_on_submit(): + post = Post(body=form.post.data, author=current_user) + db.session.add(post) + db.session.commit() + flash('Your post has been added.') + return redirect(url_for('index')) + #posts = db.session.scalars(current_user.following_posts()).all() + page = request.args.get('page', 1, type=int) + posts = db.paginate(current_user.following_posts(), page=page, per_page=app.config['POSTS_PER_PAGE'], error_out=False) + next_url = url_for('index', page=posts.next_num) if posts.has_next else None + prev_url = url_for('index', page=posts.prev_num) if posts.has_prev else None - user = {'username': 'Finnaa'} - posts = [ - { - 'author': {'username': 'john'}, - 'body': 'Beautiful day 1' - }, - { - 'author': {'username': 'susie'}, - 'body': 'Movie is good.' - } - ] - #return posts; - return render_template('index.html', title='Home', posts=posts) + return render_template('index.html', title='Home', form=form, posts=posts.items, next_url=next_url, prev_url=prev_url) + +@app.route('/explore') +@login_required +def explore(): + query = sa.select(Post).order_by(Post.timestamp.desc()) + page = request.args.get('page', 1, type=int) + posts = db.paginate(query, page=page, per_page=app.config['POSTS_PER_PAGE'], error_out=False) + next_url = url_for('explore', page=posts.next_num) if posts.has_next else None + prev_url = url_for('explore', page=posts.prev_num) if posts.has_prev else None + + return render_template('index.html', title='Explore', posts=posts.items, next_url=next_url, prev_url=prev_url) @app.route('/login', methods=['GET', 'POST']) def login(): @@ -75,12 +87,14 @@ def register(): @login_required def user(username): user = db.first_or_404(sa.select(User).where(User.username == username)) - posts = [ - {'author': user, 'body': 'Test1'}, - {'author': user, 'body': 'Test2?'} - ] + page = request.args.get('page', 1, type=int) + query = user.posts.select().order_by(Post.timestamp.desc()) + posts = db.paginate(query, page=page, per_page=app.config['POSTS_PER_PAGE'], error_out=False) + next_url = url_for('user', username=user.username, page=posts.next_num) if posts.has_next else None + prev_url = url_for('user', username=user.username, page=posts.prev_num) if posts.has_prev else None + form = EmptyForm() - return render_template('user.html', user=user, posts=posts, form=form) + return render_template('user.html', user=user, posts=posts.items, next_url=next_url, prev_url=prev_url, form=form) @app.route('/edit_profile', methods=['GET', 'POST']) @login_required diff --git a/backend/app/templates/_post.html b/backend/app/templates/_post.html index 58f0f6c..dd71e87 100644 --- a/backend/app/templates/_post.html +++ b/backend/app/templates/_post.html @@ -1,6 +1,13 @@ - - - + + +
{{ post.author.username }} says:
{{ post.body }}
+ + + + {{ post.author.username }} + says:
+ {{ post.body }} +
diff --git a/backend/app/templates/base.html b/backend/app/templates/base.html index 1c5890a..51c3d51 100644 --- a/backend/app/templates/base.html +++ b/backend/app/templates/base.html @@ -3,19 +3,20 @@ {% if title %} - {{ title }} - blog + {{ title }} - blogpage {% else %} Welcome to blog. {% endif %}
- blgo: + blog: home + explore {% if current_user.is_anonymous %} login {% else %} - Profile + profile logout {% endif %} diff --git a/backend/app/templates/index.html b/backend/app/templates/index.html index e2b6d0b..c6d27f4 100644 --- a/backend/app/templates/index.html +++ b/backend/app/templates/index.html @@ -2,9 +2,25 @@ {% block content %}

Hello, {{ current_user.username }}!

+ {% if form %} +
+ {{ form.hidden_tag() }} +

+ {{ form.post.label }} + {{ form.post(cols=32, rows=4) }} + {% for error in form.post.errors %} + [{{ error }}] + {% endfor %} +

+

{{ form.submit() }}

+
+ {% endif %} {% for post in posts %} -

{{ post.author.username }} says: {{ post.body }}

+ {% include '_post.html' %} {% endfor %} + {% if prev_url %}Newer Posts{% endif %} + {% if next_url %}Older Posts{% endif %} + {% endblock %} diff --git a/backend/app/templates/user.html b/backend/app/templates/user.html index d4c6918..a4c5d8c 100644 --- a/backend/app/templates/user.html +++ b/backend/app/templates/user.html @@ -38,5 +38,8 @@ {% for post in posts %} {% include '_post.html' %} {% endfor %} + {% if prev_url %}Newer Posts{% endif %} + {% if next_url %}Older Posts{% endif %} + {% endblock %} diff --git a/backend/config.py b/backend/config.py index d05ad60..f6b5a7f 100644 --- a/backend/config.py +++ b/backend/config.py @@ -18,5 +18,6 @@ class Config: FROM_ADDRESS = os.environ.get('FROM_ADDRESS') DC_LOGGING = True + POSTS_PER_PAGE=5 From ed9df4db6ffe585878b912ce7fbf1840c9504d83 Mon Sep 17 00:00:00 2001 From: finn Date: Sun, 4 Aug 2024 22:00:07 -0700 Subject: [PATCH 22/25] mgt c10 checkpoint --- backend/Dockerfile | 2 +- backend/README.md | 2 ++ backend/app/__init__.py | 2 ++ backend/app/email.py | 9 +++++++++ backend/app/forms.py | 4 ++++ backend/app/routes.py | 16 +++++++++++++++- backend/app/templates/login.html | 1 + .../app/templates/reset_password_request.html | 17 +++++++++++++++++ backend/config.py | 4 ++-- backend/requirements.txt | 11 +++++------ 10 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 backend/app/email.py create mode 100644 backend/app/templates/reset_password_request.html diff --git a/backend/Dockerfile b/backend/Dockerfile index 2145c54..e6faad8 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -10,7 +10,7 @@ RUN apt update && apt install -y \ WORKDIR /code COPY requirements.txt /code RUN target=/root/.cache/pip \ - pip3 install -r requirements.txt + pip3 install --root-user-action=ignore -q -r requirements.txt # Need to make this explicit as part of expansion, no migrations or venv COPY . . diff --git a/backend/README.md b/backend/README.md index e2b34d0..198de84 100644 --- a/backend/README.md +++ b/backend/README.md @@ -18,6 +18,8 @@ pip install flask-migrate pip install flask-login pip install email-validator pip install pydenticon +pip install flask-mail +pip install pyjwt Prod only, require sys packages: pip install mariadb pip install uwsgi diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 8d36e49..758e496 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -5,6 +5,7 @@ from flask_migrate import Migrate from flask_login import LoginManager import logging, sys from logging.handlers import SMTPHandler +from flask_mail import Mail app = Flask(__name__) app.config.from_object(Config) @@ -12,6 +13,7 @@ db = SQLAlchemy(app) migrate = Migrate(app, db) login = LoginManager(app) login.login_view = 'login' +mail=Mail(app) if not app.debug: if app.config['MAIL_SERVER']: diff --git a/backend/app/email.py b/backend/app/email.py new file mode 100644 index 0000000..2adc7a8 --- /dev/null +++ b/backend/app/email.py @@ -0,0 +1,9 @@ +from flask_mail import Message +from app import mail + +def send_email(subject, sender, recipients, text_body, html_body): + msg = Message(subject, sender=sender, recipients=recipients) + msg.body = text_body + msg.html = html_body + mail.send(msg) + diff --git a/backend/app/forms.py b/backend/app/forms.py index 5d50a5c..25249ca 100644 --- a/backend/app/forms.py +++ b/backend/app/forms.py @@ -43,6 +43,10 @@ class EditProfileForm(FlaskForm): if user is not None: raise ValidationError('Please use a different username.') +class ResetPasswordRequestForm(FlaskForm): + email = StringField('Email', validators=[DataRequired(), Email()]) + submit = SubmitField('Request Password Reset') + class PostForm(FlaskForm): post = TextAreaField('Post:', validators=[DataRequired(), Length(min=1, max=140)]) submit = SubmitField('Submit') diff --git a/backend/app/routes.py b/backend/app/routes.py index d2ae9a9..099877a 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -5,8 +5,9 @@ from datetime import datetime, timezone import sqlalchemy as sa from app import app, db -from app.forms import LoginForm, RegistrationForm, EditProfileForm, EmptyForm, PostForm +from app.forms import LoginForm, RegistrationForm, EditProfileForm, EmptyForm, PostForm, ResetPasswordRequestForm from app.models import User, Post +#from app.email import send_password_reset_email @app.before_request def before_request(): @@ -149,4 +150,17 @@ def unfollow(username): else: return redirect(url_for('index')) +@app.route('/reset_password_request', methods=['GET', 'POST']) +def reset_password_request(): + if current_user.is_authenticated: + return redirect(url_for('index')) + form = ResetPasswordRequestForm() + if form.validate_on_submit(): + user = db.session.scalar(sa.select(User).where(User.email == form.email.data)) + if user: + send_password_reset_email(user) + flash('Password reset sent.') + return redirect(url_for('login')) + return render_template('reset_password_request.html', title='Reset Password', form=form) + diff --git a/backend/app/templates/login.html b/backend/app/templates/login.html index c604808..9bb8d2d 100644 --- a/backend/app/templates/login.html +++ b/backend/app/templates/login.html @@ -23,6 +23,7 @@

Register Here

+

Reset Password

{% endblock %} diff --git a/backend/app/templates/reset_password_request.html b/backend/app/templates/reset_password_request.html new file mode 100644 index 0000000..da2b56e --- /dev/null +++ b/backend/app/templates/reset_password_request.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block content %} +

Reset Password

+
+ {{ form.hidden_tag() }} +

+ {{ form.email.label }}
+ {{ form.email(size=64) }}
+ {% for error in form.email.errors %} + [{{ error }}] + {% endfor %} +

+

{{ form.submit() }}

+
+ +{% endblock %} diff --git a/backend/config.py b/backend/config.py index f6b5a7f..cf211e7 100644 --- a/backend/config.py +++ b/backend/config.py @@ -8,8 +8,8 @@ class Config: #SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('MYSQL_PASSWORD') + '@db:3306/flask' - #MAIL_SERVER = 'pmb' - MAIL_SERVER = '' + MAIL_SERVER = 'pmb' + #MAIL_SERVER = '' MAIL_PORT = 25 MAIL_USE_TLS = False MAIL_USERNAME = '' diff --git a/backend/requirements.txt b/backend/requirements.txt index 2cdaf64..aa2e006 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,6 +5,7 @@ dnspython==2.6.1 email_validator==2.2.0 Flask==3.0.3 Flask-Login==0.6.3 +Flask-Mail==0.10.0 Flask-Migrate==4.0.7 Flask-SQLAlchemy==3.1.1 Flask-WTF==1.2.1 @@ -13,17 +14,15 @@ idna==3.7 itsdangerous==2.2.0 Jinja2==3.1.4 Mako==1.3.5 +mariadb==1.1.10 MarkupSafe==2.1.5 +packaging==24.1 pillow==10.4.0 pydenticon==0.3.1 +PyJWT==2.9.0 python-dotenv==1.0.1 SQLAlchemy==2.0.31 typing_extensions==4.12.2 +uWSGI==2.0.26 Werkzeug==3.0.3 WTForms==3.1.2 -mariadb==1.1.10 -packaging==24.1 -setuptools==72.1.0 -uWSGI==2.0.26 -wheel==0.43.0 - From 3d1f21ffcb879c34b3fc554c79c32b8d07b61ee5 Mon Sep 17 00:00:00 2001 From: finn Date: Mon, 5 Aug 2024 00:59:01 -0700 Subject: [PATCH 23/25] mgt c10 checkpoint with debug --- backend/app/email.py | 15 ++++++- backend/app/forms.py | 7 +++- backend/app/models.py | 41 ++++++++++++++----- backend/app/routes.py | 29 ++++++++++++- .../app/templates/email/reset_password.html | 10 +++++ .../app/templates/email/reset_password.txt | 7 ++++ backend/app/templates/reset_password.html | 26 ++++++++++++ backend/config.py | 10 +++-- compose.yaml | 13 +++--- 9 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 backend/app/templates/email/reset_password.html create mode 100644 backend/app/templates/email/reset_password.txt create mode 100644 backend/app/templates/reset_password.html diff --git a/backend/app/email.py b/backend/app/email.py index 2adc7a8..cbf8f9e 100644 --- a/backend/app/email.py +++ b/backend/app/email.py @@ -1,5 +1,6 @@ +from flask import render_template from flask_mail import Message -from app import mail +from app import mail, app def send_email(subject, sender, recipients, text_body, html_body): msg = Message(subject, sender=sender, recipients=recipients) @@ -7,3 +8,15 @@ def send_email(subject, sender, recipients, text_body, html_body): msg.html = html_body mail.send(msg) +def send_password_reset_email(user): + token = user.get_reset_password_token() + hostname = app.config['REAL_HOSTNAME'] + send_email('[Blog] Reset Password', + sender=app.config['ADMINS'][0], + recipients=[user.email], + text_body=render_template('email/reset_password.txt', hostname=hostname, user=user, token=token), + html_body=render_template('email/reset_password.html', hostname=hostname, user=user, token=token)) + + + + diff --git a/backend/app/forms.py b/backend/app/forms.py index 25249ca..d9a5c14 100644 --- a/backend/app/forms.py +++ b/backend/app/forms.py @@ -17,17 +17,20 @@ class RegistrationForm(FlaskForm): password = PasswordField('Password', validators=[DataRequired()]) password2 = PasswordField('Repeat Password', validators=[DataRequired()]) submit = SubmitField('Register') - def validate_username(self, username): user = db.session.scalar(sa.select(User).where(User.username == username.data)) if user is not None: raise ValidationError('Please use a different username.') - def validate_email(self, email): user = db.session.scalar(sa.select(User).where(User.email == email.data)) if user is not None: raise ValidationError('Please use a different email address.') +class ResetPasswordForm(FlaskForm): + password = PasswordField('Password', validators=[DataRequired()]) + password2 = PasswordField('Repeat Password', validators=[DataRequired()]) + submit = SubmitField('Request Reset') + class EditProfileForm(FlaskForm): username = StringField('Username', validators=[DataRequired()]) about_me = TextAreaField('About me', validators=[Length(min=0, max=140)]) diff --git a/backend/app/models.py b/backend/app/models.py index 7131df2..dc71998 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -1,14 +1,17 @@ -import os from datetime import datetime, timezone from typing import Optional import sqlalchemy as sa import sqlalchemy.orm as so from werkzeug.security import generate_password_hash, check_password_hash -import pydenticon, hashlib, base64 +import os, pydenticon, hashlib, base64, jwt +from time import time -from app import db, login +from app import db, login, app from flask_login import UserMixin +#debug +import sys + followers = sa.Table( 'followers', db.metadata, @@ -39,15 +42,31 @@ class User(UserMixin, db.Model): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) + def get_reset_password_token(self, expires_in=600): + token = jwt.encode({'reset_password': self.id, 'exp': time() + expires_in}, + app.config['SECRET_KEY'], algorithm='HS256') + return token + @staticmethod + def verify_reset_password_token(token): + try: + id = jwt.decode(token, + app.config['SECRET_KEY'], algorithms='HS256')['reset_password'] + + except: + return + return db.session.get(User, id) + def gen_avatar(self, write_png=True): - foreground = [ "rgb(45,79,255)", - "rgb(254,180,44)", - "rgb(226,121,234)", - "rgb(30,179,253)", - "rgb(232,77,65)", - "rgb(49,203,115)", - "rgb(141,69,170)" ] - background = "rgb(22,22,22)" + foreground = ['#ACE1AF', + '#ACC4E1', + '#E1ACDE', + '#E1CAAC', + '#AFFF00', + '#00FFCF', + '#5000FF', + '#FF0030'] + + background = '#151515' digest = hashlib.md5(self.email.lower().encode('utf-8')).hexdigest() basedir = os.path.abspath(os.path.dirname(__file__)) diff --git a/backend/app/routes.py b/backend/app/routes.py index 099877a..0b3f724 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -5,9 +5,12 @@ from datetime import datetime, timezone import sqlalchemy as sa from app import app, db -from app.forms import LoginForm, RegistrationForm, EditProfileForm, EmptyForm, PostForm, ResetPasswordRequestForm +from app.forms import LoginForm, RegistrationForm, EditProfileForm, EmptyForm, PostForm, ResetPasswordRequestForm, ResetPasswordForm from app.models import User, Post -#from app.email import send_password_reset_email +from app.email import send_password_reset_email + +#debug: +import sys @app.before_request def before_request(): @@ -84,6 +87,25 @@ def register(): return redirect(url_for('login')) return render_template('register.html', title='Register', form=form) +@app.route('/reset_password/', methods=['GET', 'POST']) +def reset_password(token): + if current_user.is_authenticated: + print('rp user is authed', file=sys.stderr) + return redirect(url_for('index')) + user = User.verify_reset_password_token(token) + if not user: + print('rp not user', file=sys.stderr) + return redirect(url_for('index')) + form = ResetPasswordForm() + if form.validate_on_submit(): + print('rp validated', file=sys.stderr) + user.set_password(form.password.data) + db.session.commit() + flash('Your password has been reset.') + return redirect(url_for('login')) + return render_template('reset_password.html', form=form) + + @app.route('/user/') @login_required def user(username): @@ -153,11 +175,14 @@ def unfollow(username): @app.route('/reset_password_request', methods=['GET', 'POST']) def reset_password_request(): if current_user.is_authenticated: + print('rpr user is authed', file=sys.stderr) return redirect(url_for('index')) form = ResetPasswordRequestForm() if form.validate_on_submit(): + print('rpr form validated', file=sys.stderr) user = db.session.scalar(sa.select(User).where(User.email == form.email.data)) if user: + print('rpr if user', file=sys.stderr) send_password_reset_email(user) flash('Password reset sent.') return redirect(url_for('login')) diff --git a/backend/app/templates/email/reset_password.html b/backend/app/templates/email/reset_password.html new file mode 100644 index 0000000..3f14636 --- /dev/null +++ b/backend/app/templates/email/reset_password.html @@ -0,0 +1,10 @@ + + + +

User {{ user.username }} requested password reset.

+

Reset link:

+

click here +

If you did not request this, ignore this message.

+ + + diff --git a/backend/app/templates/email/reset_password.txt b/backend/app/templates/email/reset_password.txt new file mode 100644 index 0000000..f6ec353 --- /dev/null +++ b/backend/app/templates/email/reset_password.txt @@ -0,0 +1,7 @@ +User {{ user.username }} requested password reset. + +Reset link follows. + +{{ hostname }}{{ url_for('reset_password', token=token) }} + +If you did not request this, ignore this message. diff --git a/backend/app/templates/reset_password.html b/backend/app/templates/reset_password.html new file mode 100644 index 0000000..50f8dba --- /dev/null +++ b/backend/app/templates/reset_password.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block content %} + +

Reset Your Password

+
+ {{ form.hidden_tag() }} +

+ {{ form.password.label }} + {{ form.password(size=32) }} + {% for error in form.password.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.password2.label }} + {{ form.password2(size=32) }} + {% for error in form.password2.errors %} + [{{ error }}] + {% endfor %} +

+

{{ form.submit() }}

+
+ + +{% endblock %} diff --git a/backend/config.py b/backend/config.py index cf211e7..ac82c1e 100644 --- a/backend/config.py +++ b/backend/config.py @@ -4,9 +4,9 @@ basedir = os.path.abspath(os.path.dirname(__file__)) # Remove or fallbacks for prod class Config: - SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') or 'flasksk' + SECRET_KEY = os.environ.get('DOTENV_FLASK_SECRET_KEY') #SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') - SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('MYSQL_PASSWORD') + '@db:3306/flask' + SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('DOTENV_MYSQL_PASSWORD') + '@db:3306/flask' MAIL_SERVER = 'pmb' #MAIL_SERVER = '' @@ -14,8 +14,10 @@ class Config: MAIL_USE_TLS = False MAIL_USERNAME = '' MAIL_PASSWORD = '' - ADMINS = [os.environ.get('ADMIN_EMAIL')] - FROM_ADDRESS = os.environ.get('FROM_ADDRESS') + ADMINS = [os.environ.get('DOTENV_ADMIN_EMAIL')] + FROM_ADDRESS = os.environ.get('DOTENV_FROM_ADDRESS') + REAL_HOSTNAME = os.environ.get('DOTENV_REAL_HOSTNAME') + DC_LOGGING = True POSTS_PER_PAGE=5 diff --git a/compose.yaml b/compose.yaml index a4ffbc3..88cff10 100644 --- a/compose.yaml +++ b/compose.yaml @@ -37,11 +37,14 @@ services: environment: - MYSQL_USER=flasku #- MYSQL_PASSWORD=flaskp - - MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD} - - TOKEN_I=${DOTENV_TOKEN_I} - - TOKEN_C=${DOTENV_TOKEN_C} - - ADMIN_EMAIL=${ADMIN_EMAIL} - - FROM_ADDRESS=${GITEA_MAIL_FROM} + - DOTENV_MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD} + - DOTENV_FLASK_SECRET_KEY=${FLASK_SECRET_KEY} + - DOTENV_TOKEN_I=${FLASK_TOKEN_I} + - DOTENV_TOKEN_C=${FLASK_TOKEN_C} + - DOTENV_ADMIN_EMAIL=${FLASK_ADMIN_EMAIL} + - DOTENV_FROM_ADDRESS=${FLASK_MAIL_FROM} + - DOTENV_JWT_PHRASE=${FLASK_JWT_PHRASE} + - DOTENV_REAL_HOSTNAME=${FLASK_REAL_HOSTNAME} #ports: # - 8000:8000 expose: From b3b188f3704dc1e766d1bb6af3c5444de001e24d Mon Sep 17 00:00:00 2001 From: finn Date: Mon, 5 Aug 2024 01:28:09 -0700 Subject: [PATCH 24/25] mgt c10 complete merge to put bootstrap css changes on their own fork --- backend/app/email.py | 7 ++++++- backend/app/models.py | 2 +- backend/app/routes.py | 8 +------- backend/app/templates/base.html | 9 ++++++--- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/backend/app/email.py b/backend/app/email.py index cbf8f9e..d5db7f4 100644 --- a/backend/app/email.py +++ b/backend/app/email.py @@ -1,12 +1,17 @@ +from threading import Thread from flask import render_template from flask_mail import Message from app import mail, app +def send_async_email(app, msg): + with app.app_context(): + mail.send(msg) + def send_email(subject, sender, recipients, text_body, html_body): msg = Message(subject, sender=sender, recipients=recipients) msg.body = text_body msg.html = html_body - mail.send(msg) + Thread(target=send_async_email, args=(app, msg)).start() def send_password_reset_email(user): token = user.get_reset_password_token() diff --git a/backend/app/models.py b/backend/app/models.py index dc71998..e93f60b 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -10,7 +10,7 @@ from app import db, login, app from flask_login import UserMixin #debug -import sys +#import sys followers = sa.Table( 'followers', diff --git a/backend/app/routes.py b/backend/app/routes.py index 0b3f724..c644031 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -10,7 +10,7 @@ from app.models import User, Post from app.email import send_password_reset_email #debug: -import sys +#import sys @app.before_request def before_request(): @@ -90,15 +90,12 @@ def register(): @app.route('/reset_password/', methods=['GET', 'POST']) def reset_password(token): if current_user.is_authenticated: - print('rp user is authed', file=sys.stderr) return redirect(url_for('index')) user = User.verify_reset_password_token(token) if not user: - print('rp not user', file=sys.stderr) return redirect(url_for('index')) form = ResetPasswordForm() if form.validate_on_submit(): - print('rp validated', file=sys.stderr) user.set_password(form.password.data) db.session.commit() flash('Your password has been reset.') @@ -175,14 +172,11 @@ def unfollow(username): @app.route('/reset_password_request', methods=['GET', 'POST']) def reset_password_request(): if current_user.is_authenticated: - print('rpr user is authed', file=sys.stderr) return redirect(url_for('index')) form = ResetPasswordRequestForm() if form.validate_on_submit(): - print('rpr form validated', file=sys.stderr) user = db.session.scalar(sa.select(User).where(User.email == form.email.data)) if user: - print('rpr if user', file=sys.stderr) send_password_reset_email(user) flash('Password reset sent.') return redirect(url_for('login')) diff --git a/backend/app/templates/base.html b/backend/app/templates/base.html index 51c3d51..ff4f233 100644 --- a/backend/app/templates/base.html +++ b/backend/app/templates/base.html @@ -9,8 +9,8 @@ {% endif %} -
- blog: +
+
+ +

oily.dad

+

destroy me

+
{% with messages = get_flashed_messages() %} {% if messages %} From 5168e6cd733f17c31edec2a7f36cca2885c9f825 Mon Sep 17 00:00:00 2001 From: finn Date: Mon, 5 Aug 2024 01:36:10 -0700 Subject: [PATCH 25/25] post c10 cleanup --- backend/Dockerfile | 2 +- backend/config.py | 1 - compose.yaml.local | 20 +++++++++++++------- compose.yaml.prod | 24 +++++++++++++++--------- dotenv | 15 +++++++++------ 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index e6faad8..51ad67d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -12,7 +12,7 @@ COPY requirements.txt /code RUN target=/root/.cache/pip \ pip3 install --root-user-action=ignore -q -r requirements.txt -# Need to make this explicit as part of expansion, no migrations or venv +# Dockerignore has this skip migrations, venv, sqlite db COPY . . ENV FLASK_APP microblog.py diff --git a/backend/config.py b/backend/config.py index ac82c1e..94539de 100644 --- a/backend/config.py +++ b/backend/config.py @@ -9,7 +9,6 @@ class Config: SQLALCHEMY_DATABASE_URI = 'mariadb+mariadbconnector://flasku:' + os.environ.get('DOTENV_MYSQL_PASSWORD') + '@db:3306/flask' MAIL_SERVER = 'pmb' - #MAIL_SERVER = '' MAIL_PORT = 25 MAIL_USE_TLS = False MAIL_USERNAME = '' diff --git a/compose.yaml.local b/compose.yaml.local index e0f2c61..88cff10 100644 --- a/compose.yaml.local +++ b/compose.yaml.local @@ -28,17 +28,23 @@ services: build: context: backend target: builder - restart: always + # Next two are only debug, used without restart + stdin_open: true + tty: true + #restart: always # Comment following line to use flask (1worker, dev), uncomment to use uwsgi (wsgi) - #command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "app:server"] + #command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "microblog:app"] environment: - MYSQL_USER=flasku #- MYSQL_PASSWORD=flaskp - - MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD} - - TOKEN_I=${DOTENV_TOKEN_I} - - TOKEN_C=${DOTENV_TOKEN_C} - - ADMIN_EMAIL=${ADMIN_EMAIL} - - FROM_ADDRESS=${GITEA_MAIL_FROM} + - DOTENV_MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD} + - DOTENV_FLASK_SECRET_KEY=${FLASK_SECRET_KEY} + - DOTENV_TOKEN_I=${FLASK_TOKEN_I} + - DOTENV_TOKEN_C=${FLASK_TOKEN_C} + - DOTENV_ADMIN_EMAIL=${FLASK_ADMIN_EMAIL} + - DOTENV_FROM_ADDRESS=${FLASK_MAIL_FROM} + - DOTENV_JWT_PHRASE=${FLASK_JWT_PHRASE} + - DOTENV_REAL_HOSTNAME=${FLASK_REAL_HOSTNAME} #ports: # - 8000:8000 expose: diff --git a/compose.yaml.prod b/compose.yaml.prod index f1ab81f..98403d8 100644 --- a/compose.yaml.prod +++ b/compose.yaml.prod @@ -28,17 +28,23 @@ services: build: context: backend target: builder + # Next two are only debug, used without restart + #stdin_open: true + #tty: true restart: always # Comment following line to use flask (1worker, dev), uncomment to use uwsgi (wsgi) - command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "app:server"] + command: ["uwsgi", "--http", "0.0.0.0:8000", "--master", "-p", "4", "-w", "microblog:app"] environment: - MYSQL_USER=flasku #- MYSQL_PASSWORD=flaskp - - MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD} - - TOKEN_I=${DOTENV_TOKEN_I} - - TOKEN_C=${DOTENV_TOKEN_C} - - ADMIN_EMAIL=${ADMIN_EMAIL} - - FROM_ADDRESS=${GITEA_MAIL_FROM} + - DOTENV_MYSQL_PASSWORD=${DOTENV_MYSQL_FLASK_PASSWORD} + - DOTENV_FLASK_SECRET_KEY=${FLASK_SECRET_KEY} + - DOTENV_TOKEN_I=${FLASK_TOKEN_I} + - DOTENV_TOKEN_C=${FLASK_TOKEN_C} + - DOTENV_ADMIN_EMAIL=${FLASK_ADMIN_EMAIL} + - DOTENV_FROM_ADDRESS=${FLASK_MAIL_FROM} + - DOTENV_JWT_PHRASE=${FLASK_JWT_PHRASE} + - DOTENV_REAL_HOSTNAME=${FLASK_REAL_HOSTNAME} #ports: # - 8000:8000 expose: @@ -90,9 +96,9 @@ services: proxy: build: proxy restart: always - volumes: - - /home/finn/d/cert/var/lib/letsencrypt:/var/lib/letsencrypt - - /home/finn/d/cert/etc/letsencrypt:/etc/letsencrypt + #volumes: + # - /home/finn/d/cert/var/lib/letsencrypt:/var/lib/letsencrypt + # - /home/finn/d/cert/etc/letsencrypt:/etc/letsencrypt ports: - 80:80 - 443:443 diff --git a/dotenv b/dotenv index 0f2a538..d93dd76 100644 --- a/dotenv +++ b/dotenv @@ -5,7 +5,7 @@ DOTENV_MYSQL_ROOT_PASSWORD=rootp DOTENV_MYSQL_GITEA_PASSWORD=giteap DOTENV_MYSQL_FLASK_PASSWORD=flaskp -GITEA_MAIL_FROM=gitea@gitea.changeme +GITEA_MAIL_FROM="git@changeme" # Build ARG GPG_PP. May still need to be empty to avoid breakage. BUILD_GPG_PP= @@ -13,12 +13,15 @@ BUILD_GPG_PP= # Backend: +FLASK_SECRET_KEY="changeme" # Inconsequential token: minimal inconvenience if exposed -DOTENV_TOKEN_I=dti +FLASK_TOKEN_I=dti # Consequential token: protect -DOTENV_TOKEN_C=dtc - -# Destination address for handler mailer -ADMIN_EMAIL="email@email.changeme" +FLASK_TOKEN_C=dtc +FLASK_MAIL_FROM="git@changeme" +# admin email must be valid send from with mail subsystem +FLASK_ADMIN_EMAIL="git@changeme" +FLASK_JWT_PHRASE="jwtphrase" +FLASK_REAL_HOSTNAME="localhost"