Version 1.1.1
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
39
README.md
39
README.md
@@ -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
260
app.py
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
87
release.ps1
87
release.ps1
@@ -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
|
|
||||||
111
release.py
111
release.py
@@ -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")
|
||||||
|
|||||||
86
release.sh
86
release.sh
@@ -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
|
|
||||||
@@ -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>
|
||||||
|
|||||||
31
templates/forgot_password.html
Normal file
31
templates/forgot_password.html
Normal 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 %}
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
29
templates/reset_password.html
Normal file
29
templates/reset_password.html
Normal 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 %}
|
||||||
Reference in New Issue
Block a user