From 156d58e5499802e0deb6eb56628b9b7dff46616f Mon Sep 17 00:00:00 2001 From: Mattia Succurro Date: Wed, 16 Oct 2019 19:06:09 +0200 Subject: [PATCH 01/41] Changing version number --- .gitignore | 1 + CHANGELOG.md | 4 +++- app.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index dd7c762..0bd8872 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ coriplus.sqlite +coriplus-*.sqlite __pycache__/ uploads/ *.pyc diff --git a/CHANGELOG.md b/CHANGELOG.md index 58ee3f1..b7823b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog -## 0.5-dev +## 0.6-dev + +## 0.5.0 * Removed `type` and `info` fields from `Message` table and merged `privacy` field, previously into a separate table, into that table. In order to make the app work, when upgrading you should run the `migrate_0_4_to_0_5.py` script. * Added flask-login dependency. Now, user logins can be persistent up to 365 days. diff --git a/app.py b/app.py index b7c92da..cd5cb29 100644 --- a/app.py +++ b/app.py @@ -8,7 +8,7 @@ from functools import wraps import argparse from flask_login import LoginManager, login_user, logout_user, login_required -__version__ = '0.5-dev' +__version__ = '0.6-dev' # we want to support Python 3 only. # Python 2 has too many caveats. From 32e7c37158fbdf5e125eb80ebe695a26afef4e62 Mon Sep 17 00:00:00 2001 From: Mattia Succurro Date: Thu, 17 Oct 2019 14:34:55 +0200 Subject: [PATCH 02/41] Adding profiles and adminship --- CHANGELOG.md | 7 +++ app.py | 57 +++++++++++++++++++++++-- static/style.css | 5 +++ templates/edit_profile.html | 13 ++++++ templates/includes/infobox_profile.html | 30 +++++++++++++ templates/join.html | 26 +++++++++-- templates/user_detail.html | 11 +---- 7 files changed, 133 insertions(+), 16 deletions(-) create mode 100644 templates/edit_profile.html create mode 100644 templates/includes/infobox_profile.html diff --git a/CHANGELOG.md b/CHANGELOG.md index b7823b4..a63f553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## 0.6-dev +* Added user adminship. Admins are users with very high privileges. Adminship can be assigned only at script level (not from the web). +* Now one's messages won't show up in public timeline. +* Added user profile info. Now you can specify your full name, biography, location, birth year, website, Facebook and Instagram. Of course this is totally optional. +* Added reference to terms of service and privacy policy on signup page. +* When visiting signup page as logged in, user should confirm he wants to create another account in order to do it. +* Moved user stats inside profile info. + ## 0.5.0 * Removed `type` and `info` fields from `Message` table and merged `privacy` field, previously into a separate table, into that table. In order to make the app work, when upgrading you should run the `migrate_0_4_to_0_5.py` script. diff --git a/app.py b/app.py index cd5cb29..9c04f8d 100644 --- a/app.py +++ b/app.py @@ -93,6 +93,38 @@ class User(BaseModel): .where( (Notification.target == self) & (Notification.seen == 0) )) + # user adminship is stored into a separate table; new in 0.6 + @property + def is_admin(self): + return UserAdminship.select().where(UserAdminship.user == self).exists() + # user profile info; new in 0.6 + @property + def profile(self): + # lazy initialization; I don't want (and don't know how) + # to do schema changes. + try: + return UserProfile.get(UserProfile.user == self) + except UserProfile.DoesNotExist: + return UserProfile.create(user=self, full_name=self.username) + +# User adminship. +# A very high privilege where users can review posts. +# For very few users only; new in 0.6 +class UserAdminship(BaseModel): + user = ForeignKeyField(User, primary_key=True) + +# User profile. +# Additional info for identifying users. +# New in 0.6 +class UserProfile(BaseModel): + user = ForeignKeyField(User, primary_key=True) + full_name = TextField() + biography = TextField(default='') + location = IntegerField(null=True) + year = IntegerField(null=True) + website = TextField(null=True) + instagram = TextField(null=True) + facebook = TextField(null=True) # The message privacy values. MSGPRV_PUBLIC = 0 # everyone @@ -118,11 +150,11 @@ class Message(BaseModel): privacy = self.privacy if user == cur_user: # short path - return True + # also: don't show user's messages in public timeline + return not is_public_timeline elif privacy == MSGPRV_PUBLIC: return True elif privacy == MSGPRV_UNLISTED: - # TODO user's posts may appear the same in public timeline, # even if unlisted return not is_public_timeline elif privacy == MSGPRV_FRIENDS: @@ -172,7 +204,8 @@ class Notification(BaseModel): def create_tables(): with database: database.create_tables([ - User, Message, Relationship, Upload, Notification]) + User, UserAdminship, UserProfile, Message, Relationship, + Upload, Notification]) if not os.path.isdir(UPLOAD_DIRECTORY): os.makedirs(UPLOAD_DIRECTORY) @@ -384,6 +417,11 @@ def register(): username = request.form['username'].lower() if not is_username(username): flash('This username is invalid') + return render_template('join.html') + if username == getattr(get_current_user(), 'username', None) and not request.form.get('confirm_another'): + flash('You are already logged in. Please confirm you want to ' + 'create another account by checking the option.') + return render_template('join.html') try: with database.atomic(): # Attempt to create the user. If the username is taken, due to the @@ -394,6 +432,10 @@ def register(): email=request.form['email'], birthday=birthday, join_date=datetime.datetime.now()) + UserProfile.create( + user=user, + full_name=request.form.get('full_name') or username + ) # mark the user as being 'authenticated' by setting the session vars login_user(user) @@ -562,6 +604,15 @@ def edit(id): #def confirm_delete(id): # return render_template('confirm_delete.html') +@app.route('/edit_profile/', methods=['GET', 'POST']) +def edit_profile(): + if request.method == 'POST': + user = get_current_user() + username = request.form['username'] + if username != user.username: + User.update(username=username).where(User.id == user.id).execute() + return render_template('edit_profile.html') + @app.route('/notifications/') @login_required def notifications(): diff --git a/static/style.css b/static/style.css index baa794c..650f108 100644 --- a/static/style.css +++ b/static/style.css @@ -8,7 +8,12 @@ body{margin:0} .metanav{float:right} .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} +} .weak{opacity:.5} +.field_desc{display:block} .message-visual img{max-width:100%;max-height:8em} .message-options-showhide::before{content:'\2026'} .message-options{display:none} diff --git a/templates/edit_profile.html b/templates/edit_profile.html new file mode 100644 index 0000000..7475d4f --- /dev/null +++ b/templates/edit_profile.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block body %} +

Edit Profile

+ +
+
+
Username:
+
+
+
+
+{% endblock %} diff --git a/templates/includes/infobox_profile.html b/templates/includes/infobox_profile.html new file mode 100644 index 0000000..730db0c --- /dev/null +++ b/templates/includes/infobox_profile.html @@ -0,0 +1,30 @@ +{% set profile = user.profile %} +
+

{{ profile.full_name }}

+

{{ profile.biography|enrich }}

+ {% if profile.location %} +

Location: {{ profile.location }}

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

Year: {{ profile.year }}

+ {% endif %} + {% if profile.website %} +

Website: {{ profile.website|urlize }}

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

Instagram: {{ profile.instagram }}

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

Facebook: {{ profile.facebook }}

+ {% endif %} +

+ {{ user.messages|count }} messages + - + {{ user.followers()|count }} followers + - + {{ user.following()|count }} following +

+ {% if user == current_user %} +

Edit profile

+ {% endif %} +
diff --git a/templates/join.html b/templates/join.html index 29411a6..ad980de 100644 --- a/templates/join.html +++ b/templates/join.html @@ -1,16 +1,34 @@ {% extends "base.html" %} {% block body %}

Join {{ site_name }}

-
+
Username:
-
+
+
Full name:
+
+ If not given, defaults to your username. + +
Password:
Email:
-
Birthday: -
+
Birthday:
+
+ Your birthday won't be shown to anyone. + +
+ {% if not current_user.is_anonymous %} +
+ + +
+ {% endif %} +
+ + +
diff --git a/templates/user_detail.html b/templates/user_detail.html index 7f93604..9316432 100644 --- a/templates/user_detail.html +++ b/templates/user_detail.html @@ -1,13 +1,7 @@ {% extends "base.html" %} {% block body %} + {% include "includes/infobox_profile.html" %}

Messages from {{ user.username }}

-

- {{ user.messages|count }} messages - - - {{ user.followers()|count }} followers - - - {{ user.following()|count }} following -

{% if not current_user.is_anonymous %} {% if user.username != current_user.username %} {% if current_user|is_following(user) %} @@ -21,8 +15,7 @@ {% endif %}

Mention this user in a message

{% else %} - - Create a status + Create a message {% endif %} {% endif %}
- +
diff --git a/src/coriplus/website.py b/src/coriplus/website.py index 14b944c..ab9ab53 100644 --- a/src/coriplus/website.py +++ b/src/coriplus/website.py @@ -239,12 +239,15 @@ def edit(id): @bp.route('/delete/', methods=['GET', 'POST']) def confirm_delete(id): - user = get_current_user() - message = get_object_or_404(Message, Message.id == id) + user: User = current_user + message: Message = get_object_or_404(Message, Message.id == id) if message.user != user: abort(404) if request.method == 'POST': - abort(501, 'CSRF-Token missing.') + if message.user == user: + message.delete_instance() + flash('Your message has been deleted forever') + return redirect(request.args.get('next', '/')) return render_template('confirm_delete.html', message=message) # Workaround for problems related to invalid data. diff --git a/src/migrations/002_move_columns_from_userprofile.py b/src/migrations/002_move_columns_from_userprofile.py index 0b2d004..d8637f7 100644 --- a/src/migrations/002_move_columns_from_userprofile.py +++ b/src/migrations/002_move_columns_from_userprofile.py @@ -70,6 +70,11 @@ def rollback(migrator: Migrator, database: pw.Database, *, fake=False): facebook=pw.TextField(null=True), telegram=pw.TextField(null=True)) + migrator.sql(""" + UPDATE "userprofile" SET biography = (SELECT p.biography FROM user p WHERE p.user_id = id LIMIT 1), + website = (SELECT p.website FROM user p WHERE p.user_id = id LIMIT 1); + """) + migrator.remove_fields('user', 'biography', 'website') migrator.change_fields('user', username=pw.CharField(max_length=255, unique=True)) From b29fa7522613f09ebc2762129dd4ca25f546ee7d Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Fri, 19 Dec 2025 11:10:32 +0100 Subject: [PATCH 41/41] add csrf_token to JavaScript actions --- CHANGELOG.md | 2 ++ src/coriplus/__init__.py | 2 +- src/coriplus/ajax.py | 5 +++-- src/coriplus/static/lib.js | 12 ++++++++++-- src/coriplus/templates/base.html | 1 + 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bf0e72..cfb7a8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,13 @@ ## 0.10.0 + Codebase refactor (with breaking changes!) + Dropped support for Python<=3.9 ++ Switched database to PostgreSQL + Move ALL config to .env (config.py is NO MORE supported) + Config SITE_NAME replaced with APP_NAME + Add CSRF token and flask_WTF + Schema changes: biography and website moved to `User`; `UserProfile` table deprecated (and useless fields removed) + Posts can now be permanently deleted ++ Miscellaneous style changes ## 0.9.0 diff --git a/src/coriplus/__init__.py b/src/coriplus/__init__.py index 305ea7b..09cecf6 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-dev47' +__version__ = '0.10.0-dev50' # we want to support Python 3.10+ only. # Python 2 has too many caveats. diff --git a/src/coriplus/ajax.py b/src/coriplus/ajax.py index d2c0be3..cad9c74 100644 --- a/src/coriplus/ajax.py +++ b/src/coriplus/ajax.py @@ -5,8 +5,9 @@ Warning: this is not the public API. ''' from flask import Blueprint, jsonify +from flask_login import current_user from .models import User, Message, MessageUpvote -from .utils import locations, get_current_user, is_username +from .utils import locations, is_username import datetime bp = Blueprint('ajax', __name__, url_prefix='/ajax') @@ -39,7 +40,7 @@ def location_search(name): @bp.route('/score//toggle', methods=['POST']) def score_toggle(id): - user = get_current_user() + user = current_user message = Message[id] upvoted_by_self = (MessageUpvote .select() diff --git a/src/coriplus/static/lib.js b/src/coriplus/static/lib.js index cc78bea..11a2316 100644 --- a/src/coriplus/static/lib.js +++ b/src/coriplus/static/lib.js @@ -99,12 +99,20 @@ function showHideMessageOptions(id){ } } +function getCsrfToken () { + var csrf_token = document.querySelector('meta[name="csrf_token"]'); + return csrf_token?.getAttribute('content'); +} + function toggleUpvote(id){ var msgElem = document.getElementById(id); - var upvoteLink = msgElem.getElementsByClassName('message-upvote')[0]; + //var upvoteLink = msgElem.getElementsByClassName('message-upvote')[0]; var scoreCounter = msgElem.getElementsByClassName('message-score')[0]; + var body = "csrf_token=" + getCsrfToken(); var xhr = new XMLHttpRequest(); xhr.open("POST", "/ajax/score/" + id + "/toggle", true); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + // TODO add csrf token somewhere xhr.onreadystatechange = function(){ if(xhr.readyState == XMLHttpRequest.DONE){ if(xhr.status == 200){ @@ -114,5 +122,5 @@ function toggleUpvote(id){ } } }; - xhr.send(); + xhr.send(body); } diff --git a/src/coriplus/templates/base.html b/src/coriplus/templates/base.html index 415711b..60667ea 100644 --- a/src/coriplus/templates/base.html +++ b/src/coriplus/templates/base.html @@ -6,6 +6,7 @@ +