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 ""
"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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 ""

View File

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

View File

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

View File

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

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

View File

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

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

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 ""
"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 <flare@theflare.at>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\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 ""
"<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
#: web/templates/base.jinja:90
msgid "base.label.copy"
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"
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 ""
"<p>Home of Team Starfall, a small group of friends dedicated to messing "
"with video games in ways the developers are unlikely to expect.</p><p>And"
" hey - occasionally we get fun(ny) results out of it.</p><p>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.</p>"
"<p>Home of Team Starfall, a small group of friends dedicated to messing with "
"video games in ways the developers are unlikely to expect.</p><p>And hey - "
"occasionally we get fun(ny) results out of it.</p><p>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.</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"
msgstr "Imprint &amp; 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).<br />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).<br />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 <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 "
"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 ""
"<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 "
"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.</p>\n"
"<p>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.</p>\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"
"<p>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.</p>\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.</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 "
"of users and data subjects<br>III. Information about the data "
"processing</p>"
"<p>I. Information about us as controllers of your data<br>II. The rights of "
"users and data subjects<br>III. Information about the data processing</p>"
#: web/templates/imprint.jinja:36
#: web/templates/public/imprint.jinja:37
msgid "page.imprint.sec2.body"
msgstr ""
"<p>The party responsible for this website (the „controller“) for purposes"
" of data protection law is:<br>Robert Bäs-Fischlmair a.k.a. Flare "
"<p>The party responsible for this website (the „controller“) for purposes of "
"data protection law is:<br>Robert Bäs-Fischlmair a.k.a. Flare "
"Starfall<br>Phone: +43 (0) 677 62890651<br>Email: <a "
"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"
msgstr ""
"<p>With regard to the data processing to be described in more detail "
"below, users and data subjects have the right</p>\n"
"<p>With regard to the data processing to be described in more detail below, "
"users and data subjects have the right</p>\n"
"<ul>\n"
"<li>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);</li>\n"
"<li>to correct or complete incorrect or incomplete data (cf. also Art. 16 "
"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 "
"DSGVO), or, alternatively, if further processing is necessary as "
"stipulated in Art. 17 Para. 3 GDPR, to restrict said processing per Art. "
"18 GDPR; </li> <li>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);</li> <li>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).</li>\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; </li> "
"<li>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);</li> <li>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).</li>\n"
"</ul>\n"
"<p>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.</p>\n"
"<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"
" pursuant to Art. 6 Para. 1 lit. f) GDPR. In particular, an objection to "
"data processing for the purpose of direct advertising is "
"permissible.</strong></p>"
"<p>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.</p>\n"
"<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 "
"pursuant to Art. 6 Para. 1 lit. f) GDPR. In particular, an objection to data "
"processing for the purpose of direct advertising is permissible.</strong></p>"
#: web/templates/imprint.jinja:38
#: web/templates/public/imprint.jinja:39
msgid "page.imprint.sec4.body"
msgstr ""
"<p>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.</p>\n"
"<p>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.</p>\n"
"<h3>Server data</h3>\n"
"<p>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.</p>\n"
"<p>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.</p>\n"
"<p>The data thus collected will be temporarily stored, but not in "
"association with any other of your data.</p>\n"
"<p>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.</p>\n"
"<p>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.</p>"
"<p>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.</p>\n"
"<p>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.</p>"
#: 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 ""
"<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: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"

View File

@ -40,6 +40,12 @@
{%endblock%}
</ul>
<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">
<button id="theme" class="btn btn-link nav-link dropdown-toggle" type="button"
data-bs-toggle="dropdown" aria-expanded="false">
@ -71,12 +77,7 @@
{%block beforeMainContainer%}{%endblock%}
{%block body%}
<div class="container" id="content">
<div class="card">
<h1 class="card-header">{{_('page.empty.title')}}</h1>
<div class="card-body">{{_('page.empty.body')|safe}}</div>
</div>
</div>
{%include 'components/content_empty.jinja'%}
{%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%}
<div class="container" id="content">
<div class="d-flex flex-column">
<h1 class="mb-4">{{_('page.imprint.title')}}</h1>
<section class="card mb-2">
<section class="d-flex flex-column">
<h1 class="mb-4 text-center text-primary-emphasis">{{_('page.imprint.title')}}</h1>
<div class="card mb-2">
<h2 class="card-header">{{_('page.imprint.card.title')}}</h2>
<div class="card-body">
<p>{{_('page.imprint.card.body')|safe}}</p>
<p><strong>{{_('page.imprint.card.lastmodified')}} {{bp.data["lastmod"]|dateformat}}</strong></p>
</div>
</section>
</div>
{%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>
{{_('page.imprint.sec' ~ i ~ '.body')|safe}}
</section>
{%endfor%}
<section>
<p><small>{{_('page.imprint.legal')|safe}}</small></p>
</section>
</div>
{%endfor%}
<div>
<p><small>{{_('page.imprint.legal')|safe}}</small></p>
</div>
</section>
</div>
{%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%}