From 9071f5ff7a9f3a7a84a38bedaaf431032c87e55f Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Wed, 12 Nov 2025 10:34:57 +0100 Subject: [PATCH 1/2] change credential access for /admin/, style changes, fix and deprecate get_current_user() --- src/coriplus/__init__.py | 2 +- src/coriplus/admin.py | 7 +++---- src/coriplus/static/style.css | 11 +++++++---- .../templates/includes/infobox_profile.html | 16 ++-------------- src/coriplus/utils.py | 9 +++++---- src/coriplus/website.py | 9 ++++++--- 6 files changed, 24 insertions(+), 30 deletions(-) diff --git a/src/coriplus/__init__.py b/src/coriplus/__init__.py index 98b4d85..9543c60 100644 --- a/src/coriplus/__init__.py +++ b/src/coriplus/__init__.py @@ -24,7 +24,7 @@ from flask_wtf import CSRFProtect import dotenv import logging -__version__ = '0.10.0-dev44' +__version__ = '0.10.0-dev45' # we want to support Python 3.10+ only. # Python 2 has too many caveats. diff --git a/src/coriplus/admin.py b/src/coriplus/admin.py index a78cf1b..d8c8fb0 100644 --- a/src/coriplus/admin.py +++ b/src/coriplus/admin.py @@ -13,17 +13,16 @@ from functools import wraps bp = Blueprint('admin', __name__, url_prefix='/admin') -def _check_auth(username, password) -> bool: +def _check_auth(username) -> bool: try: - return User.select().where((User.username == username) & (User.password == pwdhash(password)) & (User.is_admin) - ).exists() + return User.get((User.username == username)).is_admin except User.DoesNotExist: return False def admin_required(f): @wraps(f) def wrapped_view(**kwargs): - if not _check_auth(current_user.username, current_user.password): + if not _check_auth(current_user.username): abort(403) return f(**kwargs) return wrapped_view diff --git a/src/coriplus/static/style.css b/src/coriplus/static/style.css index c238285..cb05c09 100644 --- a/src/coriplus/static/style.css +++ b/src/coriplus/static/style.css @@ -2,6 +2,9 @@ --accent: #f0372e; --link: #3399ff; } +* { + box-sizing: border-box; +} body, button, input, select, textarea { font-family: Inter, Roboto, sans-serif; line-height: 1.6; @@ -38,13 +41,13 @@ a:hover{text-decoration:underline} #site-name {text-align: center;flex: 1} .header h1{margin:0;display:inline-block} .flash{background-color:#ff9;border:yellow 1px solid} -.infobox{padding:12px;border:#ccc 1px solid} -@media (min-width:640px) { - .infobox{float:right;width:320px} +.infobox{width: 50%; float: right;} +@media (max-width:639px) { + .infobox{width: 100%;} } .weak{opacity:.5} .field_desc{display:block} -ul.timeline{padding:0;margin:auto;max-width:960px} +ul.timeline{padding:0;margin:auto;max-width:960px;clear: both} ul.timeline > li{list-style:none;} .message-visual img{max-width:100%;margin:auto} .message-options-showhide::before{content:'\2026'} diff --git a/src/coriplus/templates/includes/infobox_profile.html b/src/coriplus/templates/includes/infobox_profile.html index 2cec0b7..d1b1494 100644 --- a/src/coriplus/templates/includes/infobox_profile.html +++ b/src/coriplus/templates/includes/infobox_profile.html @@ -1,27 +1,15 @@ {% set profile = user.profile %} -
+

{{ profile.full_name }}

{{ profile.biography|enrich }}

{% if profile.location %}

Location: {{ profile.location|locationdata }}

{% endif %} - {% if profile.year %} -

Year: {{ profile.year }}

- {% endif %} {% if profile.website %} {% set website = profile.website %} {% set website = website if website.startswith(('http://', 'https://')) else 'http://' + website %}

Website: {{ profile.website|urlize }}

{% endif %} - {% if profile.instagram %} -

Instagram: {{ profile.instagram }}

- {% endif %} - {% if profile.facebook %} -

Facebook: {{ profile.facebook }}

- {% endif %} - {% if profile.telegram %} -

Telegram: {{ profile.telegram }}

- {% endif %}

{{ user.messages|count }} messages - @@ -30,6 +18,6 @@ {{ user.following()|count }} following

{% if user == current_user %} -

{{ inline_svg('edit', 18) }} Edit profile

+

{{ inline_svg('edit') }} Edit profile

{% endif %}
diff --git a/src/coriplus/utils.py b/src/coriplus/utils.py index 7a98d5b..1db7414 100644 --- a/src/coriplus/utils.py +++ b/src/coriplus/utils.py @@ -3,6 +3,8 @@ A list of utilities used across modules. ''' import datetime, re, base64, hashlib, string, sys, json + +from flask_login import current_user from .models import User, Message, Notification, MSGPRV_PUBLIC, MSGPRV_UNLISTED, \ MSGPRV_FRIENDS, MSGPRV_ONLYME from flask import abort, render_template, request, session @@ -102,15 +104,14 @@ except OSError: # get the user from the session # changed in 0.5 to comply with flask_login +# DEPRECATED in 0.10; use current_user instead def get_current_user(): # new in 0.7; need a different method to get current user id if request.path.startswith('/api/'): # assume token validation is already done return User[request.args['access_token'].split(':')[0]] - else: - user_id = session.get('user_id') - if user_id: - return User[user_id] + elif current_user.is_authenticated: + return current_user def push_notification(type, target, **kwargs): try: diff --git a/src/coriplus/website.py b/src/coriplus/website.py index df43b86..9612c66 100644 --- a/src/coriplus/website.py +++ b/src/coriplus/website.py @@ -7,7 +7,7 @@ from .models import * from . import __version__ as app_version from sys import version as python_version from flask import Blueprint, abort, flash, redirect, render_template, request, url_for, __version__ as flask_version -from flask_login import login_required, login_user, logout_user +from flask_login import current_user, login_required, login_user, logout_user import json import logging @@ -17,7 +17,7 @@ bp = Blueprint('website', __name__) @bp.route('/') def homepage(): - if get_current_user(): + if current_user and current_user.is_authenticated: return private_timeline() else: return render_template('homepage.html') @@ -26,7 +26,7 @@ def private_timeline(): # the private timeline (aka feed) exemplifies the use of a subquery -- we are asking for # messages where the person who created the message is someone the current # user is following. these messages are then ordered newest-first. - user = get_current_user() + user = current_user messages = Visibility(Message .select() .where((Message.user << user.following()) @@ -83,6 +83,9 @@ def register(): @bp.route('/login/', methods=['GET', 'POST']) def login(): + if current_user and current_user.is_authenticated: + flash('You are already logged in') + return redirect(request.args.get('next', '/')) if request.method == 'POST' and request.form['username']: try: username = request.form['username'] From 536e49d1b9314604e2846d0740a377c3979c3512 Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Wed, 12 Nov 2025 11:02:53 +0100 Subject: [PATCH 2/2] schema changes --- genmig.sh | 3 +- src/coriplus/models.py | 19 +++-- .../templates/includes/infobox_profile.html | 15 ++-- src/coriplus/website.py | 7 +- .../002_move_columns_from_userprofile.py | 81 +++++++++++++++++++ 5 files changed, 104 insertions(+), 21 deletions(-) create mode 100644 src/migrations/002_move_columns_from_userprofile.py diff --git a/genmig.sh b/genmig.sh index a29c9fb..c141519 100755 --- a/genmig.sh +++ b/genmig.sh @@ -6,4 +6,5 @@ source .env && \ case "$1" in ("+") pw_migrate create --auto --auto-source=coriplus.models --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;; ("@") pw_migrate migrate --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;; -esac \ No newline at end of file + (\\) pw_migrate rollback --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;; +esac diff --git a/src/coriplus/models.py b/src/coriplus/models.py index 5a2ae50..f47fa65 100644 --- a/src/coriplus/models.py +++ b/src/coriplus/models.py @@ -29,19 +29,25 @@ class BaseModel(Model): # A user. The user is separated from its page. class User(BaseModel): # The unique username. - username = CharField(unique=True) + username = CharField(30, unique=True) # The user's full name (here for better search since 0.8) - full_name = TextField() + full_name = CharField(80) # The password hash. - password = CharField() + password = CharField(256) # An email address. - email = CharField() + email = CharField(256) # The date of birth (required because of Terms of Service) birthday = DateField() # The date joined join_date = DateTimeField() # A disabled flag. 0 = active, 1 = disabled by user, 2 = banned is_disabled = IntegerField(default=0) + # Short description of user. + biography = CharField(256, default='') + # Personal website. + website = TextField(null=True) + + # Helpers for flask_login def get_id(self): @@ -110,15 +116,12 @@ class UserAdminship(BaseModel): # User profile. # Additional info for identifying users. # New in 0.6 +# Deprecated in 0.10 and merged with User class UserProfile(BaseModel): user = ForeignKeyField(User, primary_key=True) biography = TextField(default='') location = IntegerField(null=True) - year = IntegerField(null=True) website = TextField(null=True) - instagram = TextField(null=True) - facebook = TextField(null=True) - telegram = TextField(null=True) @property def full_name(self): ''' diff --git a/src/coriplus/templates/includes/infobox_profile.html b/src/coriplus/templates/includes/infobox_profile.html index d1b1494..bc08a20 100644 --- a/src/coriplus/templates/includes/infobox_profile.html +++ b/src/coriplus/templates/includes/infobox_profile.html @@ -1,14 +1,11 @@ -{% set profile = user.profile %} +
-

{{ profile.full_name }}

-

{{ profile.biography|enrich }}

- {% if profile.location %} -

Location: {{ profile.location|locationdata }}

- {% endif %} - {% if profile.website %} - {% set website = profile.website %} +

{{ user.full_name }}

+

{{ user.biography|enrich }}

+ {% if user.website %} + {% set website = user.website %} {% set website = website if website.startswith(('http://', 'https://')) else 'http://' + website %} -

Website: {{ profile.website|urlize }}

+

Website: {{ website|urlize }}

{% endif %}

{{ user.messages|count }} messages diff --git a/src/coriplus/website.py b/src/coriplus/website.py index 9612c66..14b944c 100644 --- a/src/coriplus/website.py +++ b/src/coriplus/website.py @@ -137,11 +137,12 @@ def user_follow(username): from_user=cur_user, to_user=user, created_date=datetime.datetime.now()) + push_notification('follow', user, user=cur_user.id) + flash('You are now following %s' % user.username) except IntegrityError: - pass + flash(f'Error following {user.username}') + - flash('You are following %s' % user.username) - push_notification('follow', user, user=cur_user.id) return redirect(url_for('website.user_detail', username=user.username)) @bp.route('/+/unfollow/', methods=['POST']) diff --git a/src/migrations/002_move_columns_from_userprofile.py b/src/migrations/002_move_columns_from_userprofile.py new file mode 100644 index 0000000..0b2d004 --- /dev/null +++ b/src/migrations/002_move_columns_from_userprofile.py @@ -0,0 +1,81 @@ +"""Peewee migrations -- 002_move_columns_from_userprofile.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + migrator.add_fields( + 'user', + + biography=pw.CharField(max_length=256, default=""), + website=pw.TextField(null=True)) + + migrator.change_fields('user', username=pw.CharField(max_length=30, unique=True)) + + migrator.change_fields('user', full_name=pw.CharField(max_length=80)) + + migrator.change_fields('user', password=pw.CharField(max_length=256)) + + migrator.change_fields('user', email=pw.CharField(max_length=256)) + + migrator.sql(""" + UPDATE "user" SET biography = (SELECT p.biography FROM userprofile p WHERE p.user_id = id LIMIT 1), + website = (SELECT p.website FROM userprofile p WHERE p.user_id = id LIMIT 1); + """) + + migrator.remove_fields('userprofile', 'year', 'instagram', 'facebook', 'telegram') + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.add_fields( + 'userprofile', + + year=pw.IntegerField(null=True), + instagram=pw.TextField(null=True), + facebook=pw.TextField(null=True), + telegram=pw.TextField(null=True)) + + migrator.remove_fields('user', 'biography', 'website') + + migrator.change_fields('user', username=pw.CharField(max_length=255, unique=True)) + + migrator.change_fields('user', full_name=pw.TextField()) + + migrator.change_fields('user', password=pw.CharField(max_length=255)) + + migrator.change_fields('user', email=pw.CharField(max_length=255))