Version 1.1.1

This commit is contained in:
2026-01-28 15:57:24 +01:00
parent 105bcc8d7a
commit 06a59bc599
11 changed files with 547 additions and 197 deletions

View File

@@ -14,3 +14,5 @@ SQLALCHEMY_DATABASE_URI=sqlite:////instance/liturgie.db
# Default admin bootstrap (created only if the user does not exist yet) # Default admin bootstrap (created only if the user does not exist yet)
ADMIN_USERNAME=admin ADMIN_USERNAME=admin
ADMIN_PASSWORD=admin ADMIN_PASSWORD=admin
# NOTE: SMTP settings are configured via the Admin Dashboard and stored in the DB.

View File

@@ -45,9 +45,29 @@ ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me 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) ## 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`) 1. Prompt for a version (e.g. `1.2.3`)
2. Create a git commit with message `Version <version>` 2. Create a git commit with message `Version <version>`
@@ -62,17 +82,16 @@ This repo includes helper scripts that:
python release.py python release.py
``` ```
### Windows (PowerShell) Non-interactive:
```powershell
./release.ps1
```
### Linux / macOS (bash)
```bash ```bash
chmod +x ./release.sh python release.py --version 1.2.3 --yes
./release.sh ```
Dry run:
```bash
python release.py --version 1.2.3 --dry-run
``` ```
If Docker push fails due to authentication, run: If Docker push fails due to authentication, run:

260
app.py
View File

@@ -7,6 +7,11 @@ from flask import send_from_directory
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from flask_migrate import Migrate from flask_migrate import Migrate
import uuid 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' UPLOAD_FOLDER = 'static/uploads'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
@@ -33,6 +38,20 @@ login_manager = LoginManager(app)
login_manager.login_view = 'login' login_manager.login_view = 'login'
migrate = Migrate(app, db) 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 # Models
class Church(db.Model): class Church(db.Model):
id = db.Column(db.Integer, primary_key=True) 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) church_id = db.Column(db.Integer, db.ForeignKey('church.id'), nullable=False)
is_admin = db.Column(db.Boolean, default=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 # Association table for many-to-many relationship
board_schedule_association = db.Table('board_schedule', board_schedule_association = db.Table('board_schedule',
db.Column('board_id', db.Integer, db.ForeignKey('liturgiebord.id'), primary_key=True), db.Column('board_id', db.Integer, db.ForeignKey('liturgiebord.id'), primary_key=True),
@@ -165,6 +309,83 @@ def login():
return render_template('login.html') 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/<token>', 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/<int:user_id>', methods=['POST']) @app.route('/church/delete_user/<int:user_id>', methods=['POST'])
@login_required @login_required
def church_delete_user(user_id): def church_delete_user(user_id):
@@ -574,6 +795,9 @@ def admin_dashboard():
return redirect(url_for('portal')) return redirect(url_for('portal'))
if request.method == 'POST': if request.method == 'POST':
form_type = request.form.get('form_type', 'church_activation')
if form_type == 'church_activation':
# Update church activation states # Update church activation states
for church in Church.query.all(): for church in Church.query.all():
checkbox_val = request.form.get(f'church_active_{church.id}') checkbox_val = request.form.get(f'church_active_{church.id}')
@@ -581,9 +805,43 @@ def admin_dashboard():
db.session.commit() db.session.commit()
flash('Kerken activatiestatus bijgewerkt.') 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() churches = Church.query.all()
users = User.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/<int:user_id>') @app.route('/admin/impersonate/<int:user_id>')
@login_required @login_required

View File

@@ -11,7 +11,7 @@ import os
from werkzeug.security import generate_password_hash 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: def main() -> None:
@@ -21,6 +21,9 @@ def main() -> None:
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
# Ensure singleton SMTP settings row exists
get_smtp_settings()
if not User.query.filter_by(username=admin_username).first(): if not User.query.filter_by(username=admin_username).first():
admin_church = Church.query.filter_by(name="Admin").first() admin_church = Church.query.filter_by(name="Admin").first()
if not admin_church: if not admin_church:

View File

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

View File

@@ -11,6 +11,7 @@ Flow:
from __future__ import annotations from __future__ import annotations
import argparse
import re import re
import shutil import shutil
import subprocess import subprocess
@@ -44,6 +45,15 @@ def ensure_tools() -> None:
raise SystemExit(f"Error: '{tool}' is not available in PATH") 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: def prompt_version() -> str:
version = input("Enter version (e.g. 1.2.3): ").strip() version = input("Enter version (e.g. 1.2.3): ").strip()
if not version: if not version:
@@ -81,11 +91,64 @@ def ensure_origin(config: Config) -> None:
run(["git", "remote", "set-url", "origin", config.target_git_url]) 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 <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: def main() -> int:
config = Config() config = Config()
args = parse_args(config)
ensure_tools() 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}" commit_message = f"Version {version}"
# Branch warning # Branch warning
@@ -97,7 +160,9 @@ def main() -> int:
print( print(
f"Warning: you are on branch '{current_branch}' (expected '{config.branch}')." 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 return 1
ensure_origin(config) ensure_origin(config)
@@ -105,7 +170,31 @@ def main() -> int:
print("\n== Git: status ==") print("\n== Git: status ==")
run(["git", "status", "-sb"], check=False) 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.") print("Aborted.")
return 1 return 1
@@ -119,7 +208,16 @@ def main() -> int:
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
print("No changes to commit (or commit failed). Continuing...") print("No changes to commit (or commit failed). Continuing...")
try:
run(["git", "push", "-u", "origin", config.branch]) 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("\n== Docker: build/tag/push ==")
print(f"Docker image: {config.docker_image}") print(f"Docker image: {config.docker_image}")
@@ -136,8 +234,15 @@ def main() -> int:
".", ".",
] ]
) )
try:
run(["docker", "push", f"{config.docker_image}:{version}"]) run(["docker", "push", f"{config.docker_image}:{version}"])
run(["docker", "push", f"{config.docker_image}:latest"]) 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("\nDone.")
print("If docker push failed due to auth, run: docker login git.alphen.cloud") print("If docker push failed due to auth, run: docker login git.alphen.cloud")

View File

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

View File

@@ -11,6 +11,7 @@
<h2 class="text-3xl font-semibold mb-6">Admin Dashboard</h2> <h2 class="text-3xl font-semibold mb-6">Admin Dashboard</h2>
<form method="POST" class="space-y-6"> <form method="POST" class="space-y-6">
<input type="hidden" name="form_type" value="church_activation">
<div> <div>
<h3 class="text-2xl font-semibold mb-4">Kerken</h3> <h3 class="text-2xl font-semibold mb-4">Kerken</h3>
@@ -61,6 +62,77 @@
</section> </section>
<section>
<h3 class="text-2xl font-semibold mb-4">SMTP instellingen (wachtwoord reset)</h3>
<div class="bg-white rounded-lg border border-gray-300 p-6 space-y-6">
<form method="POST" class="grid grid-cols-1 md:grid-cols-2 gap-6">
<input type="hidden" name="form_type" value="smtp_settings">
<div>
<label class="block mb-2 font-semibold text-gray-700">SMTP host</label>
<input type="text" name="smtp_host" value="{{ smtp_settings.host }}" class="w-full rounded-md border border-gray-300 p-3 shadow-sm">
</div>
<div>
<label class="block mb-2 font-semibold text-gray-700">SMTP port</label>
<input type="number" name="smtp_port" value="{{ smtp_settings.port }}" class="w-full rounded-md border border-gray-300 p-3 shadow-sm">
</div>
<div>
<label class="block mb-2 font-semibold text-gray-700">Gebruikersnaam</label>
<input type="text" name="smtp_username" value="{{ smtp_settings.username }}" class="w-full rounded-md border border-gray-300 p-3 shadow-sm">
</div>
<div>
<label class="block mb-2 font-semibold text-gray-700">Wachtwoord</label>
<input type="password" name="smtp_password" value="" placeholder="(leeg laten om niet te wijzigen)" class="w-full rounded-md border border-gray-300 p-3 shadow-sm">
</div>
<div>
<label class="block mb-2 font-semibold text-gray-700">Van email</label>
<input type="email" name="smtp_from_email" value="{{ smtp_settings.from_email }}" class="w-full rounded-md border border-gray-300 p-3 shadow-sm">
</div>
<div>
<label class="block mb-2 font-semibold text-gray-700">Van naam</label>
<input type="text" name="smtp_from_name" value="{{ smtp_settings.from_name }}" class="w-full rounded-md border border-gray-300 p-3 shadow-sm">
</div>
<div class="flex items-center gap-3">
<input type="checkbox" name="smtp_use_tls" {% if smtp_settings.use_tls %}checked{% endif %} class="h-5 w-5 text-blue-600">
<span class="text-gray-700 font-semibold">STARTTLS (TLS)</span>
</div>
<div class="flex items-center gap-3">
<input type="checkbox" name="smtp_use_ssl" {% if smtp_settings.use_ssl %}checked{% endif %} class="h-5 w-5 text-blue-600">
<span class="text-gray-700 font-semibold">SSL (SMTP_SSL)</span>
</div>
<div class="md:col-span-2 flex items-center gap-3">
<input type="checkbox" name="smtp_verify_tls" {% if smtp_settings.verify_tls %}checked{% endif %} class="h-5 w-5 text-blue-600">
<span class="text-gray-700 font-semibold">TLS certificaat verifiëren (aanbevolen)</span>
<span class="text-xs text-gray-500">(zet uit bij SSLCertVerificationError / self-signed certificaten)</span>
</div>
<div class="md:col-span-2 flex gap-3">
<button type="submit" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition">SMTP opslaan</button>
</div>
</form>
<form method="POST" class="flex flex-col md:flex-row gap-3 items-start md:items-end">
<input type="hidden" name="form_type" value="smtp_test">
<div class="flex-1 w-full">
<label class="block mb-2 font-semibold text-gray-700">Test email naar</label>
<input type="email" name="smtp_test_to" placeholder="test@voorbeeld.nl" class="w-full rounded-md border border-gray-300 p-3 shadow-sm">
</div>
<button type="submit" class="bg-gray-800 text-white px-6 py-3 rounded-md font-semibold shadow hover:bg-gray-900 transition">Test email sturen</button>
</form>
</div>
</section>
<section> <section>
<h3 class="text-2xl font-semibold mb-4">Gebruikers</h3> <h3 class="text-2xl font-semibold mb-4">Gebruikers</h3>

View File

@@ -0,0 +1,31 @@
{% extends 'base.html' %}
{% block title %}Wachtwoord vergeten - Digitale Liturgie{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<div class="max-w-md mx-auto">
<h2 class="text-3xl font-semibold mb-6">Wachtwoord vergeten</h2>
<p class="text-gray-700 mb-6">
Vul je e-mailadres in. Als het bekend is, sturen we een link om je wachtwoord te resetten.
</p>
<form method="post" class="space-y-6">
<div>
<label for="email" class="block mb-3 font-semibold text-gray-700">E-mailadres</label>
<input type="email" id="email" name="email" required
class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<button type="submit" class="bg-[#f7d91a] text-black w-full rounded-md py-4 font-semibold shadow hover:bg-yellow-300 transition">Reset link versturen</button>
</form>
<div class="mt-6 text-center">
<a href="{{ url_for('login') }}" class="text-blue-600 hover:underline">Terug naar inloggen</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -21,6 +21,10 @@
<button type="submit" class="bg-[#f7d91a] text-black w-full rounded-md py-4 font-semibold shadow hover:bg-yellow-300 transition">Login</button> <button type="submit" class="bg-[#f7d91a] text-black w-full rounded-md py-4 font-semibold shadow hover:bg-yellow-300 transition">Login</button>
</form> </form>
<div class="mt-6 text-center">
<a href="{{ url_for('forgot_password') }}" class="text-blue-600 hover:underline">Wachtwoord vergeten?</a>
</div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,29 @@
{% extends 'base.html' %}
{% block title %}Nieuw wachtwoord instellen - Digitale Liturgie{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<div class="max-w-md mx-auto">
<h2 class="text-3xl font-semibold mb-6">Nieuw wachtwoord instellen</h2>
<form method="post" class="space-y-6">
<div>
<label for="new_password" class="block mb-3 font-semibold text-gray-700">Nieuw wachtwoord</label>
<input type="password" id="new_password" name="new_password" required
class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="confirm_password" class="block mb-3 font-semibold text-gray-700">Bevestig nieuw wachtwoord</label>
<input type="password" id="confirm_password" name="confirm_password" required
class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<button type="submit" class="bg-[#f7d91a] text-black w-full rounded-md py-4 font-semibold shadow hover:bg-yellow-300 transition">Wachtwoord opslaan</button>
</form>
</div>
</div>
{% endblock %}