Compare commits

...

2 Commits

Author SHA1 Message Date
469785ee33 mgt c8 checkpoint at tests 2024-08-04 05:44:20 -07:00
eb0f19b109 pre-c8 checkpoint 2024-08-04 04:39:49 -07:00
12 changed files with 110 additions and 21 deletions

View File

@ -2,12 +2,10 @@
### Sec: ### Sec:
* This repo is public. Mind cred slip-ups. - 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. - 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. - Note app Dockerfile debug console, found at /console. Werkzeug/flask is WILDLY insecure if left in dev/dbg.
* Avoid docker socks stuff. - Avoid docker socks stuff.
### Install: ### Install:
@ -49,19 +47,28 @@ set up cron job for script
pmb-pf - git clone of my mail thing pmb-pf - git clone of my mail thing
other - ref and non-sensitive files for dns other - ref and non-sensitive files for dns
### Timeline: ### Setup cheat:
set up certbot dns\ - set up certbot dns (prod)
see tar of cert dir with script - 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: ### 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. 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: ### Changing gitea subdomain:
Find in proxy/conf.\ Find in proxy/conf.\
Find in gitea conf.\ Find in gitea conf.\
Rebuild images. 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

View File

@ -1,3 +1,4 @@
venv venv
migrations migrations
zapp.db

View File

@ -1,7 +1,11 @@
# syntax=docker/dockerfile:1.4 # syntax=docker/dockerfile:1.4
FROM python:3-slim-bookworm AS builder 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 WORKDIR /code
COPY requirements.txt /code COPY requirements.txt /code

View File

@ -18,8 +18,14 @@ pip install flask-migrate
pip install flask-login pip install flask-login
pip install email-validator pip install email-validator
pip install pydenticon pip install pydenticon
Prod only, require sys packages:
pip install mariadb pip install mariadb
pip install uwsgi
... ...
```
Freeze/requirements.txt. Better to audit this inside python:3-bookworm-slim container.
```
pip freeze > requirements.txt pip freeze > requirements.txt
``` ```
@ -36,8 +42,11 @@ flask db downgrade base
flask db upgrade flask db upgrade
``` ```
Full reset: Full reset or maria init:
``` ```
sql:
drop table users;
drop table posts;
rm app.db rm app.db
rm -r migrations rm -r migrations
flask db init flask db init

View File

@ -26,11 +26,12 @@ if not app.debug:
app.logger.addHandler(mail_handler) app.logger.addHandler(mail_handler)
if app.config['DC_LOGGING']: 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 = logging.StreamHandler(stream=sys.stderr)
dclog.setLevel(logging.INFO) dclog.setLevel(logging.INFO)
dclog.propagate = False dclog.propagate = False
app.logger.addHandler(dclog) app.logger.addHandler(dclog)
app.logger.info('@@@@@@@@@@@@@@@@@@@@@ TEST LOGGER INFO MESSAGE')
from app import routes, models, errors from app import routes, models, errors

View File

@ -9,6 +9,13 @@ import pydenticon, hashlib, base64
from app import db, login from app import db, login
from flask_login import UserMixin 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): class User(UserMixin, db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True) id: so.Mapped[int] = so.mapped_column(primary_key=True)
username: so.Mapped[str] = so.mapped_column(sa.String(64), index=True, unique=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') posts: so.WriteOnlyMapped['Post'] = so.relationship(back_populates='author')
about_me: so.Mapped[Optional[str]] = so.mapped_column(sa.String(140)) 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)) 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): def set_password(self, password):
self.password_hash = generate_password_hash(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') pngloc = os.path.join(basedir, 'usercontent', 'identicon', digest + '.png')
return pngloc 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): def __repr__(self):
return '<User {}>'.format(self.username) return '<User {}>'.format(self.username)

View File

@ -5,8 +5,8 @@ basedir = os.path.abspath(os.path.dirname(__file__))
class Config: class Config:
SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') or 'flasksk' SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') or 'flasksk'
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'zapp.db') #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('MYSQL_PASSWORD') + '@db:3306/flask'
#MAIL_SERVER = 'pmb' #MAIL_SERVER = 'pmb'
MAIL_SERVER = '' MAIL_SERVER = ''

4
backend/dbdb.sh Normal file
View File

@ -0,0 +1,4 @@
#!/bin/bash
# Okay to publish -- creds are local dev only
mariadb -hdb -uflasku -pflaskp flask

View File

@ -21,5 +21,9 @@ SQLAlchemy==2.0.31
typing_extensions==4.12.2 typing_extensions==4.12.2
Werkzeug==3.0.3 Werkzeug==3.0.3
WTForms==3.1.2 WTForms==3.1.2
uwsgi mariadb==1.1.10
mariadb packaging==24.1
setuptools==72.1.0
uWSGI==2.0.26
wheel==0.43.0

5
backend/tests.py Normal file
View File

@ -0,0 +1,5 @@
import os
os.environ['DATABASE_URL'] = 'sqlite://'
from datetime import datetime, timezone, timedelta

View File

@ -28,9 +28,12 @@ services:
build: build:
context: backend context: backend
target: builder 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) # 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: environment:
- MYSQL_USER=flasku - MYSQL_USER=flasku
#- MYSQL_PASSWORD=flaskp #- MYSQL_PASSWORD=flaskp

6
dotenv
View File

@ -5,7 +5,7 @@ DOTENV_MYSQL_ROOT_PASSWORD=rootp
DOTENV_MYSQL_GITEA_PASSWORD=giteap DOTENV_MYSQL_GITEA_PASSWORD=giteap
DOTENV_MYSQL_FLASK_PASSWORD=flaskp 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 ARG GPG_PP. May still need to be empty to avoid breakage.
BUILD_GPG_PP= BUILD_GPG_PP=
@ -18,3 +18,7 @@ DOTENV_TOKEN_I=dti
# Consequential token: protect # Consequential token: protect
DOTENV_TOKEN_C=dtc DOTENV_TOKEN_C=dtc
# Destination address for handler mailer
ADMIN_EMAIL="email@email.changeme"