fix: disable public signup and restore local login flow
This commit is contained in:
12
README.md
12
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
|
- Echte Web-Push-Architektur mit gespeicherten `PushSubscription`s
|
||||||
- CLI-Kommandos für Archivierung und serverseitig triggerbare Benachrichtigungen
|
- CLI-Kommandos für Archivierung und serverseitig triggerbare Benachrichtigungen
|
||||||
- Cloudron-/Container-tauglicher Start mit `start.sh`, `Dockerfile` und `CloudronManifest.json`
|
- 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
|
## Projektstruktur
|
||||||
|
|
||||||
@@ -108,6 +109,17 @@ Demo-Logins:
|
|||||||
- `anna@putzliga.local` / `putzliga123`
|
- `anna@putzliga.local` / `putzliga123`
|
||||||
- `ben@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
|
### 5. Entwicklungsserver starten
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
19
app/cli.py
19
app/cli.py
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
from .extensions import db
|
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.monthly import archive_months_missing_up_to_previous
|
||||||
from .services.notifications import send_due_notifications, send_monthly_winner_notifications
|
from .services.notifications import send_due_notifications, send_monthly_winner_notifications
|
||||||
|
|
||||||
@@ -54,6 +54,22 @@ def register_cli(app) -> None:
|
|||||||
seed_badges()
|
seed_badges()
|
||||||
click.echo("Datenbank und Standard-Badges sind bereit.")
|
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")
|
@app.cli.command("archive-months")
|
||||||
def archive_months_command():
|
def archive_months_command():
|
||||||
archive_months_missing_up_to_previous()
|
archive_months_missing_up_to_previous()
|
||||||
@@ -68,4 +84,3 @@ def register_cli(app) -> None:
|
|||||||
def notify_monthly_winner_command():
|
def notify_monthly_winner_command():
|
||||||
result = send_monthly_winner_notifications()
|
result = send_monthly_winner_notifications()
|
||||||
click.echo(f"Winner-Push: sent={result.sent} skipped={result.skipped} failed={result.failed}")
|
click.echo(f"Winner-Push: sent={result.sent} skipped={result.skipped} failed={result.failed}")
|
||||||
|
|
||||||
|
|||||||
15
app/forms.py
15
app/forms.py
@@ -5,21 +5,23 @@ from flask_wtf.file import FileAllowed, FileField
|
|||||||
from wtforms import (
|
from wtforms import (
|
||||||
BooleanField,
|
BooleanField,
|
||||||
DateField,
|
DateField,
|
||||||
EmailField,
|
|
||||||
IntegerField,
|
IntegerField,
|
||||||
PasswordField,
|
PasswordField,
|
||||||
SelectField,
|
|
||||||
StringField,
|
StringField,
|
||||||
|
SelectField,
|
||||||
SubmitField,
|
SubmitField,
|
||||||
TextAreaField,
|
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
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
EMAIL_LIKE = Regexp(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", message="Bitte gib eine gültige E-Mail-Adresse ein.")
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(FlaskForm):
|
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)])
|
password = PasswordField("Passwort", validators=[DataRequired(), Length(min=6, max=128)])
|
||||||
remember_me = BooleanField("Angemeldet bleiben")
|
remember_me = BooleanField("Angemeldet bleiben")
|
||||||
submit = SubmitField("Einloggen")
|
submit = SubmitField("Einloggen")
|
||||||
@@ -27,7 +29,7 @@ class LoginForm(FlaskForm):
|
|||||||
|
|
||||||
class RegisterForm(FlaskForm):
|
class RegisterForm(FlaskForm):
|
||||||
name = StringField("Name", validators=[DataRequired(), Length(min=2, max=120)])
|
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 = PasswordField("Passwort", validators=[DataRequired(), Length(min=6, max=128)])
|
||||||
password_confirm = PasswordField(
|
password_confirm = PasswordField(
|
||||||
"Passwort wiederholen",
|
"Passwort wiederholen",
|
||||||
@@ -76,7 +78,7 @@ class TaskForm(FlaskForm):
|
|||||||
|
|
||||||
class SettingsProfileForm(FlaskForm):
|
class SettingsProfileForm(FlaskForm):
|
||||||
name = StringField("Name", validators=[DataRequired(), Length(min=2, max=120)])
|
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)])
|
password = PasswordField("Neues Passwort", validators=[Optional(), Length(min=6, max=128)])
|
||||||
avatar = FileField(
|
avatar = FileField(
|
||||||
"Avatar",
|
"Avatar",
|
||||||
@@ -96,4 +98,3 @@ class SettingsProfileForm(FlaskForm):
|
|||||||
return
|
return
|
||||||
if User.query.filter_by(email=value).first():
|
if User.query.filter_by(email=value).first():
|
||||||
raise ValidationError("Diese E-Mail-Adresse wird bereits verwendet.")
|
raise ValidationError("Diese E-Mail-Adresse wird bereits verwendet.")
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ from ..models import User
|
|||||||
bp = Blueprint("auth", __name__)
|
bp = Blueprint("auth", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
def registration_open() -> bool:
|
||||||
|
return User.query.count() == 0
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/login", methods=["GET", "POST"])
|
@bp.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
@@ -24,13 +28,16 @@ def login():
|
|||||||
flash(f"Willkommen zurück, {user.name}.", "success")
|
flash(f"Willkommen zurück, {user.name}.", "success")
|
||||||
return redirect(url_for("tasks.my_tasks"))
|
return redirect(url_for("tasks.my_tasks"))
|
||||||
flash("Die Kombination aus E-Mail und Passwort passt leider nicht.", "error")
|
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"])
|
@bp.route("/register", methods=["GET", "POST"])
|
||||||
def register():
|
def register():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for("tasks.my_tasks"))
|
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()
|
form = RegisterForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
@@ -50,4 +57,3 @@ def logout():
|
|||||||
logout_user()
|
logout_user()
|
||||||
flash("Du bist jetzt abgemeldet.", "info")
|
flash("Du bist jetzt abgemeldet.", "info")
|
||||||
return redirect(url_for("auth.login"))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 358 B |
Binary file not shown.
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
@@ -1,6 +1,5 @@
|
|||||||
const CACHE_NAME = "putzliga-shell-v1";
|
const CACHE_NAME = "putzliga-shell-v2";
|
||||||
const ASSETS = [
|
const ASSETS = [
|
||||||
"/my-tasks",
|
|
||||||
"/static/css/style.css",
|
"/static/css/style.css",
|
||||||
"/static/js/app.js",
|
"/static/js/app.js",
|
||||||
"/static/images/logo.svg",
|
"/static/images/logo.svg",
|
||||||
@@ -24,6 +23,24 @@ self.addEventListener("activate", (event) => {
|
|||||||
|
|
||||||
self.addEventListener("fetch", (event) => {
|
self.addEventListener("fetch", (event) => {
|
||||||
if (event.request.method !== "GET") return;
|
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(
|
event.respondWith(
|
||||||
caches.match(event.request).then((cached) => {
|
caches.match(event.request).then((cached) => {
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
@@ -32,8 +49,7 @@ self.addEventListener("fetch", (event) => {
|
|||||||
const clone = response.clone();
|
const clone = response.clone();
|
||||||
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
||||||
return response;
|
return response;
|
||||||
})
|
});
|
||||||
.catch(() => caches.match("/my-tasks"));
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,8 +41,11 @@
|
|||||||
{{ form.submit(class_="button button--wide") }}
|
{{ form.submit(class_="button button--wide") }}
|
||||||
</form>
|
</form>
|
||||||
<p class="inline-note">Demo-Logins nach dem Seeden: `anna@putzliga.local` / `putzliga123` und `ben@putzliga.local` / `putzliga123`.</p>
|
<p class="inline-note">Demo-Logins nach dem Seeden: `anna@putzliga.local` / `putzliga123` und `ben@putzliga.local` / `putzliga123`.</p>
|
||||||
<p class="inline-note">Noch kein Konto? <a href="{{ url_for('auth.register') }}">Neu registrieren</a></p>
|
{% if registration_open %}
|
||||||
|
<p class="inline-note">Es gibt noch keinen Nutzer. <a href="{{ url_for('auth.register') }}">Ersten Account anlegen</a></p>
|
||||||
|
{% else %}
|
||||||
|
<p class="inline-note">Freie Registrierung ist deaktiviert.</p>
|
||||||
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user