Login system (not fully complete yet)

This commit is contained in:
Flare Starfall 2026-03-18 10:50:27 +01:00
parent ec7a433864
commit 5989f0d97e
23 changed files with 694 additions and 258 deletions

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Starfall 0.1.0-alpha.3\n" "Project-Id-Version: Starfall 0.1.0-alpha.3\n"
"Report-Msgid-Bugs-To: flare@theflare.at\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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,6 +17,59 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\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 #: web/templates/base.jinja:6
msgid "base.meta.title.empty" msgid "base.meta.title.empty"
msgstr "" msgstr ""
@ -41,96 +94,36 @@ msgstr ""
msgid "base.label.toggle_nav" msgid "base.label.toggle_nav"
msgstr "" 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" msgid "base.label.theme.btn"
msgstr "" msgstr ""
#: web/templates/base.jinja:52 #: web/templates/base.jinja:58
msgid "base.label.theme.light" msgid "base.label.theme.light"
msgstr "" msgstr ""
#: web/templates/base.jinja:55 #: web/templates/base.jinja:61
msgid "base.label.theme.dark" msgid "base.label.theme.dark"
msgstr "" msgstr ""
#: web/templates/base.jinja:58 #: web/templates/base.jinja:64
msgid "base.label.theme.auto" msgid "base.label.theme.auto"
msgstr "" msgstr ""
#: web/templates/base.jinja:76 #: web/templates/base.jinja:90
msgid "page.empty.title"
msgstr ""
#: web/templates/base.jinja:77
msgid "page.empty.body"
msgstr ""
#: web/templates/base.jinja:89
msgid "base.label.copy" msgid "base.label.copy"
msgstr "" msgstr ""
#: web/templates/home.jinja:3 #: web/templates/components/content_empty.jinja:3
msgid "page.home.title" msgid "page.empty.title"
msgstr "" msgstr ""
#: web/templates/home.jinja:8 #: web/templates/components/content_empty.jinja:6
msgid "page.home.card.title" msgid "page.empty.body"
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"
msgstr "" msgstr ""
#: web/templates/errors/not_found.jinja:8 #: web/templates/errors/not_found.jinja:8
@ -141,3 +134,95 @@ msgstr ""
msgid "error.404.body" msgid "error.404.body"
msgstr "" 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 ""

View File

@ -2,7 +2,9 @@ discord-py-interactions
flask flask
flask_assets flask_assets
flask-babel flask-babel
flask-login
flask-sqlalchemy flask-sqlalchemy
flask-wtf
libsass libsass
livereload livereload
rich rich

View File

@ -3,8 +3,11 @@ from sqlalchemy.orm import Mapped, mapped_column
from starfall.db import db from starfall.db import db
class Users(db.Table): class User(db.Model):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
email: Mapped[str] = mapped_column(unique=True) email: Mapped[str] = mapped_column(unique=True)
username: Mapped[str] = mapped_column() username: Mapped[str] = mapped_column()
password: Mapped[str] = mapped_column() password: Mapped[str] = mapped_column()
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)

View File

@ -8,12 +8,14 @@ from typing import final
from flask import Blueprint, Flask, request from flask import Blueprint, Flask, request
from flask_assets import Bundle, Environment from flask_assets import Bundle, Environment
from flask_babel import Babel from flask_babel import Babel
from flask_wtf import CSRFProtect
from livereload import Server from livereload import Server
from starfall.config import Config from starfall.config import Config
from starfall.db import db, load_schema from starfall.db import db, load_schema
from starfall.types import SnapshotQueue from starfall.types import SnapshotQueue
from starfall.web.blueprints.base import BaseBlueprint from starfall.web.blueprints.base import BaseBlueprint
from starfall.web.controllers.user import login_manager
@final @final
@ -26,6 +28,7 @@ class WebUI:
self.config: Config | None = None self.config: Config | None = None
self.queue: SnapshotQueue | None = None self.queue: SnapshotQueue | None = None
self.server: Server | None = None self.server: Server | None = None
self.csrf: CSRFProtect | None = None
def select_locale(self): def select_locale(self):
# user = getattr(g, "user", None) # user = getattr(g, "user", None)
@ -58,12 +61,21 @@ class WebUI:
TEMPLATES_AUTO_RELOAD=True, TEMPLATES_AUTO_RELOAD=True,
) )
self.app.jinja_env.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.babel = Babel(
self.app, self.app,
locale_selector=self.select_locale, locale_selector=self.select_locale,
timezone_selector=self.select_timezone, timezone_selector=self.select_timezone,
) )
logging.getLogger("web").debug("babel initialized")
self.assets = Environment(self.app) self.assets = Environment(self.app)
self.assets.load_path = ["web/static/scss", "web/static/css"] self.assets.load_path = ["web/static/scss", "web/static/css"]
@ -79,37 +91,43 @@ class WebUI:
depends=["**/*.scss"], depends=["**/*.scss"],
) )
_ = self.assets.register("scss", scss) _ = self.assets.register("scss", scss)
logging.getLogger("web").debug("assets initialized")
db.init_app(self.app) self.csrf = CSRFProtect(self.app)
load_schema() logging.getLogger("web").debug("csrf connected")
with self.app.app_context(): login_manager.init_app(self.app)
db.create_all()
self.import_blueprints("secure", "secure")
self.import_blueprints("public", "public")
self.import_blueprints() self.import_blueprints()
self.app.register_blueprint( self.app.register_blueprint(
self.blueprint, self.blueprint,
options={"queue": self.queue}, options={"queue": self.queue},
) )
logging.getLogger("web").debug("blueprints initialized")
self.server = Server(self.app.wsgi_app) self.server = Server(self.app.wsgi_app)
self.server.watch( self.server.watch(os.path.join(str(self.app.template_folder), "**/*.jinja"))
filepath=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"))
self.server.watch( logging.getLogger("web").debug("livereload initialized")
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.serve( self.server.serve(
host=self.config.get("web.host"), host=self.config.get("web.host"),
port=self.config.get("web.port"), 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") 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"): if module_name.endswith("base"):
continue continue

View File

@ -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)

View File

@ -3,19 +3,19 @@ from flask_assets import Environment
from flask_babel import get_locale from flask_babel import get_locale
from starfall.web.blueprints.base import BaseBlueprint 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): class ImprintBlueprint(BaseBlueprint):
def __init__(self, blueprint: Blueprint, assets: Environment, app: Flask) -> None: def __init__(self, blueprint: Blueprint, assets: Environment, app: Flask) -> None:
super().__init__(blueprint, assets, app) 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): def imprint(self):
self._log_access() self._log_access()
ImprintController.apply(self) ImprintController.apply(self)
return render_template( return render_template(
"imprint.jinja", "public/imprint.jinja",
bp=self, bp=self,
lang=get_locale(), lang=get_locale(),
) )

View File

@ -16,7 +16,7 @@ class MainBlueprint(BaseBlueprint):
def index(self): def index(self):
self._log_access() self._log_access()
return render_template( return render_template(
"home.jinja", "public/home.jinja",
bp=self, bp=self,
lang=get_locale(), lang=get_locale(),
) )

View File

@ -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)

View File

@ -1,2 +1,11 @@
from werkzeug.security import generate_password_hash, check_password_hash
class BaseController: 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)

View File

@ -1,9 +1,10 @@
from datetime import datetime from datetime import datetime
from starfall.web.blueprints.base import BaseBlueprint from starfall.web.blueprints.base import BaseBlueprint
from starfall.web.controllers.base import BaseController
class ImprintController: class ImprintController(BaseController):
@classmethod @classmethod
def apply(cls, bp: BaseBlueprint): def apply(cls, bp: BaseBlueprint):
bp.data["lastmod"] = cls.last_modified() bp.data["lastmod"] = cls.last_modified()

View File

@ -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()

View File

@ -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")

View File

@ -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

View File

@ -7,16 +7,70 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Starfall 0.1.0-alpha.3\n" "Project-Id-Version: Starfall 0.1.0-alpha.3\n"
"Report-Msgid-Bugs-To: flare@theflare.at\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: 2026-03-10 01:17+0100\n" "PO-Revision-Date: 2026-03-18 10:47+0100\n"
"Last-Translator: Flare Starfall <flare@theflare.at>\n" "Last-Translator: Flare Starfall <flare@theflare.at>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\n" "Language-Team: en <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Language: en\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Generated-By: Babel 2.18.0\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 #: web/templates/base.jinja:6
msgid "base.meta.title.empty" msgid "base.meta.title.empty"
@ -42,79 +96,92 @@ msgstr "Starfall - Logo"
msgid "base.label.toggle_nav" msgid "base.label.toggle_nav"
msgstr "Open menu" 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" msgid "base.label.theme.btn"
msgstr "Theme" msgstr "Theme"
#: web/templates/base.jinja:52 #: web/templates/base.jinja:58
msgid "base.label.theme.light" msgid "base.label.theme.light"
msgstr "Light" msgstr "Light"
#: web/templates/base.jinja:55 #: web/templates/base.jinja:61
msgid "base.label.theme.dark" msgid "base.label.theme.dark"
msgstr "Dark" msgstr "Dark"
#: web/templates/base.jinja:58 #: web/templates/base.jinja:64
msgid "base.label.theme.auto" msgid "base.label.theme.auto"
msgstr "Auto" msgstr "Auto"
#: web/templates/base.jinja:76 #: web/templates/base.jinja:90
msgid "page.empty.title"
msgstr "This page has no content"
#: web/templates/base.jinja:77
msgid "page.empty.body"
msgstr ""
"<p>This page has not been assigned any content.</p><p>This is unlikely to"
" be intentional - <span class=\"text-warning\">please notify the site "
"administrator</span> about this problem.</p>"
#: web/templates/base.jinja:89
msgid "base.label.copy" msgid "base.label.copy"
msgstr "&copy; Team Starfall" msgstr "&copy; 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 ""
"<p>This page has not been assigned any content.</p><p>This is unlikely to be "
"intentional - <span class=\"text-warning\">please notify the site "
"administrator</span> about this problem.</p>"
#: 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 ""
"<p>Whatever you were looking for cannot be found here.<br>You may want to <a "
"href=\"/\">start over</a>.</p>"
#: web/templates/public/home.jinja:3
msgid "page.home.title" msgid "page.home.title"
msgstr "Home of Team Starfall" msgstr "Home of Team Starfall"
#: web/templates/home.jinja:8 #: web/templates/public/home.jinja:8
msgid "page.home.card.title" msgid "page.home.card.title"
msgstr "Shoot for the stars." msgstr "Shoot for the stars."
#: web/templates/home.jinja:9 #: web/templates/public/home.jinja:11
msgid "page.home.card.body" msgid "page.home.card.body"
msgstr "" msgstr ""
"<p>Home of Team Starfall, a small group of friends dedicated to messing " "<p>Home of Team Starfall, a small group of friends dedicated to messing with "
"with video games in ways the developers are unlikely to expect.</p><p>And" "video games in ways the developers are unlikely to expect.</p><p>And hey - "
" hey - occasionally we get fun(ny) results out of it.</p><p>On the side, " "occasionally we get fun(ny) results out of it.</p><p>On the side, some of us "
"some of us get involved in all kinds of shenanigans, ranging from " "get involved in all kinds of shenanigans, ranging from character creation in "
"character creation in various kinds of games and software, over creating " "various kinds of games and software, over creating music, to web design and "
"music, to web design and programming.</p>" "programming.</p>"
#: 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" msgid "page.imprint.title"
msgstr "Imprint &amp; Privacy" msgstr "Imprint &amp; Privacy"
#: web/templates/imprint.jinja:10 #: web/templates/public/imprint.jinja:11
msgid "page.imprint.card.title" msgid "page.imprint.card.title"
msgstr "In short" msgstr "In short"
#: web/templates/imprint.jinja:12 #: web/templates/public/imprint.jinja:13
msgid "page.imprint.card.body" msgid "page.imprint.card.body"
msgstr "" msgstr ""
"The only thing this site processes is your site visit, stored for no " "The only thing this site processes is your site visit, stored for no longer "
"longer than 7 days unless absolutely necessary. This data is only ever " "than 7 days unless absolutely necessary. This data is only ever accessed by "
"accessed by one person (this website's creator), and only in cases where " "one person (this website's creator), and only in cases where reading the "
"reading the server logs is necessary (say, a website outage, or reading " "server logs is necessary (say, a website outage, or reading the logs while "
"the logs while developing a new part of the website).<br />This " "developing a new part of the website).<br />This disclaimer is not by itself "
"disclaimer is not by itself legally binding. The full privacy statement " "legally binding. The full privacy statement is found below."
"is found below."
#: web/templates/imprint.jinja:13 #: web/templates/public/imprint.jinja:14
msgid "page.imprint.card.lastmodified" msgid "page.imprint.card.lastmodified"
msgstr "Last modified:" msgstr "Last modified:"
#: web/templates/imprint.jinja:23 #: web/templates/public/imprint.jinja:24
msgid "page.imprint.legal" msgid "page.imprint.legal"
msgstr "" msgstr ""
"Based on the <a href=\"https://www.generator-datenschutzerklärung.de\" " "Based on the <a href=\"https://www.generator-datenschutzerklärung.de\" "
@ -124,123 +191,135 @@ msgstr ""
"Partner</a>.<br>Edited to provide details of the data controller, and to " "Partner</a>.<br>Edited to provide details of the data controller, and to "
"detail exactly what server data is stored." "detail exactly what server data is stored."
#: web/templates/imprint.jinja:31 #: web/templates/public/imprint.jinja:32
msgid "page.imprint.sec1.title" msgid "page.imprint.sec1.title"
msgstr "Privacy Policy" msgstr "Privacy Policy"
#: web/templates/imprint.jinja:32 #: web/templates/public/imprint.jinja:33
msgid "page.imprint.sec2.title" msgid "page.imprint.sec2.title"
msgstr "I. Information about us as controllers of your data" 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" msgid "page.imprint.sec3.title"
msgstr "II. The rights of users and data subjects" 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" msgid "page.imprint.sec4.title"
msgstr "III. Information about the data processing" msgstr "III. Information about the data processing"
#: web/templates/imprint.jinja:35 #: web/templates/public/imprint.jinja:36
msgid "page.imprint.sec1.body" msgid "page.imprint.sec1.body"
msgstr "" msgstr ""
"<p>Personal data (usually referred to just as „data“ below) will only be " "<p>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 " "processed by us to the extent necessary and for the purpose of providing a "
"a functional and user-friendly website, including its contents, and the " "functional and user-friendly website, including its contents, and the "
"services offered there.</p>\n" "services offered there.</p>\n"
"<p>Per Art. 4 No. 1 of Regulation (EU) 2016/679, i.e. the General Data " "<p>Per Art. 4 No. 1 of Regulation (EU) 2016/679, i.e. the General Data "
"Protection Regulation (hereinafter referred to as the „GDPR“), " "Protection Regulation (hereinafter referred to as the „GDPR“), „processing“ "
"„processing“ refers to any operation or set of operations such as " "refers to any operation or set of operations such as collection, recording, "
"collection, recording, organization, structuring, storage, adaptation, " "organization, structuring, storage, adaptation, alteration, retrieval, "
"alteration, retrieval, consultation, use, disclosure by transmission, " "consultation, use, disclosure by transmission, dissemination, or otherwise "
"dissemination, or otherwise making available, alignment, or combination, " "making available, alignment, or combination, restriction, erasure, or "
"restriction, erasure, or destruction performed on personal data, whether " "destruction performed on personal data, whether by automated means or not.</"
"by automated means or not.</p>\n" "p>\n"
"<p>The following privacy policy is intended to inform you in particular " "<p>The following privacy policy is intended to inform you in particular "
"about the type, scope, purpose, duration, and legal basis for the " "about the type, scope, purpose, duration, and legal basis for the processing "
"processing of such data either under our own control or in conjunction " "of such data either under our own control or in conjunction with others. We "
"with others. We also inform you below about the third-party components we" "also inform you below about the third-party components we use to optimize "
" use to optimize our website and improve the user experience which may " "our website and improve the user experience which may result in said third "
"result in said third parties also processing data they collect and " "parties also processing data they collect and control.</p>\n"
"control.</p>\n"
"<p>Our privacy policy is structured as follows:</p>\n" "<p>Our privacy policy is structured as follows:</p>\n"
"<p>I. Information about us as controllers of your data<br>II. The rights " "<p>I. Information about us as controllers of your data<br>II. The rights of "
"of users and data subjects<br>III. Information about the data " "users and data subjects<br>III. Information about the data processing</p>"
"processing</p>"
#: web/templates/imprint.jinja:36 #: web/templates/public/imprint.jinja:37
msgid "page.imprint.sec2.body" msgid "page.imprint.sec2.body"
msgstr "" msgstr ""
"<p>The party responsible for this website (the „controller“) for purposes" "<p>The party responsible for this website (the „controller“) for purposes of "
" of data protection law is:<br>Robert Bäs-Fischlmair a.k.a. Flare " "data protection law is:<br>Robert Bäs-Fischlmair a.k.a. Flare "
"Starfall<br>Phone: +43 (0) 677 62890651<br>Email: <a " "Starfall<br>Phone: +43 (0) 677 62890651<br>Email: <a "
"href=\"mailto:flare@theflare.at\">flare@theflare.at</a></p>" "href=\"mailto:flare@theflare.at\">flare@theflare.at</a></p>"
#: web/templates/imprint.jinja:37 #: web/templates/public/imprint.jinja:38
msgid "page.imprint.sec3.body" msgid "page.imprint.sec3.body"
msgstr "" msgstr ""
"<p>With regard to the data processing to be described in more detail " "<p>With regard to the data processing to be described in more detail below, "
"below, users and data subjects have the right</p>\n" "users and data subjects have the right</p>\n"
"<ul>\n" "<ul>\n"
"<li>to confirmation of whether data concerning them is being processed, " "<li>to confirmation of whether data concerning them is being processed, "
"information about the data being processed, further information about the" "information about the data being processed, further information about the "
" nature of the data processing, and copies of the data (cf. also Art. 15 " "nature of the data processing, and copies of the data (cf. also Art. 15 "
"GDPR);</li>\n"
"<li>to correct or complete incorrect or incomplete data (cf. also Art. 16 "
"GDPR);</li>\n" "GDPR);</li>\n"
"<li>to correct or complete incorrect or incomplete data (cf. also Art. 16"
" GDPR);</li>\n"
"<li>to the immediate deletion of data concerning them (cf. also Art. 17 " "<li>to the immediate deletion of data concerning them (cf. also Art. 17 "
"DSGVO), or, alternatively, if further processing is necessary as " "DSGVO), or, alternatively, if further processing is necessary as stipulated "
"stipulated in Art. 17 Para. 3 GDPR, to restrict said processing per Art. " "in Art. 17 Para. 3 GDPR, to restrict said processing per Art. 18 GDPR; </li> "
"18 GDPR; </li> <li>to receive copies of the data concerning them and/or " "<li>to receive copies of the data concerning them and/or provided by them "
"provided by them and to have the same transmitted to other " "and to have the same transmitted to other providers/controllers (cf. also "
"providers/controllers (cf. also Art. 20 GDPR);</li> <li>to file " "Art. 20 GDPR);</li> <li>to file complaints with the supervisory authority if "
"complaints with the supervisory authority if they believe that data " "they believe that data concerning them is being processed by the controller "
"concerning them is being processed by the controller in breach of data " "in breach of data protection provisions (see also Art. 77 GDPR).</li>\n"
"protection provisions (see also Art. 77 GDPR).</li>\n"
"</ul>\n" "</ul>\n"
"<p>In addition, the controller is obliged to inform all recipients to " "<p>In addition, the controller is obliged to inform all recipients to whom "
"whom it discloses data of any such corrections, deletions, or " "it discloses data of any such corrections, deletions, or restrictions placed "
"restrictions placed on processing the same per Art. 16, 17 Para. 1, 18 " "on processing the same per Art. 16, 17 Para. 1, 18 GDPR. However, this "
"GDPR. However, this obligation does not apply if such notification is " "obligation does not apply if such notification is impossible or involves a "
"impossible or involves a disproportionate effort. Nevertheless, users " "disproportionate effort. Nevertheless, users have a right to information "
"have a right to information about these recipients.</p>\n" "about these recipients.</p>\n"
"<p><strong>Likewise, under Art. 21 GDPR, users and data subjects have the" "<p><strong>Likewise, under Art. 21 GDPR, users and data subjects have the "
" right to object to the controller&apos;s future processing of their data" "right to object to the controller&apos;s future processing of their data "
" pursuant to Art. 6 Para. 1 lit. f) GDPR. In particular, an objection to " "pursuant to Art. 6 Para. 1 lit. f) GDPR. In particular, an objection to data "
"data processing for the purpose of direct advertising is " "processing for the purpose of direct advertising is permissible.</strong></p>"
"permissible.</strong></p>"
#: web/templates/imprint.jinja:38 #: web/templates/public/imprint.jinja:39
msgid "page.imprint.sec4.body" msgid "page.imprint.sec4.body"
msgstr "" msgstr ""
"<p>Your data processed when using our website will be deleted or blocked " "<p>Your data processed when using our website will be deleted or blocked as "
"as soon as the purpose for its storage ceases to apply, provided the " "soon as the purpose for its storage ceases to apply, provided the deletion "
"deletion of the same is not in breach of any statutory storage " "of the same is not in breach of any statutory storage obligations or unless "
"obligations or unless otherwise stipulated below.</p>\n" "otherwise stipulated below.</p>\n"
"<h3>Server data</h3>\n" "<h3>Server data</h3>\n"
"<p>For technical reasons, the following data sent by your internet " "<p>For technical reasons, the following data sent by your internet browser "
"browser to us or to our server provider will be collected, especially to " "to us or to our server provider will be collected, especially to ensure a "
"ensure a secure and stable website: These server log files record the " "secure and stable website: These server log files record the type and "
"type and version of your browser, operating system, the webpages on our " "version of your browser, operating system, the webpages on our site visited, "
"site visited, and the date and time of your visit. The IP address from " "and the date and time of your visit. The IP address from which you visited "
"which you visited our site is additionally recorded if an error " "our site is additionally recorded if an error occurs.</p>\n"
"occurs.</p>\n"
"<p>The data thus collected will be temporarily stored, but not in " "<p>The data thus collected will be temporarily stored, but not in "
"association with any other of your data.</p>\n" "association with any other of your data.</p>\n"
"<p>The basis for this storage is Art. 6 Para. 1 lit. f) GDPR. Our " "<p>The basis for this storage is Art. 6 Para. 1 lit. f) GDPR. Our legitimate "
"legitimate interest lies in the improvement, stability, functionality, " "interest lies in the improvement, stability, functionality, and security of "
"and security of our website.</p>\n" "our website.</p>\n"
"<p>The data will be deleted within no more than seven days, unless " "<p>The data will be deleted within no more than seven days, unless continued "
"continued storage is required for evidentiary purposes. In which case, " "storage is required for evidentiary purposes. In which case, all or part of "
"all or part of the data will be excluded from deletion until the " "the data will be excluded from deletion until the investigation of the "
"investigation of the relevant incident is finally resolved.</p>" "relevant incident is finally resolved.</p>"
#: web/templates/errors/not_found.jinja:8 #: web/templates/secure/login.jinja:3 web/templates/secure/login.jinja:8
msgid "error.404.title" msgid "page.login.title"
msgstr "404 Not Found" msgstr "Login"
#: web/templates/errors/not_found.jinja:9 #: web/templates/secure/login.jinja:20
msgid "error.404.body" msgid "page.login.form.submit"
msgstr "" msgstr "Login"
"<p>Whatever you were looking for cannot be found here.<br>You may want to"
" <a href=\"/\">start over</a>.</p>"
#: 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"

View File

@ -40,6 +40,12 @@
{%endblock%} {%endblock%}
</ul> </ul>
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/secure/login">
<i class="bi bi-person-circle"></i>
<span class="visually-hidden">{{_('base.label.login')}}</span>
</a>
</li>
<li class="nav-menu dropdown"> <li class="nav-menu dropdown">
<button id="theme" class="btn btn-link nav-link dropdown-toggle" type="button" <button id="theme" class="btn btn-link nav-link dropdown-toggle" type="button"
data-bs-toggle="dropdown" aria-expanded="false"> data-bs-toggle="dropdown" aria-expanded="false">
@ -71,12 +77,7 @@
{%block beforeMainContainer%}{%endblock%} {%block beforeMainContainer%}{%endblock%}
{%block body%} {%block body%}
<div class="container" id="content"> {%include 'components/content_empty.jinja'%}
<div class="card">
<h1 class="card-header">{{_('page.empty.title')}}</h1>
<div class="card-body">{{_('page.empty.body')|safe}}</div>
</div>
</div>
{%endblock%} {%endblock%}
{%block afterMainContainer%}{%endblock%} {%block afterMainContainer%}{%endblock%}

View File

@ -0,0 +1,9 @@
<div class="container" id="content">
<section>
<h1 class="text-center text-primary-emphasis">{{_('page.empty.title')}}</h1>
<div class="card">
<div class="card-body">{{_('page.empty.body')|safe}}</div>
</div>
</section>
</div>

View File

@ -1,12 +0,0 @@
{%extends 'base.jinja'%}
{%block title%}{{_('page.home.title')}}{%endblock%}
{%block body%}
<div class="container" id="content">
<div class="card">
<h1 class="card-header text-center text-primary-emphasis">{{_('page.home.card.title')}}</h1>
<div class="card-body">{{_('page.home.card.body')|safe}}</div>
</div>
</div>
{%endblock%}

View File

@ -0,0 +1,33 @@
{% macro field(input, id='', prefix='', suffix='') -%}
<div class="form-group my-1">
{{input.label}}
{%if id and (prefix or suffix)%}
<div class="input-group">
{%if prefix%}
<div class="input-group-prepend">
<div class="input-group-text" id="{{id}}-addon">{{prefix}}</div>
</div>
{%endif%}
{{input()}}
{%if suffix%}
<div class="input-group-append">
<div class="input-group-text" id="{{id}}-addon">{{suffix}}</div>
</div>
{%endif%}
</div>
{%else%}
{{input()}}
{%endif%}
{%if input.errors%}
<p class="text-error">
{%for error in input.errors%}
<span>{{error}}</span>
{%endfor%}
</p>
{%endif%}
</div>
{%- endmacro %}

View File

@ -0,0 +1,15 @@
{%extends 'base.jinja'%}
{%block title%}{{_('page.home.title')}}{%endblock%}
{%block body%}
<div class="container" id="content">
<section>
<h1 class="text-center text-primary-emphasis">{{_('page.home.card.title')}}</h1>
<div class="card">
<div class="card-body">{{_('page.home.card.body')|safe}}</div>
</div>
</section>
</div>
{%endblock%}

View File

@ -4,25 +4,26 @@
{%block body%} {%block body%}
<div class="container" id="content"> <div class="container" id="content">
<div class="d-flex flex-column"> <section class="d-flex flex-column">
<h1 class="mb-4">{{_('page.imprint.title')}}</h1> <h1 class="mb-4 text-center text-primary-emphasis">{{_('page.imprint.title')}}</h1>
<section class="card mb-2">
<div class="card mb-2">
<h2 class="card-header">{{_('page.imprint.card.title')}}</h2> <h2 class="card-header">{{_('page.imprint.card.title')}}</h2>
<div class="card-body"> <div class="card-body">
<p>{{_('page.imprint.card.body')|safe}}</p> <p>{{_('page.imprint.card.body')|safe}}</p>
<p><strong>{{_('page.imprint.card.lastmodified')}} {{bp.data["lastmod"]|dateformat}}</strong></p> <p><strong>{{_('page.imprint.card.lastmodified')}} {{bp.data["lastmod"]|dateformat}}</strong></p>
</div> </div>
</section> </div>
{%for i in range(1,4)%} {%for i in range(1,4)%}
<section class="border rounded p-2 mb-2"> <div class="border rounded p-2 mb-2">
<h2>{{_('page.imprint.sec' ~ i ~ '.title')}}</h2> <h2>{{_('page.imprint.sec' ~ i ~ '.title')}}</h2>
{{_('page.imprint.sec' ~ i ~ '.body')|safe}} {{_('page.imprint.sec' ~ i ~ '.body')|safe}}
</section>
{%endfor%}
<section>
<p><small>{{_('page.imprint.legal')|safe}}</small></p>
</section>
</div> </div>
{%endfor%}
<div>
<p><small>{{_('page.imprint.legal')|safe}}</small></p>
</div>
</section>
</div> </div>
{%endblock%} {%endblock%}

View File

@ -0,0 +1,28 @@
{%extends "base.jinja"%}
{%import "macros/form.jinja" as form%}
{%block title%}{{_('page.login.title')}}{% endblock %}
{%block body%}
<div class="container" id="content">
<section class="d-flex flex-column align-items-center">
<h1 class="text-center text-primary-emphasis">{{_('page.login.title')}}</h1>
<form class="border border-secondary bg-secondary-subtle rounded p-3" method="POST">
{%if bp.data["status"]%}
<div class="alert alert-{{bp.data['status_class']}}" role="alert">{{bp.data["status"]}}</div>
{%endif%}
{{bp.data["form"].hidden_tag() }}
{{form.field(bp.data["form"].username)}}
{{form.field(bp.data["form"].password)}}
<button type="submit" class="my-3 btn btn-primary text-center">{{_('page.login.form.submit')}}</button>
</form>
<div class="d-flex flex-row justify-content-center">
<a class="text-muted p-2" href="/secure/register"><small>{{_('page.login.label.register')}}</small></a>
<a class="text-muted p-2"
href="/secure/lostpassword"><small>{{_('page.login.label.lostpassword')}}</small></a>
</div>
</section>
</div>
{%endblock%}

View File

@ -0,0 +1,27 @@
{%extends "base.jinja"%}
{%import "macros/form.jinja" as form%}
{%block title%}{{_('page.register.title')}}{% endblock %}
{%block body%}
<div class="container" id="content">
<section class="d-flex flex-column align-items-center">
<h1 class="text-center text-primary-emphasis">{{_('page.register.title')}}</h1>
<form class="border border-secondary bg-secondary-subtle rounded p-3" method="POST">
{%if bp.data["status"]%}
<div class="alert alert-{{bp.data['status_class']}}" role="alert">{{bp.data["status"]}}</div>
{%endif%}
{{bp.data["form"].hidden_tag() }}
{{form.field(input=bp.data["form"].username, id="username", prefix="@")}}
{{form.field(input=bp.data["form"].password)}}
{{form.field(input=bp.data["form"].email)}}
<button type="submit" class="my-3 btn btn-primary text-center">{{_('page.register.form.submit')}}</button>
</form>
<div class="d-flex flex-row justify-content-center">
<a class="text-muted p-2" href="/secure/login"><small>{{_('page.register.label.login')}}</small></a>
</div>
</section>
</div>
{%endblock%}