feat: Enhance app config and user feedback

Introduced changes to `app.py` and templates to enhance application configuration options, improve user feedback mechanisms, and bolster security practices. Key updates include:

- Expanded Flask's configuration based on `settings.ini`, enabling `debug` mode and applying `ProxyFix` middleware conditionally to support reverse proxy setups.
- Extended the functionality to include dynamic footer links, sourced from the configuration file, across all relevant templates. This contributes to a more dynamic and maintainable web interface.
- Adjusted the rate limiting functionality from a 1-hour to a 1-day window, offering a more lenient and user-friendly request limitation system.
- Implemented an error handling flow for user creation in Planka, providing clearer feedback when password requirements are not met, thus enhancing the user signup experience.
- Added a new cron route for cleaning up stale requests from the database, aligning data retention practices with privacy concerns.

These changes aim to provide a more configurable, user-friendly, and secure application, addressing feedback and evolving requirements.
This commit is contained in:
Kumi 2024-04-26 09:45:43 +02:00
parent 79e83aa0a7
commit 89825cf85c
Signed by: kumi
GPG key ID: ECBCC9082395383F
4 changed files with 105 additions and 12 deletions

73
app.py
View file

@ -1,11 +1,13 @@
from flask import Flask, request, redirect, url_for, render_template from flask import Flask, request, redirect, url_for, render_template, jsonify
from plankapy import Planka, User, InvalidToken from plankapy import Planka, User, InvalidToken
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, PasswordField from wtforms import StringField, SubmitField, PasswordField
from wtforms.validators import DataRequired, Email, ValidationError from wtforms.validators import DataRequired, Email, ValidationError
from werkzeug.middleware.proxy_fix import ProxyFix
from configparser import ConfigParser from configparser import ConfigParser
from random import SystemRandom from random import SystemRandom
from typing import List, Tuple
import sqlite3 import sqlite3
import smtplib import smtplib
@ -22,6 +24,12 @@ app.config["SECRET_KEY"] = "".join(
config = ConfigParser() config = ConfigParser()
config.read("settings.ini") config.read("settings.ini")
if config.getboolean("App", "debug", fallback=False):
app.debug = True
if config.getboolean("App", "proxyfix", fallback=False):
app.wsgi_app = ProxyFix(app.wsgi_app)
def initialize_database(): def initialize_database():
conn = sqlite3.connect("db.sqlite3") conn = sqlite3.connect("db.sqlite3")
@ -49,7 +57,7 @@ def rate_limit(request):
""" """
SELECT COUNT(*) SELECT COUNT(*)
FROM requests FROM requests
WHERE ip = ? AND created_at > datetime('now', '-1 hour') WHERE ip = ? AND created_at > datetime('now', '-1 day')
""", """,
(request.remote_addr,), (request.remote_addr,),
) )
@ -76,6 +84,16 @@ def get_mailserver():
return mailserver return mailserver
def get_footer_links() -> List[Tuple[str, str]]:
links = []
if "Footer" in config.sections():
for key in config["Footer"]:
links.append((key.capitalize(), config["Footer"][key]))
return links
def send_email(email, token): def send_email(email, token):
mailserver = get_mailserver() mailserver = get_mailserver()
sender = config.get("SMTP", "from", fallback=config["SMTP"]["username"]) sender = config.get("SMTP", "from", fallback=config["SMTP"]["username"])
@ -126,6 +144,7 @@ def process_request(request):
app=config["App"]["name"], app=config["App"]["name"],
title="Already Requested", title="Already Requested",
subtitle="You have already requested access with this email address.", subtitle="You have already requested access with this email address.",
footer_links=get_footer_links(),
) )
token = str(uuid.uuid4()) token = str(uuid.uuid4())
@ -154,7 +173,13 @@ class EmailForm(FlaskForm):
@app.route("/", methods=["GET", "POST"]) @app.route("/", methods=["GET", "POST"])
def start_request(): def start_request():
if rate_limit(request): if rate_limit(request):
return render_template("rate_limit.html") return render_template(
"rate_limit.html",
app=config["App"]["name"],
title="Rate Limited",
subtitle="You have reached the rate limit for requests. Please try again later.",
footer_links=get_footer_links(),
)
form = EmailForm() form = EmailForm()
@ -167,6 +192,7 @@ def start_request():
title="Request Access", title="Request Access",
subtitle="Please enter your email address to request access.", subtitle="Please enter your email address to request access.",
form=form, form=form,
footer_links=get_footer_links(),
) )
@ -177,6 +203,7 @@ def post_request():
app=config["App"]["name"], app=config["App"]["name"],
title="Request Received", title="Request Received",
subtitle="Your request has been received. Please check your email for further instructions.", subtitle="Your request has been received. Please check your email for further instructions.",
footer_links=get_footer_links(),
) )
@ -187,8 +214,6 @@ class SignupForm(FlaskForm):
password = PasswordField("Password", validators=[DataRequired()]) password = PasswordField("Password", validators=[DataRequired()])
submit = SubmitField("Submit") submit = SubmitField("Submit")
email.render_kw = {"readonly": True}
def validate_username(self, field): def validate_username(self, field):
planka = Planka( planka = Planka(
url=config["Planka"]["url"], url=config["Planka"]["url"],
@ -231,6 +256,7 @@ def confirm_request(token):
app=config["App"]["name"], app=config["App"]["name"],
title="Invalid Token", title="Invalid Token",
subtitle="The token you provided is invalid.", subtitle="The token you provided is invalid.",
footer_links=get_footer_links(),
) )
email = row[0] email = row[0]
@ -252,7 +278,22 @@ def confirm_request(token):
email=email, email=email,
) )
users.create(new_user) try:
users.create(new_user)
except InvalidToken:
form.password.errors.append(
"Your password did not meet Planka's requirements. Please try again."
)
return render_template(
"signup.html",
app=config["App"]["name"],
title="Complete Signup",
subtitle="Please confirm your email address by filling out the form below.",
email=email,
form=form,
footer_links=get_footer_links(),
)
cursor.execute( cursor.execute(
""" """
@ -274,6 +315,7 @@ def confirm_request(token):
subtitle="Please confirm your email address by filling out the form below.", subtitle="Please confirm your email address by filling out the form below.",
email=email, email=email,
form=form, form=form,
footer_links=get_footer_links(),
) )
@ -285,7 +327,26 @@ def post_signup():
title="Signup Complete", title="Signup Complete",
subtitle="Your account has been created. You may now log in.", subtitle="Your account has been created. You may now log in.",
planka=config["Planka"]["url"], planka=config["Planka"]["url"],
footer_links=get_footer_links(),
) )
@app.route("/cron")
def cron():
conn = sqlite3.connect("db.sqlite3")
cursor = conn.cursor()
cursor.execute(
"""
DELETE FROM requests
WHERE created_at < datetime('now', '-2 day')
"""
)
conn.commit()
conn.close()
return jsonify({"status": "ok"})
initialize_database() initialize_database()

29
settings.dist.ini Normal file
View file

@ -0,0 +1,29 @@
[App]
# Name of the app
Name = Private.coffee Planka
# Hostname of the app - the app always assumes that HTTPS is used
Host = register.planka.private.coffee
# Set to 1 if you are using a reverse proxy in front of the app
ProxyFix = 1
[SMTP]
# SMTP server settings
Host = mail.local
Port = 587
Username = planka@mail.local
Password = verysecurepassword
SSL = 1 # Set to 0 if you are not using SSL/TLS
STARTTLS = 1 # Set to 0 if you are not using STARTTLS
[Planka]
# URL and credentials for the Planka instance
URL = https://planka.local
Username = admin@mail.local
Password = extremelysecurepassword
[Footer]
Website = https://private.coffee
Legal = https://private.coffee/legal.html
Privacy = https://private.coffee/privacy.html

View file

@ -4,7 +4,7 @@
<meta charset="utf8" /> <meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ app }} - {{ title }}</title> <title>{{ app }} - {{ title }}</title>
<link rel="stylesheet" href="static/style.css" /> <link rel="stylesheet" href="/static/style.css" />
</head> </head>
<body> <body>
<header> <header>
@ -22,7 +22,9 @@
<footer> <footer>
<div class="container"> <div class="container">
<p> <p>
&copy; 2024 Private.coffee | &copy; 2024 Private.coffee {% for link in footer_links %} |
<a href="{{ link.1 }}">{{ link.0 }}</a>
{% endfor %} |
<a href="https://git.private.coffee/PrivateCoffee/planka-register" <a href="https://git.private.coffee/PrivateCoffee/planka-register"
>Git</a >Git</a
> >

View file

@ -7,11 +7,13 @@
<strong>Privacy notice:</strong> <strong>Privacy notice:</strong>
The provided data will be stored in our database to allow you to log in to The provided data will be stored in our database to allow you to log in to
our Planka instance. Your data will not be used for any other purpose. our Planka instance. Your data will not be used for any other purpose.
Note that your name will be visible to other users of the Planka instance. Note that your name, email address and any other data you add to your
profile will be visible to other users of the Planka instance.
</small> </small>
</p> </p>
<form method="post"> <form method="post">
{{ form.hidden_tag() }} {{ form.email.label }}: {{ form.email(size=20, disabled="disabled", value=email) }}<br /> {{ form.hidden_tag() }} {{ form.email.label }}: {{ form.email(size=20,
disabled="disabled", value=email) }}<br />
{% for error in form.email.errors %} {% for error in form.email.errors %}
<span style="color: red">[{{ error }}]</span> <span style="color: red">[{{ error }}]</span>
{% endfor %} {% endfor %}
@ -29,8 +31,7 @@
{{ form.password.label }}: {{ form.password(size=20) }}<br /> {{ form.password.label }}: {{ form.password(size=20) }}<br />
{% for error in form.password.errors %} {% for error in form.password.errors %}
<span style="color: red">[{{ error }}]</span> <span style="color: red">[{{ error }}]</span>
{% endfor %} {% endfor %} {{ form.submit() }}
{{ form.submit() }}
</form> </form>
</div> </div>
{% endblock %} {% endblock %}