diff --git a/README.md b/README.md index f2b3114..11274f9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Putzliga ist eine moderne, leichte Haushaltsaufgaben-Web-App mit spielerischem C - Echte Web-Push-Architektur mit gespeicherten `PushSubscription`s - CLI-Kommandos für Archivierung und serverseitig triggerbare Benachrichtigungen - Cloudron-/Container-tauglicher Start mit `start.sh`, `Dockerfile` und `CloudronManifest.json` +- Keine freie Registrierung nach dem ersten Nutzer; weitere Nutzer lassen sich kontrolliert per CLI anlegen ## Projektstruktur @@ -108,6 +109,17 @@ Demo-Logins: - `anna@putzliga.local` / `putzliga123` - `ben@putzliga.local` / `putzliga123` +## Nutzer anlegen + +Freie Registrierung ist deaktiviert, sobald mindestens ein Nutzer existiert. + +- Wenn die Datenbank noch leer ist, darf genau der erste Nutzer über `/register` angelegt werden. +- Weitere Nutzer legst du kontrolliert per CLI an: + +```bash +flask --app app.py create-user +``` + ### 5. Entwicklungsserver starten ```bash diff --git a/app/cli.py b/app/cli.py index b6b4897..0c2bcea 100644 --- a/app/cli.py +++ b/app/cli.py @@ -3,7 +3,7 @@ from __future__ import annotations import click from .extensions import db -from .models import BadgeDefinition +from .models import BadgeDefinition, User from .services.monthly import archive_months_missing_up_to_previous from .services.notifications import send_due_notifications, send_monthly_winner_notifications @@ -54,6 +54,22 @@ def register_cli(app) -> None: seed_badges() click.echo("Datenbank und Standard-Badges sind bereit.") + @app.cli.command("create-user") + @click.option("--name", prompt=True, help="Anzeigename") + @click.option("--email", prompt=True, help="E-Mail") + @click.option("--password", prompt=True, hide_input=True, confirmation_prompt=True, help="Passwort") + def create_user_command(name: str, email: str, password: str): + existing = User.query.filter_by(email=email.lower().strip()).first() + if existing: + click.echo("Es existiert bereits ein Nutzer mit dieser E-Mail.") + raise SystemExit(1) + + user = User(name=name.strip(), email=email.lower().strip()) + user.set_password(password) + db.session.add(user) + db.session.commit() + click.echo(f"Nutzer {user.email} wurde angelegt.") + @app.cli.command("archive-months") def archive_months_command(): archive_months_missing_up_to_previous() @@ -68,4 +84,3 @@ def register_cli(app) -> None: def notify_monthly_winner_command(): result = send_monthly_winner_notifications() click.echo(f"Winner-Push: sent={result.sent} skipped={result.skipped} failed={result.failed}") - diff --git a/app/forms.py b/app/forms.py index 396bef1..5ee8cfc 100644 --- a/app/forms.py +++ b/app/forms.py @@ -5,21 +5,23 @@ from flask_wtf.file import FileAllowed, FileField from wtforms import ( BooleanField, DateField, - EmailField, IntegerField, PasswordField, - SelectField, StringField, + SelectField, SubmitField, TextAreaField, ) -from wtforms.validators import DataRequired, Email, EqualTo, Length, NumberRange, Optional, ValidationError +from wtforms.validators import DataRequired, EqualTo, Length, NumberRange, Optional, Regexp, ValidationError from .models import User +EMAIL_LIKE = Regexp(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", message="Bitte gib eine gültige E-Mail-Adresse ein.") + + class LoginForm(FlaskForm): - email = EmailField("E-Mail", validators=[DataRequired(), Email(), Length(max=255)]) + email = StringField("E-Mail", validators=[DataRequired(), EMAIL_LIKE, Length(max=255)]) password = PasswordField("Passwort", validators=[DataRequired(), Length(min=6, max=128)]) remember_me = BooleanField("Angemeldet bleiben") submit = SubmitField("Einloggen") @@ -27,7 +29,7 @@ class LoginForm(FlaskForm): class RegisterForm(FlaskForm): name = StringField("Name", validators=[DataRequired(), Length(min=2, max=120)]) - email = EmailField("E-Mail", validators=[DataRequired(), Email(), Length(max=255)]) + email = StringField("E-Mail", validators=[DataRequired(), EMAIL_LIKE, Length(max=255)]) password = PasswordField("Passwort", validators=[DataRequired(), Length(min=6, max=128)]) password_confirm = PasswordField( "Passwort wiederholen", @@ -76,7 +78,7 @@ class TaskForm(FlaskForm): class SettingsProfileForm(FlaskForm): name = StringField("Name", validators=[DataRequired(), Length(min=2, max=120)]) - email = EmailField("E-Mail", validators=[DataRequired(), Email(), Length(max=255)]) + email = StringField("E-Mail", validators=[DataRequired(), EMAIL_LIKE, Length(max=255)]) password = PasswordField("Neues Passwort", validators=[Optional(), Length(min=6, max=128)]) avatar = FileField( "Avatar", @@ -96,4 +98,3 @@ class SettingsProfileForm(FlaskForm): return if User.query.filter_by(email=value).first(): raise ValidationError("Diese E-Mail-Adresse wird bereits verwendet.") - diff --git a/app/routes/auth.py b/app/routes/auth.py index d4786ed..808b01c 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -11,6 +11,10 @@ from ..models import User bp = Blueprint("auth", __name__) +def registration_open() -> bool: + return User.query.count() == 0 + + @bp.route("/login", methods=["GET", "POST"]) def login(): if current_user.is_authenticated: @@ -24,13 +28,16 @@ def login(): flash(f"Willkommen zurück, {user.name}.", "success") return redirect(url_for("tasks.my_tasks")) flash("Die Kombination aus E-Mail und Passwort passt leider nicht.", "error") - return render_template("auth/login.html", form=form) + return render_template("auth/login.html", form=form, registration_open=registration_open()) @bp.route("/register", methods=["GET", "POST"]) def register(): if current_user.is_authenticated: return redirect(url_for("tasks.my_tasks")) + if not registration_open(): + flash("Freie Registrierung ist deaktiviert.", "info") + return redirect(url_for("auth.login")) form = RegisterForm() if form.validate_on_submit(): @@ -50,4 +57,3 @@ def logout(): logout_user() flash("Du bist jetzt abgemeldet.", "info") return redirect(url_for("auth.login")) - diff --git a/app/static/images/pwa-badge.png b/app/static/images/pwa-badge.png index e2b7ae3..17961a8 100644 Binary files a/app/static/images/pwa-badge.png and b/app/static/images/pwa-badge.png differ diff --git a/app/static/images/pwa-icon-512.png b/app/static/images/pwa-icon-512.png index dc832a8..a5e29fd 100644 Binary files a/app/static/images/pwa-icon-512.png and b/app/static/images/pwa-icon-512.png differ diff --git a/app/static/service-worker.js b/app/static/service-worker.js index e084201..142e92c 100644 --- a/app/static/service-worker.js +++ b/app/static/service-worker.js @@ -1,6 +1,5 @@ -const CACHE_NAME = "putzliga-shell-v1"; +const CACHE_NAME = "putzliga-shell-v2"; const ASSETS = [ - "/my-tasks", "/static/css/style.css", "/static/js/app.js", "/static/images/logo.svg", @@ -24,6 +23,24 @@ self.addEventListener("activate", (event) => { self.addEventListener("fetch", (event) => { if (event.request.method !== "GET") return; + const url = new URL(event.request.url); + const isStaticAsset = + url.origin === self.location.origin && + ( + url.pathname.startsWith("/static/") || + url.pathname === "/manifest.json" || + url.pathname === "/service-worker.js" + ); + + if (event.request.mode === "navigate") { + event.respondWith(fetch(event.request)); + return; + } + + if (!isStaticAsset) { + return; + } + event.respondWith( caches.match(event.request).then((cached) => { if (cached) return cached; @@ -32,8 +49,7 @@ self.addEventListener("fetch", (event) => { const clone = response.clone(); caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); return response; - }) - .catch(() => caches.match("/my-tasks")); + }); }) ); }); diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html index dda1ba3..c150d0d 100644 --- a/app/templates/auth/login.html +++ b/app/templates/auth/login.html @@ -41,8 +41,11 @@ {{ form.submit(class_="button button--wide") }}

Demo-Logins nach dem Seeden: `anna@putzliga.local` / `putzliga123` und `ben@putzliga.local` / `putzliga123`.

-

Noch kein Konto? Neu registrieren

+ {% if registration_open %} +

Es gibt noch keinen Nutzer. Ersten Account anlegen

+ {% else %} +

Freie Registrierung ist deaktiviert.

+ {% endif %} {% endblock %} -