From 5989f0d97eda29d3135826d3055d86ed9ef5469a Mon Sep 17 00:00:00 2001
From: Flare Starfall
Date: Wed, 18 Mar 2026 10:50:27 +0100
Subject: [PATCH] Login system (not fully complete yet)
---
messages.pot | 233 +++++++++----
requirements.txt | 4 +-
starfall/db/schema/users.py | 7 +-
starfall/web/__init__.py | 48 ++-
starfall/web/blueprints/admin.py | 13 -
.../web/blueprints/{ => public}/imprint.py | 6 +-
starfall/web/blueprints/{ => public}/main.py | 2 +-
starfall/web/blueprints/secure/main.py | 25 ++
starfall/web/controllers/base.py | 11 +-
.../web/controllers/{ => public}/imprint.py | 3 +-
starfall/web/controllers/secure/login.py | 38 +++
starfall/web/controllers/secure/register.py | 72 ++++
starfall/web/controllers/user.py | 15 +
translations/en/LC_MESSAGES/messages.mo | Bin 8213 -> 9657 bytes
translations/en/LC_MESSAGES/messages.po | 319 +++++++++++-------
web/templates/base.jinja | 13 +-
web/templates/components/content_empty.jinja | 9 +
web/templates/home.jinja | 12 -
web/templates/macros/form.jinja | 33 ++
web/templates/public/home.jinja | 15 +
web/templates/{ => public}/imprint.jinja | 19 +-
web/templates/secure/login.jinja | 28 ++
web/templates/secure/register.jinja | 27 ++
23 files changed, 694 insertions(+), 258 deletions(-)
delete mode 100644 starfall/web/blueprints/admin.py
rename starfall/web/blueprints/{ => public}/imprint.py (75%)
rename starfall/web/blueprints/{ => public}/main.py (96%)
create mode 100644 starfall/web/blueprints/secure/main.py
rename starfall/web/controllers/{ => public}/imprint.py (74%)
create mode 100644 starfall/web/controllers/secure/login.py
create mode 100644 starfall/web/controllers/secure/register.py
create mode 100644 starfall/web/controllers/user.py
create mode 100644 web/templates/components/content_empty.jinja
delete mode 100644 web/templates/home.jinja
create mode 100644 web/templates/macros/form.jinja
create mode 100644 web/templates/public/home.jinja
rename web/templates/{ => public}/imprint.jinja (77%)
create mode 100644 web/templates/secure/login.jinja
create mode 100644 web/templates/secure/register.jinja
diff --git a/messages.pot b/messages.pot
index 4542f34..0731007 100644
--- a/messages.pot
+++ b/messages.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Starfall 0.1.0-alpha.3\n"
"Report-Msgid-Bugs-To: flare@theflare.at\n"
-"POT-Creation-Date: 2026-03-10 10:46+0100\n"
+"POT-Creation-Date: 2026-03-18 10:46+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -17,6 +17,59 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
+#: starfall/web/controllers/secure/login.py:12
+#: starfall/web/controllers/secure/login.py:16
+msgid "page.login.form.username"
+msgstr ""
+
+#: starfall/web/controllers/secure/login.py:19
+msgid "page.login.error.username"
+msgstr ""
+
+#: starfall/web/controllers/secure/login.py:23
+#: starfall/web/controllers/secure/login.py:27
+msgid "page.login.form.password"
+msgstr ""
+
+#: starfall/web/controllers/secure/login.py:30
+msgid "page.login.error.password"
+msgstr ""
+
+#: starfall/web/controllers/secure/register.py:15
+#: starfall/web/controllers/secure/register.py:20
+msgid "page.register.form.username"
+msgstr ""
+
+#: starfall/web/controllers/secure/register.py:23
+msgid "page.register.error.username"
+msgstr ""
+
+#: starfall/web/controllers/secure/register.py:27
+#: starfall/web/controllers/secure/register.py:31
+msgid "page.register.form.password"
+msgstr ""
+
+#: starfall/web/controllers/secure/register.py:34
+msgid "page.register.error.password"
+msgstr ""
+
+#: starfall/web/controllers/secure/register.py:38
+#: starfall/web/controllers/secure/register.py:42
+msgid "page.register.form.email"
+msgstr ""
+
+#: starfall/web/controllers/secure/register.py:45
+msgid "page.register.error.email"
+msgstr ""
+
+#: starfall/web/controllers/secure/register.py:71
+msgid "page.register.status.email_already_exists"
+msgstr ""
+
+#: starfall/web/controllers/secure/register.py:72
+msgid "page.register.status.success"
+msgstr ""
+
#: web/templates/base.jinja:6
msgid "base.meta.title.empty"
msgstr ""
@@ -41,96 +94,36 @@ msgstr ""
msgid "base.label.toggle_nav"
msgstr ""
-#: web/templates/base.jinja:47
+#: web/templates/base.jinja:46
+msgid "base.label.login"
+msgstr ""
+
+#: web/templates/base.jinja:53
msgid "base.label.theme.btn"
msgstr ""
-#: web/templates/base.jinja:52
+#: web/templates/base.jinja:58
msgid "base.label.theme.light"
msgstr ""
-#: web/templates/base.jinja:55
+#: web/templates/base.jinja:61
msgid "base.label.theme.dark"
msgstr ""
-#: web/templates/base.jinja:58
+#: web/templates/base.jinja:64
msgid "base.label.theme.auto"
msgstr ""
-#: web/templates/base.jinja:76
-msgid "page.empty.title"
-msgstr ""
-
-#: web/templates/base.jinja:77
-msgid "page.empty.body"
-msgstr ""
-
-#: web/templates/base.jinja:89
+#: web/templates/base.jinja:90
msgid "base.label.copy"
msgstr ""
-#: web/templates/home.jinja:3
-msgid "page.home.title"
+#: web/templates/components/content_empty.jinja:3
+msgid "page.empty.title"
msgstr ""
-#: web/templates/home.jinja:8
-msgid "page.home.card.title"
-msgstr ""
-
-#: web/templates/home.jinja:9
-msgid "page.home.card.body"
-msgstr ""
-
-#: web/templates/imprint.jinja:3 web/templates/imprint.jinja:8
-msgid "page.imprint.title"
-msgstr ""
-
-#: web/templates/imprint.jinja:10
-msgid "page.imprint.card.title"
-msgstr ""
-
-#: web/templates/imprint.jinja:12
-msgid "page.imprint.card.body"
-msgstr ""
-
-#: web/templates/imprint.jinja:13
-msgid "page.imprint.card.lastmodified"
-msgstr ""
-
-#: web/templates/imprint.jinja:23
-msgid "page.imprint.legal"
-msgstr ""
-
-#: web/templates/imprint.jinja:31
-msgid "page.imprint.sec1.title"
-msgstr ""
-
-#: web/templates/imprint.jinja:32
-msgid "page.imprint.sec2.title"
-msgstr ""
-
-#: web/templates/imprint.jinja:33
-msgid "page.imprint.sec3.title"
-msgstr ""
-
-#: web/templates/imprint.jinja:34
-msgid "page.imprint.sec4.title"
-msgstr ""
-
-#: web/templates/imprint.jinja:35
-msgid "page.imprint.sec1.body"
-msgstr ""
-
-#: web/templates/imprint.jinja:36
-msgid "page.imprint.sec2.body"
-msgstr ""
-
-#: web/templates/imprint.jinja:37
-msgid "page.imprint.sec3.body"
-msgstr ""
-
-#: web/templates/imprint.jinja:38
-msgid "page.imprint.sec4.body"
+#: web/templates/components/content_empty.jinja:6
+msgid "page.empty.body"
msgstr ""
#: web/templates/errors/not_found.jinja:8
@@ -141,3 +134,95 @@ msgstr ""
msgid "error.404.body"
msgstr ""
+#: web/templates/public/home.jinja:3
+msgid "page.home.title"
+msgstr ""
+
+#: web/templates/public/home.jinja:8
+msgid "page.home.card.title"
+msgstr ""
+
+#: web/templates/public/home.jinja:11
+msgid "page.home.card.body"
+msgstr ""
+
+#: web/templates/public/imprint.jinja:3 web/templates/public/imprint.jinja:8
+msgid "page.imprint.title"
+msgstr ""
+
+#: web/templates/public/imprint.jinja:11
+msgid "page.imprint.card.title"
+msgstr ""
+
+#: web/templates/public/imprint.jinja:13
+msgid "page.imprint.card.body"
+msgstr ""
+
+#: web/templates/public/imprint.jinja:14
+msgid "page.imprint.card.lastmodified"
+msgstr ""
+
+#: web/templates/public/imprint.jinja:24
+msgid "page.imprint.legal"
+msgstr ""
+
+#: web/templates/public/imprint.jinja:32
+msgid "page.imprint.sec1.title"
+msgstr ""
+
+#: web/templates/public/imprint.jinja:33
+msgid "page.imprint.sec2.title"
+msgstr ""
+
+#: web/templates/public/imprint.jinja:34
+msgid "page.imprint.sec3.title"
+msgstr ""
+
+#: web/templates/public/imprint.jinja:35
+msgid "page.imprint.sec4.title"
+msgstr ""
+
+#: web/templates/public/imprint.jinja:36
+msgid "page.imprint.sec1.body"
+msgstr ""
+
+#: web/templates/public/imprint.jinja:37
+msgid "page.imprint.sec2.body"
+msgstr ""
+
+#: web/templates/public/imprint.jinja:38
+msgid "page.imprint.sec3.body"
+msgstr ""
+
+#: web/templates/public/imprint.jinja:39
+msgid "page.imprint.sec4.body"
+msgstr ""
+
+#: web/templates/secure/login.jinja:3 web/templates/secure/login.jinja:8
+msgid "page.login.title"
+msgstr ""
+
+#: web/templates/secure/login.jinja:20
+msgid "page.login.form.submit"
+msgstr ""
+
+#: web/templates/secure/login.jinja:24
+msgid "page.login.label.register"
+msgstr ""
+
+#: web/templates/secure/login.jinja:26
+msgid "page.login.label.lostpassword"
+msgstr ""
+
+#: web/templates/secure/register.jinja:5 web/templates/secure/register.jinja:10
+msgid "page.register.title"
+msgstr ""
+
+#: web/templates/secure/register.jinja:20
+msgid "page.register.form.submit"
+msgstr ""
+
+#: web/templates/secure/register.jinja:24
+msgid "page.register.label.login"
+msgstr ""
+
diff --git a/requirements.txt b/requirements.txt
index 1fcbbaa..7a9a535 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,8 +2,10 @@ discord-py-interactions
flask
flask_assets
flask-babel
+flask-login
flask-sqlalchemy
+flask-wtf
libsass
livereload
rich
-twitchio
\ No newline at end of file
+twitchio
diff --git a/starfall/db/schema/users.py b/starfall/db/schema/users.py
index b74578b..ba65f90 100644
--- a/starfall/db/schema/users.py
+++ b/starfall/db/schema/users.py
@@ -3,8 +3,11 @@ from sqlalchemy.orm import Mapped, mapped_column
from starfall.db import db
-class Users(db.Table):
- id: Mapped[int] = mapped_column(primary_key=True)
+class User(db.Model):
+ id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
email: Mapped[str] = mapped_column(unique=True)
username: Mapped[str] = mapped_column()
password: Mapped[str] = mapped_column()
+
+ def __init__(self, **kwargs) -> None:
+ super().__init__(**kwargs)
diff --git a/starfall/web/__init__.py b/starfall/web/__init__.py
index 846a3d0..81b7326 100644
--- a/starfall/web/__init__.py
+++ b/starfall/web/__init__.py
@@ -8,12 +8,14 @@ from typing import final
from flask import Blueprint, Flask, request
from flask_assets import Bundle, Environment
from flask_babel import Babel
+from flask_wtf import CSRFProtect
from livereload import Server
from starfall.config import Config
from starfall.db import db, load_schema
from starfall.types import SnapshotQueue
from starfall.web.blueprints.base import BaseBlueprint
+from starfall.web.controllers.user import login_manager
@final
@@ -26,6 +28,7 @@ class WebUI:
self.config: Config | None = None
self.queue: SnapshotQueue | None = None
self.server: Server | None = None
+ self.csrf: CSRFProtect | None = None
def select_locale(self):
# user = getattr(g, "user", None)
@@ -58,12 +61,21 @@ class WebUI:
TEMPLATES_AUTO_RELOAD=True,
)
self.app.jinja_env.auto_reload = True
+ logging.getLogger("web").debug("flask initialized")
+
+ db.init_app(self.app)
+ load_schema()
+
+ with self.app.app_context():
+ db.create_all()
+ logging.getLogger("web").debug("db initialized")
self.babel = Babel(
self.app,
locale_selector=self.select_locale,
timezone_selector=self.select_timezone,
)
+ logging.getLogger("web").debug("babel initialized")
self.assets = Environment(self.app)
self.assets.load_path = ["web/static/scss", "web/static/css"]
@@ -79,37 +91,43 @@ class WebUI:
depends=["**/*.scss"],
)
_ = self.assets.register("scss", scss)
+ logging.getLogger("web").debug("assets initialized")
- db.init_app(self.app)
- load_schema()
+ self.csrf = CSRFProtect(self.app)
+ logging.getLogger("web").debug("csrf connected")
- with self.app.app_context():
- db.create_all()
+ login_manager.init_app(self.app)
+ self.import_blueprints("secure", "secure")
+ self.import_blueprints("public", "public")
self.import_blueprints()
self.app.register_blueprint(
self.blueprint,
options={"queue": self.queue},
)
+ logging.getLogger("web").debug("blueprints initialized")
self.server = Server(self.app.wsgi_app)
- self.server.watch(
- filepath=os.path.join(str(self.app.template_folder), "**/*.jinja"),
- )
- self.server.watch(
- filepath=os.path.join(str(self.app.static_folder), "**/*.js"),
- )
- self.server.watch(
- filepath=os.path.join(str(self.app.static_folder), "**/*.scss"),
- )
+ self.server.watch(os.path.join(str(self.app.template_folder), "**/*.jinja"))
+ self.server.watch(os.path.join(str(self.app.static_folder), "**/*.js"))
+ self.server.watch(os.path.join(str(self.app.static_folder), "**/*.scss"))
+ logging.getLogger("web").debug("livereload initialized")
+
self.server.serve(
host=self.config.get("web.host"),
port=self.config.get("web.port"),
)
- def import_blueprints(self):
+ def import_blueprints(self, path_suffix: str = "", module_suffix: str = ""):
path = os.path.realpath(os.path.dirname(__file__) + os.sep + "blueprints")
- for _, module_name, _ in iter_modules([path], f"{__name__}.blueprints."):
+ if len(path_suffix):
+ path += os.sep + path_suffix
+
+ prefix = f"{__name__}.blueprints."
+ if len(module_suffix):
+ prefix += module_suffix + "."
+
+ for _, module_name, _ in iter_modules([path], prefix):
if module_name.endswith("base"):
continue
diff --git a/starfall/web/blueprints/admin.py b/starfall/web/blueprints/admin.py
deleted file mode 100644
index a05b8a4..0000000
--- a/starfall/web/blueprints/admin.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from flask import Blueprint, Flask, render_template
-from flask_assets import Environment
-
-from starfall.web.blueprints.base import BaseBlueprint
-
-
-class AdminBlueprint(BaseBlueprint):
- def __init__(self, blueprint: Blueprint, assets: Environment, app: Flask):
- super().__init__(blueprint, assets, app)
- blueprint.add_url_rule("/login", methods=["GET", "POST"], view_func=self.login)
-
- def login(self):
- return render_template("admin/login.jinja", bp=self)
diff --git a/starfall/web/blueprints/imprint.py b/starfall/web/blueprints/public/imprint.py
similarity index 75%
rename from starfall/web/blueprints/imprint.py
rename to starfall/web/blueprints/public/imprint.py
index 1cd69aa..941b77a 100644
--- a/starfall/web/blueprints/imprint.py
+++ b/starfall/web/blueprints/public/imprint.py
@@ -3,19 +3,19 @@ from flask_assets import Environment
from flask_babel import get_locale
from starfall.web.blueprints.base import BaseBlueprint
-from starfall.web.controllers.imprint import ImprintController
+from starfall.web.controllers.public.imprint import ImprintController
class ImprintBlueprint(BaseBlueprint):
def __init__(self, blueprint: Blueprint, assets: Environment, app: Flask) -> None:
super().__init__(blueprint, assets, app)
- blueprint.add_url_rule("/imprint", view_func=self.imprint)
+ blueprint.add_url_rule("/imprint/", view_func=self.imprint)
def imprint(self):
self._log_access()
ImprintController.apply(self)
return render_template(
- "imprint.jinja",
+ "public/imprint.jinja",
bp=self,
lang=get_locale(),
)
diff --git a/starfall/web/blueprints/main.py b/starfall/web/blueprints/public/main.py
similarity index 96%
rename from starfall/web/blueprints/main.py
rename to starfall/web/blueprints/public/main.py
index fef6009..a6c7bdc 100644
--- a/starfall/web/blueprints/main.py
+++ b/starfall/web/blueprints/public/main.py
@@ -16,7 +16,7 @@ class MainBlueprint(BaseBlueprint):
def index(self):
self._log_access()
return render_template(
- "home.jinja",
+ "public/home.jinja",
bp=self,
lang=get_locale(),
)
diff --git a/starfall/web/blueprints/secure/main.py b/starfall/web/blueprints/secure/main.py
new file mode 100644
index 0000000..b673e5b
--- /dev/null
+++ b/starfall/web/blueprints/secure/main.py
@@ -0,0 +1,25 @@
+from flask import Blueprint, Flask, redirect, render_template, url_for
+from flask_assets import Environment
+
+from starfall.web.blueprints.base import BaseBlueprint
+from starfall.web.controllers.secure.login import LoginController
+from starfall.web.controllers.secure.register import RegisterController
+
+
+class AdminBlueprint(BaseBlueprint):
+ def __init__(self, blueprint: Blueprint, assets: Environment, app: Flask):
+ super().__init__(blueprint, assets, app)
+ blueprint.add_url_rule("/secure/", view_func=self.secure)
+ blueprint.add_url_rule("/secure/login/", methods=["GET", "POST"], view_func=self.login)
+ blueprint.add_url_rule("/secure/register/", methods=["GET", "POST"], view_func=self.register)
+
+ def secure(self):
+ return redirect(url_for("starfall.login"))
+
+ def login(self):
+ LoginController.apply(self)
+ return render_template("secure/login.jinja", bp=self)
+
+ def register(self):
+ RegisterController.apply(self)
+ return render_template("secure/register.jinja", bp=self)
diff --git a/starfall/web/controllers/base.py b/starfall/web/controllers/base.py
index 50acd5e..00510c3 100644
--- a/starfall/web/controllers/base.py
+++ b/starfall/web/controllers/base.py
@@ -1,2 +1,11 @@
+from werkzeug.security import generate_password_hash, check_password_hash
+
+
class BaseController:
- pass
+ @classmethod
+ def encrypt_password(cls, password: str):
+ return generate_password_hash(password)
+
+ @classmethod
+ def validate_password(cls, pwhash: str, password: str):
+ return check_password_hash(pwhash, password)
\ No newline at end of file
diff --git a/starfall/web/controllers/imprint.py b/starfall/web/controllers/public/imprint.py
similarity index 74%
rename from starfall/web/controllers/imprint.py
rename to starfall/web/controllers/public/imprint.py
index af95920..a82ed77 100644
--- a/starfall/web/controllers/imprint.py
+++ b/starfall/web/controllers/public/imprint.py
@@ -1,9 +1,10 @@
from datetime import datetime
from starfall.web.blueprints.base import BaseBlueprint
+from starfall.web.controllers.base import BaseController
-class ImprintController:
+class ImprintController(BaseController):
@classmethod
def apply(cls, bp: BaseBlueprint):
bp.data["lastmod"] = cls.last_modified()
diff --git a/starfall/web/controllers/secure/login.py b/starfall/web/controllers/secure/login.py
new file mode 100644
index 0000000..a5fef83
--- /dev/null
+++ b/starfall/web/controllers/secure/login.py
@@ -0,0 +1,38 @@
+from flask_babel import _, lazy_gettext
+from flask_wtf import FlaskForm
+from wtforms import PasswordField, StringField
+from wtforms.validators import DataRequired
+
+from starfall.web.blueprints.base import BaseBlueprint
+from starfall.web.controllers.base import BaseController
+
+
+class LoginForm(FlaskForm):
+ username: StringField = StringField(
+ label=lazy_gettext("page.login.form.username"), # pyright: ignore[reportArgumentType]
+ render_kw={
+ "autocomplete": "username",
+ "class": "form-control",
+ "placeholder": lazy_gettext("page.login.form.username"),
+ },
+ validators=[
+ DataRequired(message=lazy_gettext("page.login.error.username")), # pyright: ignore[reportArgumentType]
+ ],
+ )
+ password: PasswordField = PasswordField(
+ label=lazy_gettext("page.login.form.password"), # pyright: ignore[reportArgumentType]
+ render_kw={
+ "autocomplete": "password",
+ "class": "form-control",
+ "placeholder": lazy_gettext("page.login.form.password"),
+ },
+ validators=[
+ DataRequired(message=lazy_gettext("page.login.error.password")), # pyright: ignore[reportArgumentType]
+ ],
+ )
+
+
+class LoginController(BaseController):
+ @classmethod
+ def apply(cls, bp: BaseBlueprint):
+ bp.data["form"] = LoginForm()
diff --git a/starfall/web/controllers/secure/register.py b/starfall/web/controllers/secure/register.py
new file mode 100644
index 0000000..e2d6e94
--- /dev/null
+++ b/starfall/web/controllers/secure/register.py
@@ -0,0 +1,72 @@
+from flask import request
+from flask_babel import LazyString, lazy_gettext
+from flask_wtf import FlaskForm
+from wtforms import EmailField, PasswordField, StringField
+from wtforms.validators import DataRequired
+
+from starfall.db import db
+from starfall.db.schema.users import User
+from starfall.web.blueprints.base import BaseBlueprint
+from starfall.web.controllers.base import BaseController
+
+
+class RegisterForm(FlaskForm):
+ username: StringField = StringField(
+ label=lazy_gettext("page.register.form.username"), # pyright: ignore[reportArgumentType]
+ render_kw={
+ "autocomplete": "username",
+ "class": "form-control",
+ "aria-describedby": "username-addon",
+ "placeholder": lazy_gettext("page.register.form.username"),
+ },
+ validators=[
+ DataRequired(message=lazy_gettext("page.register.error.username")), # pyright: ignore[reportArgumentType]
+ ],
+ )
+ password: PasswordField = PasswordField(
+ label=lazy_gettext("page.register.form.password"), # pyright: ignore[reportArgumentType]
+ render_kw={
+ "autocomplete": "password",
+ "class": "form-control",
+ "placeholder": lazy_gettext("page.register.form.password"),
+ },
+ validators=[
+ DataRequired(message=lazy_gettext("page.register.error.password")), # pyright: ignore[reportArgumentType]
+ ],
+ )
+ email: EmailField = EmailField(
+ label=lazy_gettext("page.register.form.email"), # pyright: ignore[reportArgumentType]
+ render_kw={
+ "autocomplete": "email",
+ "class": "form-control",
+ "placeholder": lazy_gettext("page.register.form.email"),
+ },
+ validators=[
+ DataRequired(message=lazy_gettext("page.register.error.email")), # pyright: ignore[reportArgumentType]
+ ],
+ )
+
+
+class RegisterController(BaseController):
+ @classmethod
+ def apply(cls, bp: BaseBlueprint):
+ if "POST" == request.method:
+ bp.data["status_class"], bp.data["status"] = cls.handle_form()
+ bp.data["form"] = RegisterForm()
+
+ @classmethod
+ def handle_form(cls) -> tuple[str | None, LazyString | None]:
+ form = RegisterForm()
+ if not form.validate_on_submit():
+ return None, None
+ user = User(
+ username=str(form.username.data),
+ password=cls.encrypt_password(str(form.password.data)),
+ email=str(form.email.data),
+ )
+ try:
+ db.session.add(user)
+ db.session.commit()
+ except Exception:
+ return "error", lazy_gettext("page.register.status.email_already_exists")
+ return "success", lazy_gettext("page.register.status.success")
diff --git a/starfall/web/controllers/user.py b/starfall/web/controllers/user.py
new file mode 100644
index 0000000..f4ddd6d
--- /dev/null
+++ b/starfall/web/controllers/user.py
@@ -0,0 +1,15 @@
+from starfall.web.controllers.base import BaseController
+from flask_login import LoginManager
+
+login_manager = LoginManager()
+
+
+@login_manager.user_loader
+def user(user_id):
+ return UserController.get(user_id)
+
+
+class UserController(BaseController):
+ @classmethod
+ def get(cls, user_id):
+ return None
diff --git a/translations/en/LC_MESSAGES/messages.mo b/translations/en/LC_MESSAGES/messages.mo
index 069a805b92b6e81fce8ecf8cc494a4367ee9aca2..fc1a3840bc1f486a64a724714c2380cc332c6656 100644
GIT binary patch
delta 2116
zcma)*U1(fI6vwA)Hf_^bYpb>Cns%c7wCy%+D%wP>!Hjl0
zQg^`Za4kFpC4SV-yVifv`tPKEqSPfj@wHuW)%t%yNwj*oQa!NGatLm~KLMpc-TJS>
zUi^2VtiKH90N-2x7bprZTaj@S+(3Ob#Gev84&}oM>%U?B58zJbzl4(DIuu2guT<(S
z+Es$0%*8IH5JP;&vh&HCL4JL%0|}FPsy_be#-oVP+I&ayOMkbWO;%4Rcq+~OYC6g)@(sH
z4g7}%B{%>#lKnf{@Fd)c-@93$AHHuNRUDr)tWcUv=wc69j%xo^Zl&(hn+XNhA(UPo%pe>6I5JpZEhvjr1ryVaaPG$PE
znL?JZ{mL*N8#CeJV5Zt=(D;|S%R>L~iTl;}d&(*qMG{2B(GIhc#ruXVc(*{502US1P
z`LaXmXnsqw4~F&gj^`$ocDet@rdl?eRm@8*RLVD%9oU{pPrA`r9i1hEsXfkJ)V5zb
k>sQ%+kC?7s*S1mPu~*j5`mwG#)4J|YY96QNbuJY94?-cZ?*IS*
delta 662
zcmYk(Pbg(k9Ki8|%eYU5{2$(U*G9(c*15>y+RXHng@pxb($sX*Q_4fHm!kAaN){+L
zQL>QISZJoa5)0C7hGZdIl2U4Rik0v^?pe5X@8|x0=XcNVoOAa&m%gU2N{l7JWpRhq
zoyFtb70wo-i1;&>qK(z~4a@NdmSIxIGgv{qrsItEM0<^@{t-V4kro9xL4#^-BNkKO
zqH5TQ>VuzpJcX*kG}huOR^mRE;JF^Z)%hnKml;7_6xH{!w>T|CpH3uo!7QqY)^UT=
z$e>!l_q<@C3GE~@B<3&|7j?X%<25wNZ=gD@5lQZ@dE-8|zVHsb-udPdKCto=gb
zh#z{#R{mon@kJzP^b+yMjJlm)#(1`0WA@hf*s)~XOAU?;+wEQ|?pjTK7S%qQ!eWJM@Glnte&`+u)0%T
Oa=;rJ+W8mS`t%AwXjcUQ
diff --git a/translations/en/LC_MESSAGES/messages.po b/translations/en/LC_MESSAGES/messages.po
index e1bbee8..d48ac5b 100644
--- a/translations/en/LC_MESSAGES/messages.po
+++ b/translations/en/LC_MESSAGES/messages.po
@@ -7,16 +7,70 @@ msgid ""
msgstr ""
"Project-Id-Version: Starfall 0.1.0-alpha.3\n"
"Report-Msgid-Bugs-To: flare@theflare.at\n"
-"POT-Creation-Date: 2026-03-10 10:46+0100\n"
-"PO-Revision-Date: 2026-03-10 01:17+0100\n"
+"POT-Creation-Date: 2026-03-18 10:46+0100\n"
+"PO-Revision-Date: 2026-03-18 10:47+0100\n"
"Last-Translator: Flare Starfall \n"
-"Language: en\n"
"Language-Team: en \n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Generated-By: Babel 2.18.0\n"
+"X-Generator: Poedit 3.9\n"
+
+#: starfall/web/controllers/secure/login.py:12
+#: starfall/web/controllers/secure/login.py:16
+msgid "page.login.form.username"
+msgstr "Username / Email"
+
+#: starfall/web/controllers/secure/login.py:19
+msgid "page.login.error.username"
+msgstr "A username or email is required."
+
+#: starfall/web/controllers/secure/login.py:23
+#: starfall/web/controllers/secure/login.py:27
+msgid "page.login.form.password"
+msgstr "Password"
+
+#: starfall/web/controllers/secure/login.py:30
+msgid "page.login.error.password"
+msgstr "A password is required."
+
+#: starfall/web/controllers/secure/register.py:15
+#: starfall/web/controllers/secure/register.py:20
+msgid "page.register.form.username"
+msgstr "Username"
+
+#: starfall/web/controllers/secure/register.py:23
+msgid "page.register.error.username"
+msgstr "A username is required."
+
+#: starfall/web/controllers/secure/register.py:27
+#: starfall/web/controllers/secure/register.py:31
+msgid "page.register.form.password"
+msgstr "Password"
+
+#: starfall/web/controllers/secure/register.py:34
+msgid "page.register.error.password"
+msgstr "A password is required."
+
+#: starfall/web/controllers/secure/register.py:38
+#: starfall/web/controllers/secure/register.py:42
+msgid "page.register.form.email"
+msgstr "Email"
+
+#: starfall/web/controllers/secure/register.py:45
+msgid "page.register.error.email"
+msgstr "A valid email is required."
+
+#: starfall/web/controllers/secure/register.py:71
+msgid "page.register.status.email_already_exists"
+msgstr "A user with this email already exists."
+
+#: starfall/web/controllers/secure/register.py:72
+msgid "page.register.status.success"
+msgstr "Registration successful. You may now log in."
#: web/templates/base.jinja:6
msgid "base.meta.title.empty"
@@ -42,79 +96,92 @@ msgstr "Starfall - Logo"
msgid "base.label.toggle_nav"
msgstr "Open menu"
-#: web/templates/base.jinja:47
+#: web/templates/base.jinja:46
+msgid "base.label.login"
+msgstr "Login"
+
+#: web/templates/base.jinja:53
msgid "base.label.theme.btn"
msgstr "Theme"
-#: web/templates/base.jinja:52
+#: web/templates/base.jinja:58
msgid "base.label.theme.light"
msgstr "Light"
-#: web/templates/base.jinja:55
+#: web/templates/base.jinja:61
msgid "base.label.theme.dark"
msgstr "Dark"
-#: web/templates/base.jinja:58
+#: web/templates/base.jinja:64
msgid "base.label.theme.auto"
msgstr "Auto"
-#: web/templates/base.jinja:76
-msgid "page.empty.title"
-msgstr "This page has no content"
-
-#: web/templates/base.jinja:77
-msgid "page.empty.body"
-msgstr ""
-"This page has not been assigned any content.
This is unlikely to"
-" be intentional - please notify the site "
-"administrator about this problem.
"
-
-#: web/templates/base.jinja:89
+#: web/templates/base.jinja:90
msgid "base.label.copy"
msgstr "© Team Starfall"
-#: web/templates/home.jinja:3
+#: web/templates/components/content_empty.jinja:3
+msgid "page.empty.title"
+msgstr "This page has no content"
+
+#: web/templates/components/content_empty.jinja:6
+msgid "page.empty.body"
+msgstr ""
+"This page has not been assigned any content.
This is unlikely to be "
+"intentional - please notify the site "
+"administrator about this problem.
"
+
+#: web/templates/errors/not_found.jinja:8
+msgid "error.404.title"
+msgstr "404 Not Found"
+
+#: web/templates/errors/not_found.jinja:9
+msgid "error.404.body"
+msgstr ""
+"Whatever you were looking for cannot be found here.
You may want to start over.
"
+
+#: web/templates/public/home.jinja:3
msgid "page.home.title"
msgstr "Home of Team Starfall"
-#: web/templates/home.jinja:8
+#: web/templates/public/home.jinja:8
msgid "page.home.card.title"
msgstr "Shoot for the stars."
-#: web/templates/home.jinja:9
+#: web/templates/public/home.jinja:11
msgid "page.home.card.body"
msgstr ""
-"Home of Team Starfall, a small group of friends dedicated to messing "
-"with video games in ways the developers are unlikely to expect.
And"
-" hey - occasionally we get fun(ny) results out of it.
On the side, "
-"some of us get involved in all kinds of shenanigans, ranging from "
-"character creation in various kinds of games and software, over creating "
-"music, to web design and programming.
"
+"Home of Team Starfall, a small group of friends dedicated to messing with "
+"video games in ways the developers are unlikely to expect.
And hey - "
+"occasionally we get fun(ny) results out of it.
On the side, some of us "
+"get involved in all kinds of shenanigans, ranging from character creation in "
+"various kinds of games and software, over creating music, to web design and "
+"programming.
"
-#: web/templates/imprint.jinja:3 web/templates/imprint.jinja:8
+#: web/templates/public/imprint.jinja:3 web/templates/public/imprint.jinja:8
msgid "page.imprint.title"
msgstr "Imprint & Privacy"
-#: web/templates/imprint.jinja:10
+#: web/templates/public/imprint.jinja:11
msgid "page.imprint.card.title"
msgstr "In short"
-#: web/templates/imprint.jinja:12
+#: web/templates/public/imprint.jinja:13
msgid "page.imprint.card.body"
msgstr ""
-"The only thing this site processes is your site visit, stored for no "
-"longer than 7 days unless absolutely necessary. This data is only ever "
-"accessed by one person (this website's creator), and only in cases where "
-"reading the server logs is necessary (say, a website outage, or reading "
-"the logs while developing a new part of the website).
This "
-"disclaimer is not by itself legally binding. The full privacy statement "
-"is found below."
+"The only thing this site processes is your site visit, stored for no longer "
+"than 7 days unless absolutely necessary. This data is only ever accessed by "
+"one person (this website's creator), and only in cases where reading the "
+"server logs is necessary (say, a website outage, or reading the logs while "
+"developing a new part of the website).
This disclaimer is not by itself "
+"legally binding. The full privacy statement is found below."
-#: web/templates/imprint.jinja:13
+#: web/templates/public/imprint.jinja:14
msgid "page.imprint.card.lastmodified"
msgstr "Last modified:"
-#: web/templates/imprint.jinja:23
+#: web/templates/public/imprint.jinja:24
msgid "page.imprint.legal"
msgstr ""
"Based on the .
Edited to provide details of the data controller, and to "
"detail exactly what server data is stored."
-#: web/templates/imprint.jinja:31
+#: web/templates/public/imprint.jinja:32
msgid "page.imprint.sec1.title"
msgstr "Privacy Policy"
-#: web/templates/imprint.jinja:32
+#: web/templates/public/imprint.jinja:33
msgid "page.imprint.sec2.title"
msgstr "I. Information about us as controllers of your data"
-#: web/templates/imprint.jinja:33
+#: web/templates/public/imprint.jinja:34
msgid "page.imprint.sec3.title"
msgstr "II. The rights of users and data subjects"
-#: web/templates/imprint.jinja:34
+#: web/templates/public/imprint.jinja:35
msgid "page.imprint.sec4.title"
msgstr "III. Information about the data processing"
-#: web/templates/imprint.jinja:35
+#: web/templates/public/imprint.jinja:36
msgid "page.imprint.sec1.body"
msgstr ""
"Personal data (usually referred to just as „data“ below) will only be "
-"processed by us to the extent necessary and for the purpose of providing "
-"a functional and user-friendly website, including its contents, and the "
+"processed by us to the extent necessary and for the purpose of providing a "
+"functional and user-friendly website, including its contents, and the "
"services offered there.
\n"
"Per Art. 4 No. 1 of Regulation (EU) 2016/679, i.e. the General Data "
-"Protection Regulation (hereinafter referred to as the „GDPR“), "
-"„processing“ refers to any operation or set of operations such as "
-"collection, recording, organization, structuring, storage, adaptation, "
-"alteration, retrieval, consultation, use, disclosure by transmission, "
-"dissemination, or otherwise making available, alignment, or combination, "
-"restriction, erasure, or destruction performed on personal data, whether "
-"by automated means or not.
\n"
+"Protection Regulation (hereinafter referred to as the „GDPR“), „processing“ "
+"refers to any operation or set of operations such as collection, recording, "
+"organization, structuring, storage, adaptation, alteration, retrieval, "
+"consultation, use, disclosure by transmission, dissemination, or otherwise "
+"making available, alignment, or combination, restriction, erasure, or "
+"destruction performed on personal data, whether by automated means or not."
+"p>\n"
"The following privacy policy is intended to inform you in particular "
-"about the type, scope, purpose, duration, and legal basis for the "
-"processing of such data either under our own control or in conjunction "
-"with others. We also inform you below about the third-party components we"
-" use to optimize our website and improve the user experience which may "
-"result in said third parties also processing data they collect and "
-"control.
\n"
+"about the type, scope, purpose, duration, and legal basis for the processing "
+"of such data either under our own control or in conjunction with others. We "
+"also inform you below about the third-party components we use to optimize "
+"our website and improve the user experience which may result in said third "
+"parties also processing data they collect and control.
\n"
"Our privacy policy is structured as follows:
\n"
-"I. Information about us as controllers of your data
II. The rights "
-"of users and data subjects
III. Information about the data "
-"processing
"
+"I. Information about us as controllers of your data
II. The rights of "
+"users and data subjects
III. Information about the data processing
"
-#: web/templates/imprint.jinja:36
+#: web/templates/public/imprint.jinja:37
msgid "page.imprint.sec2.body"
msgstr ""
-"The party responsible for this website (the „controller“) for purposes"
-" of data protection law is:
Robert Bäs-Fischlmair a.k.a. Flare "
+"
The party responsible for this website (the „controller“) for purposes of "
+"data protection law is:
Robert Bäs-Fischlmair a.k.a. Flare "
"Starfall
Phone: +43 (0) 677 62890651
Email: flare@theflare.at
"
-#: web/templates/imprint.jinja:37
+#: web/templates/public/imprint.jinja:38
msgid "page.imprint.sec3.body"
msgstr ""
-"With regard to the data processing to be described in more detail "
-"below, users and data subjects have the right
\n"
+"With regard to the data processing to be described in more detail below, "
+"users and data subjects have the right
\n"
"\n"
"- to confirmation of whether data concerning them is being processed, "
-"information about the data being processed, further information about the"
-" nature of the data processing, and copies of the data (cf. also Art. 15 "
+"information about the data being processed, further information about the "
+"nature of the data processing, and copies of the data (cf. also Art. 15 "
+"GDPR);
\n"
+"- to correct or complete incorrect or incomplete data (cf. also Art. 16 "
"GDPR);
\n"
-"- to correct or complete incorrect or incomplete data (cf. also Art. 16"
-" GDPR);
\n"
"- to the immediate deletion of data concerning them (cf. also Art. 17 "
-"DSGVO), or, alternatively, if further processing is necessary as "
-"stipulated in Art. 17 Para. 3 GDPR, to restrict said processing per Art. "
-"18 GDPR;
- to receive copies of the data concerning them and/or "
-"provided by them and to have the same transmitted to other "
-"providers/controllers (cf. also Art. 20 GDPR);
- to file "
-"complaints with the supervisory authority if they believe that data "
-"concerning them is being processed by the controller in breach of data "
-"protection provisions (see also Art. 77 GDPR).
\n"
+"DSGVO), or, alternatively, if further processing is necessary as stipulated "
+"in Art. 17 Para. 3 GDPR, to restrict said processing per Art. 18 GDPR; "
+"- to receive copies of the data concerning them and/or provided by them "
+"and to have the same transmitted to other providers/controllers (cf. also "
+"Art. 20 GDPR);
- to file complaints with the supervisory authority if "
+"they believe that data concerning them is being processed by the controller "
+"in breach of data protection provisions (see also Art. 77 GDPR).
\n"
"
\n"
-"In addition, the controller is obliged to inform all recipients to "
-"whom it discloses data of any such corrections, deletions, or "
-"restrictions placed on processing the same per Art. 16, 17 Para. 1, 18 "
-"GDPR. However, this obligation does not apply if such notification is "
-"impossible or involves a disproportionate effort. Nevertheless, users "
-"have a right to information about these recipients.
\n"
-"Likewise, under Art. 21 GDPR, users and data subjects have the"
-" right to object to the controller's future processing of their data"
-" pursuant to Art. 6 Para. 1 lit. f) GDPR. In particular, an objection to "
-"data processing for the purpose of direct advertising is "
-"permissible.
"
+"In addition, the controller is obliged to inform all recipients to whom "
+"it discloses data of any such corrections, deletions, or restrictions placed "
+"on processing the same per Art. 16, 17 Para. 1, 18 GDPR. However, this "
+"obligation does not apply if such notification is impossible or involves a "
+"disproportionate effort. Nevertheless, users have a right to information "
+"about these recipients.
\n"
+"Likewise, under Art. 21 GDPR, users and data subjects have the "
+"right to object to the controller's future processing of their data "
+"pursuant to Art. 6 Para. 1 lit. f) GDPR. In particular, an objection to data "
+"processing for the purpose of direct advertising is permissible.
"
-#: web/templates/imprint.jinja:38
+#: web/templates/public/imprint.jinja:39
msgid "page.imprint.sec4.body"
msgstr ""
-"Your data processed when using our website will be deleted or blocked "
-"as soon as the purpose for its storage ceases to apply, provided the "
-"deletion of the same is not in breach of any statutory storage "
-"obligations or unless otherwise stipulated below.
\n"
+"Your data processed when using our website will be deleted or blocked as "
+"soon as the purpose for its storage ceases to apply, provided the deletion "
+"of the same is not in breach of any statutory storage obligations or unless "
+"otherwise stipulated below.
\n"
"Server data
\n"
-"For technical reasons, the following data sent by your internet "
-"browser to us or to our server provider will be collected, especially to "
-"ensure a secure and stable website: These server log files record the "
-"type and version of your browser, operating system, the webpages on our "
-"site visited, and the date and time of your visit. The IP address from "
-"which you visited our site is additionally recorded if an error "
-"occurs.
\n"
+"For technical reasons, the following data sent by your internet browser "
+"to us or to our server provider will be collected, especially to ensure a "
+"secure and stable website: These server log files record the type and "
+"version of your browser, operating system, the webpages on our site visited, "
+"and the date and time of your visit. The IP address from which you visited "
+"our site is additionally recorded if an error occurs.
\n"
"The data thus collected will be temporarily stored, but not in "
"association with any other of your data.
\n"
-"The basis for this storage is Art. 6 Para. 1 lit. f) GDPR. Our "
-"legitimate interest lies in the improvement, stability, functionality, "
-"and security of our website.
\n"
-"The data will be deleted within no more than seven days, unless "
-"continued storage is required for evidentiary purposes. In which case, "
-"all or part of the data will be excluded from deletion until the "
-"investigation of the relevant incident is finally resolved.
"
+"The basis for this storage is Art. 6 Para. 1 lit. f) GDPR. Our legitimate "
+"interest lies in the improvement, stability, functionality, and security of "
+"our website.
\n"
+"The data will be deleted within no more than seven days, unless continued "
+"storage is required for evidentiary purposes. In which case, all or part of "
+"the data will be excluded from deletion until the investigation of the "
+"relevant incident is finally resolved.
"
-#: web/templates/errors/not_found.jinja:8
-msgid "error.404.title"
-msgstr "404 Not Found"
+#: web/templates/secure/login.jinja:3 web/templates/secure/login.jinja:8
+msgid "page.login.title"
+msgstr "Login"
-#: web/templates/errors/not_found.jinja:9
-msgid "error.404.body"
-msgstr ""
-"Whatever you were looking for cannot be found here.
You may want to"
-" start over.
"
+#: web/templates/secure/login.jinja:20
+msgid "page.login.form.submit"
+msgstr "Login"
+#: web/templates/secure/login.jinja:24
+msgid "page.login.label.register"
+msgstr "Register"
+
+#: web/templates/secure/login.jinja:26
+msgid "page.login.label.lostpassword"
+msgstr "Forgot Password?"
+
+#: web/templates/secure/register.jinja:5 web/templates/secure/register.jinja:10
+msgid "page.register.title"
+msgstr "Register"
+
+#: web/templates/secure/register.jinja:20
+msgid "page.register.form.submit"
+msgstr "Register"
+
+#: web/templates/secure/register.jinja:24
+msgid "page.register.label.login"
+msgstr "Login"
diff --git a/web/templates/base.jinja b/web/templates/base.jinja
index d2a3eb9..f84113d 100644
--- a/web/templates/base.jinja
+++ b/web/templates/base.jinja
@@ -40,6 +40,12 @@
{%endblock%}