From 06a59bc5991e0da1d2387be6f41f5393f575447f Mon Sep 17 00:00:00 2001 From: bramval Date: Wed, 28 Jan 2026 15:57:24 +0100 Subject: [PATCH] Version 1.1.1 --- .env.example | 2 + README.md | 39 +++-- app.py | 272 ++++++++++++++++++++++++++++++++- init_db.py | 5 +- release.ps1 | 87 ----------- release.py | 117 +++++++++++++- release.sh | 86 ----------- templates/admin_dashboard.html | 72 +++++++++ templates/forgot_password.html | 31 ++++ templates/login.html | 4 + templates/reset_password.html | 29 ++++ 11 files changed, 547 insertions(+), 197 deletions(-) delete mode 100644 release.ps1 delete mode 100644 release.sh create mode 100644 templates/forgot_password.html create mode 100644 templates/reset_password.html diff --git a/.env.example b/.env.example index 475ad78..237acf9 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,5 @@ SQLALCHEMY_DATABASE_URI=sqlite:////instance/liturgie.db # Default admin bootstrap (created only if the user does not exist yet) ADMIN_USERNAME=admin ADMIN_PASSWORD=admin + +# NOTE: SMTP settings are configured via the Admin Dashboard and stored in the DB. diff --git a/README.md b/README.md index 8576dfc..211dacc 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,29 @@ ADMIN_USERNAME=admin ADMIN_PASSWORD=change-me ``` +## SMTP + wachtwoord reset + +De applicatie kan wachtwoord-reset emails sturen via SMTP. + +1. Log in als admin en ga naar **Admin Dashboard**. +2. Vul bij **SMTP instellingen (wachtwoord reset)** de SMTP host/port/credentials en `Van email` in. +3. Gebruik **Test email sturen** om te verifiëren. +4. Gebruikers kunnen vervolgens op de login-pagina klikken op **Wachtwoord vergeten?**. + +Let op: SMTP instellingen worden opgeslagen in de SQLite database (single-row tabel `smtp_settings`). + +### Handmatige test + +1. Start de app. +2. Log in als admin → **Admin Dashboard** → configureer SMTP. +3. Klik **Test email sturen**. +4. Ga naar `/login` → **Wachtwoord vergeten?** → vul een bestaand gebruikers-emailadres in. +5. Open de reset-link uit de email en stel een nieuw wachtwoord in. +6. Log in met het nieuwe wachtwoord. + ## Release / publish (git + docker push) -This repo includes helper scripts that: +This repo includes a helper script that: 1. Prompt for a version (e.g. `1.2.3`) 2. Create a git commit with message `Version ` @@ -62,17 +82,16 @@ This repo includes helper scripts that: python release.py ``` -### Windows (PowerShell) - -```powershell -./release.ps1 -``` - -### Linux / macOS (bash) +Non-interactive: ```bash -chmod +x ./release.sh -./release.sh +python release.py --version 1.2.3 --yes +``` + +Dry run: + +```bash +python release.py --version 1.2.3 --dry-run ``` If Docker push fails due to authentication, run: diff --git a/app.py b/app.py index 1ca6e4d..b4afe22 100644 --- a/app.py +++ b/app.py @@ -7,6 +7,11 @@ from flask import send_from_directory from werkzeug.utils import secure_filename from flask_migrate import Migrate import uuid +import smtplib +import ssl +from email.message import EmailMessage +from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired +from sqlalchemy.exc import OperationalError UPLOAD_FOLDER = 'static/uploads' ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} @@ -33,6 +38,20 @@ login_manager = LoginManager(app) login_manager.login_view = 'login' migrate = Migrate(app, db) +def _get_serializer() -> URLSafeTimedSerializer: + # Uses Flask secret key; rotating SECRET_KEY invalidates outstanding reset links. + return URLSafeTimedSerializer(app.config['SECRET_KEY']) + + +def _current_year() -> int: + from datetime import datetime + return datetime.now().year + + +@app.context_processor +def inject_current_year(): + return {"current_year": _current_year()} + # Models class Church(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -52,6 +71,131 @@ class User(UserMixin, db.Model): church_id = db.Column(db.Integer, db.ForeignKey('church.id'), nullable=False) is_admin = db.Column(db.Boolean, default=False) + +class SMTPSettings(db.Model): + """Global SMTP settings editable by admin. + + For now we keep it simple: a single-row table (id=1) that stores the current + outbound email settings. + """ + + id = db.Column(db.Integer, primary_key=True) + host = db.Column(db.String(255), default="") + port = db.Column(db.Integer, default=587) + username = db.Column(db.String(255), default="") + password = db.Column(db.String(255), default="") # stored as plain text; consider encrypting at rest later + use_tls = db.Column(db.Boolean, default=True) + use_ssl = db.Column(db.Boolean, default=False) + verify_tls = db.Column(db.Boolean, default=True) + from_email = db.Column(db.String(255), default="") + from_name = db.Column(db.String(255), default="PsalmbordOnline") + + +def _ensure_smtp_settings_schema() -> None: + """Best-effort schema sync for sqlite in dev. + + This project uses SQLite and sometimes runs without applying Alembic + migrations (e.g. `flask run`). When new columns are added, older DB files + can crash on SELECT with "no such column". + + We keep this narrowly-scoped to the SMTP settings table. + """ + from sqlalchemy import text + + # Ensure tables exist first (idempotent) + db.create_all() + + # PRAGMA returns empty result if the table doesn't exist. + cols = [row[1] for row in db.session.execute(text("PRAGMA table_info(smtp_settings)"))] + if not cols: + db.create_all() + cols = [row[1] for row in db.session.execute(text("PRAGMA table_info(smtp_settings)"))] + + # Add missing columns (idempotent guarded by PRAGMA) + if "verify_tls" not in cols: + db.session.execute(text("ALTER TABLE smtp_settings ADD COLUMN verify_tls BOOLEAN DEFAULT 1")) + db.session.commit() + + +def get_smtp_settings() -> SMTPSettings: + """Fetch singleton SMTP settings row. + + When running via `flask run`, the container init script (`init_db.py`) is not + executed. If the database already exists but the `smtp_settings` table was + added later, this can raise `OperationalError: no such table`. + + We recover by calling `db.create_all()` (safe/ idempotent for SQLite) and + retrying. + """ + # Make sure schema is up-to-date before ORM SELECTs columns. + try: + _ensure_smtp_settings_schema() + except Exception: + db.session.rollback() + + try: + settings = SMTPSettings.query.get(1) + except OperationalError: + # Handles cases where the DB is older/newer than the model. + db.session.rollback() + try: + _ensure_smtp_settings_schema() + except Exception: + db.session.rollback() + settings = SMTPSettings.query.get(1) + + if not settings: + settings = SMTPSettings(id=1) + db.session.add(settings) + db.session.commit() + return settings + + +def send_email(to_email: str, subject: str, body_text: str) -> tuple[bool, str]: + """Send an email using configured SMTP settings. + + Returns (ok, message) where message is safe to show in UI/log. + """ + settings = get_smtp_settings() + + if not settings.host or not settings.port or not settings.from_email: + return False, "SMTP is niet geconfigureerd. Neem contact op met de beheerder." + + msg = EmailMessage() + msg["Subject"] = subject + msg["From"] = f"{settings.from_name} <{settings.from_email}>" if settings.from_name else settings.from_email + msg["To"] = to_email + msg.set_content(body_text) + + try: + if settings.use_ssl: + context = ssl.create_default_context() + if not settings.verify_tls: + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + with smtplib.SMTP_SSL(settings.host, int(settings.port), context=context, timeout=20) as server: + if settings.username: + server.login(settings.username, settings.password or "") + server.send_message(msg) + else: + with smtplib.SMTP(settings.host, int(settings.port), timeout=20) as server: + server.ehlo() + if settings.use_tls: + context = ssl.create_default_context() + if not settings.verify_tls: + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + server.starttls(context=context) + server.ehlo() + if settings.username: + server.login(settings.username, settings.password or "") + server.send_message(msg) + return True, "Email verstuurd." + except Exception as e: + # Avoid leaking credentials; show generic message. + app.logger.exception("Failed to send email") + return False, f"Email versturen mislukt: {type(e).__name__}" + # Association table for many-to-many relationship board_schedule_association = db.Table('board_schedule', db.Column('board_id', db.Integer, db.ForeignKey('liturgiebord.id'), primary_key=True), @@ -165,6 +309,83 @@ def login(): return render_template('login.html') +def _make_reset_token(user: User) -> str: + s = _get_serializer() + return s.dumps({"user_id": user.id, "pw": user.password}, salt="password-reset") + + +def _load_user_from_reset_token(token: str, max_age_seconds: int = 3600) -> User | None: + s = _get_serializer() + try: + data = s.loads(token, salt="password-reset", max_age=max_age_seconds) + user_id = int(data.get("user_id")) + pw_hash = data.get("pw") + except (BadSignature, SignatureExpired, ValueError, TypeError): + return None + + user = User.query.get(user_id) + if not user: + return None + # Invalidate token if password changed since token issuance. + if pw_hash != user.password: + return None + return user + + +@app.route('/password/forgot', methods=['GET', 'POST']) +def forgot_password(): + if current_user.is_authenticated: + return redirect(url_for('portal')) + + if request.method == 'POST': + email = request.form.get('email', '').strip().lower() + user = User.query.filter_by(username=email).first() if email else None + + # Always show generic message to prevent account enumeration. + flash('Als dit e-mailadres bekend is, ontvang je een link om je wachtwoord te resetten.') + + if user: + token = _make_reset_token(user) + reset_url = url_for('reset_password', token=token, _external=True) + body = ( + "Je hebt een wachtwoord-reset aangevraagd voor PsalmbordOnline.\n\n" + f"Klik op deze link om een nieuw wachtwoord in te stellen (geldig voor 1 uur):\n{reset_url}\n\n" + "Als jij dit niet was, kun je deze email negeren.\n" + ) + send_email(to_email=user.username, subject='Wachtwoord reset', body_text=body) + + return redirect(url_for('login')) + + return render_template('forgot_password.html') + + +@app.route('/password/reset/', methods=['GET', 'POST']) +def reset_password(token): + if current_user.is_authenticated: + return redirect(url_for('portal')) + + user = _load_user_from_reset_token(token) + if not user: + flash('Deze wachtwoord-reset link is ongeldig of verlopen.') + return redirect(url_for('forgot_password')) + + if request.method == 'POST': + new_password = request.form.get('new_password', '') + confirm_password = request.form.get('confirm_password', '') + + if not new_password or not confirm_password: + flash('Vul beide wachtwoordvelden in.') + elif new_password != confirm_password: + flash('De wachtwoorden komen niet overeen.') + else: + user.password = generate_password_hash(new_password) + db.session.commit() + flash('Wachtwoord gewijzigd. Je kunt nu inloggen.') + return redirect(url_for('login')) + + return render_template('reset_password.html', token=token) + + @app.route('/church/delete_user/', methods=['POST']) @login_required def church_delete_user(user_id): @@ -574,16 +795,53 @@ def admin_dashboard(): return redirect(url_for('portal')) if request.method == 'POST': - # Update church activation states - for church in Church.query.all(): - checkbox_val = request.form.get(f'church_active_{church.id}') - church.is_active = checkbox_val == 'on' - db.session.commit() - flash('Kerken activatiestatus bijgewerkt.') + form_type = request.form.get('form_type', 'church_activation') + + if form_type == 'church_activation': + # Update church activation states + for church in Church.query.all(): + checkbox_val = request.form.get(f'church_active_{church.id}') + church.is_active = checkbox_val == 'on' + db.session.commit() + flash('Kerken activatiestatus bijgewerkt.') + + elif form_type == 'smtp_settings': + settings = get_smtp_settings() + settings.host = request.form.get('smtp_host', '').strip() + port_raw = request.form.get('smtp_port', '').strip() + try: + settings.port = int(port_raw) if port_raw else 587 + except ValueError: + settings.port = 587 + settings.username = request.form.get('smtp_username', '').strip() + # Only overwrite password if a value was provided (so admins can leave it blank). + pw = request.form.get('smtp_password', '') + if pw != '': + settings.password = pw + settings.use_tls = request.form.get('smtp_use_tls') == 'on' + settings.use_ssl = request.form.get('smtp_use_ssl') == 'on' + settings.verify_tls = request.form.get('smtp_verify_tls') == 'on' + settings.from_email = request.form.get('smtp_from_email', '').strip() + settings.from_name = request.form.get('smtp_from_name', '').strip() or 'PsalmbordOnline' + db.session.commit() + flash('SMTP instellingen opgeslagen.') + + elif form_type == 'smtp_test': + to_email = request.form.get('smtp_test_to', '').strip() + if not to_email: + flash('Vul een test emailadres in.') + else: + ok, msg = send_email( + to_email=to_email, + subject='PsalmbordOnline SMTP test', + body_text='Dit is een test email vanuit PsalmbordOnline.', + ) + flash(msg) churches = Church.query.all() users = User.query.all() - return render_template('admin_dashboard.html', churches=churches, users=users) + smtp_settings = get_smtp_settings() + return render_template('admin_dashboard.html', churches=churches, users=users, smtp_settings=smtp_settings) @app.route('/admin/impersonate/') @login_required diff --git a/init_db.py b/init_db.py index 784ce2f..5044b83 100644 --- a/init_db.py +++ b/init_db.py @@ -11,7 +11,7 @@ import os from werkzeug.security import generate_password_hash -from app import app, db, User, Church +from app import app, db, User, Church, get_smtp_settings def main() -> None: @@ -21,6 +21,9 @@ def main() -> None: with app.app_context(): db.create_all() + # Ensure singleton SMTP settings row exists + get_smtp_settings() + if not User.query.filter_by(username=admin_username).first(): admin_church = Church.query.filter_by(name="Admin").first() if not admin_church: diff --git a/release.ps1 b/release.ps1 deleted file mode 100644 index a96e40c..0000000 --- a/release.ps1 +++ /dev/null @@ -1,87 +0,0 @@ -$ErrorActionPreference = 'Stop' - -function Ensure-Success($exitCode, $what) { - if ($exitCode -ne 0) { - throw "Failed: $what (exit code $exitCode)" - } -} - -function Get-VersionFromUser { - $version = Read-Host "Enter version (e.g. 1.2.3)" - $version = $version.Trim() - if ([string]::IsNullOrWhiteSpace($version)) { - throw "Version cannot be empty" - } - # Basic docker tag safety: allow only common characters - if ($version -notmatch '^[0-9A-Za-z][0-9A-Za-z._-]{0,127}$') { - throw "Invalid version '$version'. Use only letters/numbers and . _ - (max 128 chars)" - } - return $version -} - -$TargetGitUrl = "https://git.alphen.cloud/bramval/PsalmbordOnlineCE.git" -$DockerImage = "git.alphen.cloud/bramval/psalmbordonlinece" - -$gitCmd = Get-Command git -ErrorAction SilentlyContinue -if (-not $gitCmd) { throw "git is not available in PATH" } -$dockerCmd = Get-Command docker -ErrorAction SilentlyContinue -if (-not $dockerCmd) { throw "docker is not available in PATH" } - -$version = Get-VersionFromUser -$commitMessage = "Version $version" - -$branch = (git branch --show-current).Trim() -if ($branch -ne 'main') { - Write-Host "You are on branch '$branch' (expected 'main')." -ForegroundColor Yellow - $ans = Read-Host "Continue and push 'main' anyway? (y/N)" - if ($ans.ToLower() -ne 'y') { throw "Aborted by user" } -} - -Write-Host "\n== Git: ensuring remote 'origin' points to $TargetGitUrl ==" -ForegroundColor Cyan -$originUrl = (git remote get-url origin 2>$null) -if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($originUrl)) { - Write-Host "Remote 'origin' not found; adding it." -ForegroundColor Yellow - git remote add origin $TargetGitUrl - Ensure-Success $LASTEXITCODE "git remote add" -} elseif ($originUrl -ne $TargetGitUrl) { - Write-Host "Remote 'origin' is '$originUrl' -> updating to '$TargetGitUrl'" -ForegroundColor Yellow - git remote set-url origin $TargetGitUrl - Ensure-Success $LASTEXITCODE "git remote set-url" -} - -Write-Host "\n== Git: status ==" -ForegroundColor Cyan -git status -sb - -$confirm = Read-Host "Proceed with commit+push and docker push? (y/N)" -if ($confirm.ToLower() -ne 'y') { throw "Aborted by user" } - -Write-Host "\n== Git: add/commit/push ==" -ForegroundColor Cyan -git add -A -Ensure-Success $LASTEXITCODE "git add" - -# Commit might fail if nothing to commit; handle gracefully -git commit -m $commitMessage -if ($LASTEXITCODE -ne 0) { - Write-Host "No changes to commit (or commit failed). Continuing to push anyway..." -ForegroundColor Yellow -} else { - Write-Host "Committed: $commitMessage" -ForegroundColor Green -} - -git push -u origin main -Ensure-Success $LASTEXITCODE "git push" - -Write-Host "\n== Docker: build/tag/push ==" -ForegroundColor Cyan -Write-Host "Docker image: $DockerImage" -ForegroundColor Gray -Write-Host "Tags: $version, latest" -ForegroundColor Gray - -docker build -t "${DockerImage}:${version}" -t "${DockerImage}:latest" . -Ensure-Success $LASTEXITCODE "docker build" - -docker push "${DockerImage}:${version}" -Ensure-Success $LASTEXITCODE "docker push (version)" - -docker push "${DockerImage}:latest" -Ensure-Success $LASTEXITCODE "docker push (latest)" - -Write-Host "\nDone." -ForegroundColor Green -Write-Host "If docker push failed due to auth, run: docker login git.alphen.cloud" -ForegroundColor Yellow diff --git a/release.py b/release.py index c4dc335..df337d7 100644 --- a/release.py +++ b/release.py @@ -11,6 +11,7 @@ Flow: from __future__ import annotations +import argparse import re import shutil import subprocess @@ -44,6 +45,15 @@ def ensure_tools() -> None: raise SystemExit(f"Error: '{tool}' is not available in PATH") +def ensure_git_repo() -> None: + try: + out = subprocess.check_output(["git", "rev-parse", "--is-inside-work-tree"], text=True).strip() + except subprocess.CalledProcessError: + raise SystemExit("Error: not a git repository (run from within the repo)") + if out.lower() != "true": + raise SystemExit("Error: not a git work tree") + + def prompt_version() -> str: version = input("Enter version (e.g. 1.2.3): ").strip() if not version: @@ -81,11 +91,64 @@ def ensure_origin(config: Config) -> None: run(["git", "remote", "set-url", "origin", config.target_git_url]) +def parse_args(config: Config) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Commit+push to PsalmbordOnlineCE and build+push docker image (version + latest)." + ) + ) + parser.add_argument( + "--version", + dest="version", + help="Version string used for commit message (Version ) and Docker tag", + ) + parser.add_argument( + "--yes", + action="store_true", + help="Skip confirmation prompts", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print what would happen without committing/pushing/building/pushing docker", + ) + parser.add_argument( + "--branch", + default=config.branch, + help=f"Branch to push (default: {config.branch})", + ) + parser.add_argument( + "--remote-url", + default=config.target_git_url, + help=f"Remote URL to set for origin (default: {config.target_git_url})", + ) + parser.add_argument( + "--docker-image", + default=config.docker_image, + help=f"Docker image name (default: {config.docker_image})", + ) + return parser.parse_args() + + def main() -> int: config = Config() + args = parse_args(config) ensure_tools() - version = prompt_version() + ensure_git_repo() + + version = (args.version or "").strip() or prompt_version() + if not VERSION_RE.match(version): + raise SystemExit( + "Error: invalid version. Use only letters/numbers and . _ - (max 128 chars)" + ) + + config = Config( + target_git_url=args.remote_url, + docker_image=args.docker_image, + branch=args.branch, + ) + commit_message = f"Version {version}" # Branch warning @@ -97,7 +160,9 @@ def main() -> int: print( f"Warning: you are on branch '{current_branch}' (expected '{config.branch}')." ) - if not prompt_yes_no(f"Continue and push '{config.branch}' anyway?", default_no=True): + if not args.yes and not prompt_yes_no( + f"Continue and push '{config.branch}' anyway?", default_no=True + ): return 1 ensure_origin(config) @@ -105,7 +170,31 @@ def main() -> int: print("\n== Git: status ==") run(["git", "status", "-sb"], check=False) - if not prompt_yes_no("Proceed with commit+push and docker push?", default_no=True): + if args.dry_run: + print("\n== Dry run ==") + print("Would run:") + for c in ( + ["git", "add", "-A"], + ["git", "commit", "-m", commit_message], + ["git", "push", "-u", "origin", config.branch], + [ + "docker", + "build", + "-t", + f"{config.docker_image}:{version}", + "-t", + f"{config.docker_image}:latest", + ".", + ], + ["docker", "push", f"{config.docker_image}:{version}"], + ["docker", "push", f"{config.docker_image}:latest"], + ): + print(f" - {' '.join(c)}") + return 0 + + if not args.yes and not prompt_yes_no( + "Proceed with commit+push and docker push?", default_no=True + ): print("Aborted.") return 1 @@ -119,7 +208,16 @@ def main() -> int: except subprocess.CalledProcessError: print("No changes to commit (or commit failed). Continuing...") - run(["git", "push", "-u", "origin", config.branch]) + try: + run(["git", "push", "-u", "origin", config.branch]) + except subprocess.CalledProcessError as e: + print( + "\nGit push failed. If you're using Gitea over HTTPS, you likely need to authenticate with a Personal Access Token (PAT) instead of your password.\n" + "- Ensure you can push: git remote -v\n" + "- Configure credentials (Windows): Credential Manager / Git Credential Manager\n" + "- Or switch to SSH remote and ensure your key is added on the server\n" + ) + return int(getattr(e, "returncode", 1) or 1) print("\n== Docker: build/tag/push ==") print(f"Docker image: {config.docker_image}") @@ -136,8 +234,15 @@ def main() -> int: ".", ] ) - run(["docker", "push", f"{config.docker_image}:{version}"]) - run(["docker", "push", f"{config.docker_image}:latest"]) + try: + run(["docker", "push", f"{config.docker_image}:{version}"]) + run(["docker", "push", f"{config.docker_image}:latest"]) + except subprocess.CalledProcessError as e: + print( + "\nDocker push failed. Make sure you're logged in:\n" + " docker login git.alphen.cloud\n" + ) + return int(getattr(e, "returncode", 1) or 1) print("\nDone.") print("If docker push failed due to auth, run: docker login git.alphen.cloud") diff --git a/release.sh b/release.sh deleted file mode 100644 index ade11d1..0000000 --- a/release.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -TARGET_GIT_URL="https://git.alphen.cloud/bramval/PsalmbordOnlineCE.git" -DOCKER_IMAGE="git.alphen.cloud/bramval/psalmbordonlinece" -BRANCH="main" - -command -v git >/dev/null 2>&1 || { echo "git is not available in PATH" >&2; exit 1; } -command -v docker >/dev/null 2>&1 || { echo "docker is not available in PATH" >&2; exit 1; } - -read -r -p "Enter version (e.g. 1.2.3): " version -version="${version//[[:space:]]/}" -if [[ -z "$version" ]]; then - echo "Version cannot be empty" >&2 - exit 1 -fi - -# Basic docker tag safety -if [[ ! "$version" =~ ^[0-9A-Za-z][0-9A-Za-z._-]{0,127}$ ]]; then - echo "Invalid version '$version'. Use only letters/numbers and . _ - (max 128 chars)" >&2 - exit 1 -fi - -commit_message="Version ${version}" - -current_branch="$(git branch --show-current)" -if [[ "$current_branch" != "$BRANCH" ]]; then - echo "You are on branch '$current_branch' (expected '$BRANCH')." >&2 - read -r -p "Continue and push '$BRANCH' anyway? (y/N): " ans - if [[ "${ans,,}" != "y" ]]; then - echo "Aborted by user" >&2 - exit 1 - fi -fi - -echo -echo "== Git: ensuring remote 'origin' points to ${TARGET_GIT_URL} ==" -if git remote get-url origin >/dev/null 2>&1; then - origin_url="$(git remote get-url origin)" - if [[ "$origin_url" != "$TARGET_GIT_URL" ]]; then - echo "Remote 'origin' is '$origin_url' -> updating to '$TARGET_GIT_URL'" - git remote set-url origin "$TARGET_GIT_URL" - fi -else - echo "Remote 'origin' not found; adding it." - git remote add origin "$TARGET_GIT_URL" -fi - -echo -echo "== Git: status ==" -git status -sb - -read -r -p "Proceed with commit+push and docker push? (y/N): " confirm -if [[ "${confirm,,}" != "y" ]]; then - echo "Aborted by user" >&2 - exit 1 -fi - -echo -echo "== Git: add/commit/push ==" -git add -A - -set +e -git commit -m "$commit_message" -commit_ec=$? -set -e -if [[ $commit_ec -ne 0 ]]; then - echo "No changes to commit (or commit failed). Continuing to push anyway..." >&2 -else - echo "Committed: $commit_message" -fi - -git push -u origin "$BRANCH" - -echo -echo "== Docker: build/tag/push ==" -echo "Docker image: $DOCKER_IMAGE" -echo "Tags: $version, latest" - -docker build -t "${DOCKER_IMAGE}:${version}" -t "${DOCKER_IMAGE}:latest" . -docker push "${DOCKER_IMAGE}:${version}" -docker push "${DOCKER_IMAGE}:latest" - -echo -echo "Done." -echo "If docker push failed due to auth, run: docker login git.alphen.cloud" >&2 diff --git a/templates/admin_dashboard.html b/templates/admin_dashboard.html index 1453431..399eeaf 100644 --- a/templates/admin_dashboard.html +++ b/templates/admin_dashboard.html @@ -11,6 +11,7 @@

Admin Dashboard

+

Kerken

@@ -61,6 +62,77 @@ +
+ +

SMTP instellingen (wachtwoord reset)

+ +
+ + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + STARTTLS (TLS) +
+ +
+ + SSL (SMTP_SSL) +
+ +
+ + TLS certificaat verifiëren (aanbevolen) + (zet uit bij SSLCertVerificationError / self-signed certificaten) +
+ +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+

Gebruikers

diff --git a/templates/forgot_password.html b/templates/forgot_password.html new file mode 100644 index 0000000..162bde7 --- /dev/null +++ b/templates/forgot_password.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} + +{% block title %}Wachtwoord vergeten - Digitale Liturgie{% endblock %} + +{% block content %} + +
+
+

Wachtwoord vergeten

+ +

+ Vul je e-mailadres in. Als het bekend is, sturen we een link om je wachtwoord te resetten. +

+ +
+
+ + +
+ + +
+ + +
+
+ +{% endblock %} diff --git a/templates/login.html b/templates/login.html index 40aef66..e680dd3 100644 --- a/templates/login.html +++ b/templates/login.html @@ -21,6 +21,10 @@ + +
diff --git a/templates/reset_password.html b/templates/reset_password.html new file mode 100644 index 0000000..3d674e5 --- /dev/null +++ b/templates/reset_password.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block title %}Nieuw wachtwoord instellen - Digitale Liturgie{% endblock %} + +{% block content %} + +
+
+

Nieuw wachtwoord instellen

+ +
+
+ + +
+ +
+ + +
+ + +
+
+
+ +{% endblock %}