Add email verification and password reset
mindflux-ci / build (push) Successful in 2m26s

This commit is contained in:
AliceDarkest
2026-07-15 11:10:07 +02:00
parent 80f556b1d0
commit ce1975b47a
19 changed files with 467 additions and 13 deletions
+15
View File
@@ -41,6 +41,21 @@ spring.output.ansi.enabled=ALWAYS
# Öffentliche Basis-URL, z. B. für künftig erzeugte absolute Links.
mindflux.public-url=${MINDFLUX_PUBLIC_URL:http://localhost:8282}
mindflux.mail.from=${MAIL_USERNAME:noreply@sysnaptik.de}
# =======================
# E-Mail-Versand
# =======================
spring.mail.host=${MAIL_HOST:localhost}
spring.mail.port=${MAIL_PORT:587}
spring.mail.username=${MAIL_USERNAME:}
spring.mail.password=${MAIL_PASSWORD:}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.connectiontimeout=5000
spring.mail.properties.mail.smtp.timeout=5000
spring.mail.properties.mail.smtp.writetimeout=5000
# =======================
# Datei-Uploads (Profilbilder)
@@ -0,0 +1,17 @@
ALTER TABLE users
ADD COLUMN email_verified BOOLEAN NOT NULL DEFAULT TRUE;
CREATE TABLE account_email_tokens (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash CHAR(64) NOT NULL UNIQUE,
purpose VARCHAR(32) NOT NULL,
expires_at TIMESTAMP NOT NULL,
consumed_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT account_email_tokens_purpose_check
CHECK (purpose IN ('EMAIL_VERIFICATION', 'PASSWORD_RESET'))
);
CREATE INDEX idx_account_email_tokens_lookup
ON account_email_tokens (token_hash, purpose);
@@ -28,6 +28,7 @@
</header>
<main>
<div th:if="${param.passwordPolicy}" class="mf-alert err">Das neue Passwort muss mindestens 8 Zeichen lang sein.</div>
<div class="mf-panel">
<table class="mf-table">
<thead>
@@ -48,8 +49,8 @@
<form th:action="@{'/admin/users/' + ${user.id} + '/reset-password'}" method="post"
style="display:flex; gap:.35rem; align-items:center;"
onsubmit="return confirm('Passwort für diesen Nutzer wirklich neu setzen?');">
<input class="mf-input" type="text" name="newPassword" placeholder="Neues Passwort"
required minlength="4" style="width:150px; padding:.45rem .6rem;">
<input class="mf-input" type="password" name="newPassword" placeholder="Neues Passwort"
required minlength="8" style="width:150px; padding:.45rem .6rem;">
<button type="submit" class="mf-btn">PW zurücksetzen</button>
</form>
<form th:action="@{'/admin/users/delete/' + ${user.id}}" method="post" style="display:inline;"
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="de" data-theme="dark" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passwort zurücksetzen · Mindflux</title>
<link rel="stylesheet" href="/css/theme.css?v=3">
<script src="/js/theme.js?v=4"></script>
</head>
<body>
<div class="mf-theme-float"><button class="mf-theme"></button></div>
<main class="mf-container" style="max-width:34rem; padding-top:5rem">
<section class="mf-panel">
<div class="mf-eyebrow">Account-Zugang</div>
<h1>Passwort zurücksetzen</h1>
<p>Gib deine E-Mail-Adresse ein. Falls ein bestätigter Account dazugehört, senden wir dir einen Link zum Setzen eines neuen Passworts.</p>
<div th:if="${param.sent}" class="mf-alert ok">Falls ein passender Account existiert, wurde eine E-Mail versendet.</div>
<form th:action="@{/forgot-password}" method="post">
<div class="mf-field">
<label class="mf-label" for="email">E-Mail-Adresse</label>
<input id="email" class="mf-input" type="email" name="email" required autocomplete="email">
</div>
<button class="mf-btn" type="submit">Reset-Link senden</button>
</form>
<p style="margin-top:1rem"><a href="/">Zur Anmeldung</a></p>
</section>
</main>
</body>
</html>
+19 -2
View File
@@ -29,7 +29,12 @@
<div th:if="${param.error}" class="mf-alert err">Benutzername oder Passwort sind falsch.</div>
<div th:if="${param.logout}" class="mf-alert ok">Du hast dich erfolgreich abgemeldet.</div>
<div th:if="${param.registerSuccess}" class="mf-alert ok">Registrierung erfolgreich! Du kannst dich jetzt einloggen.</div>
<div th:if="${param.registerSuccess}" class="mf-alert ok">Fast geschafft: Wir haben dir einen Bestätigungslink per E-Mail gesendet.</div>
<div th:if="${param.emailVerified}" class="mf-alert ok">Deine E-Mail-Adresse wurde bestätigt. Du kannst dich jetzt anmelden.</div>
<div th:if="${param.passwordReset}" class="mf-alert ok">Dein Passwort wurde geändert. Du kannst dich jetzt anmelden.</div>
<div th:if="${param.verificationSent}" class="mf-alert ok">Falls ein unbestätigter Account zu dieser Adresse existiert, wurde ein neuer Link versendet.</div>
<div th:if="${param.verificationInvalid}" class="mf-alert err">Dieser Bestätigungslink ist ungültig oder abgelaufen.</div>
<div th:if="${param.resetInvalid}" class="mf-alert err">Dieser Passwort-Link ist ungültig oder abgelaufen.</div>
<form th:action="@{/login}" method="post">
<div class="mf-field">
@@ -43,6 +48,8 @@
<button class="neon-btn" type="submit">Login</button>
</form>
<p class="mobile-switch"><a href="/forgot-password">Passwort vergessen?</a></p>
<p class="mobile-switch">Noch keinen Account? <a data-goto="signup">Jetzt registrieren</a></p>
</div>
</div>
@@ -61,6 +68,8 @@
<div th:if="${param.passwordMismatch}" class="mf-alert err">Die Passwörter stimmen nicht überein. Bitte erneut eingeben.</div>
<div th:if="${param.passwordPolicy}" class="mf-alert err">Das Passwort muss mindestens 8 Zeichen lang sein.</div>
<div th:if="${param.emailInvalid}" class="mf-alert err">Bitte gib eine gültige E-Mail-Adresse an.</div>
<div th:if="${param.emailTaken}" class="mf-alert err">Diese E-Mail-Adresse wird bereits verwendet.</div>
<div th:if="${param.registerMailError}" class="mf-alert err">Der Account wurde angelegt, aber die Bestätigungs-E-Mail konnte nicht gesendet werden. Fordere unten einen neuen Link an.</div>
<form th:action="@{/register}" method="post">
<div class="mf-field">
@@ -83,6 +92,14 @@
<button class="neon-btn" type="submit">Account erstellen</button>
</form>
<form th:action="@{/verification/resend}" method="post" style="margin-top:1rem">
<label class="mf-label" for="resend-email">Bestätigungslink erneut senden</label>
<div style="display:flex; gap:.5rem; flex-wrap:wrap; margin-top:.35rem">
<input id="resend-email" class="mf-input" type="email" name="email" placeholder="E-Mail-Adresse" required>
<button class="mf-btn outline" type="submit">Link senden</button>
</div>
</form>
<p class="mobile-switch">Schon dabei? <a data-goto="login">Zum Login</a></p>
</div>
</div>
@@ -138,7 +155,7 @@
// Bei Registrierungs-/Passwortfehler direkt das Signup-Panel öffnen
var qp = new URLSearchParams(window.location.search);
if (qp.has('registerError') || qp.has('passwordMismatch')) {
if (qp.has('registerError') || qp.has('passwordMismatch') || qp.has('passwordPolicy') || qp.has('emailInvalid') || qp.has('emailTaken') || qp.has('registerMailError')) {
c.classList.add('active');
}
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="de" data-theme="dark" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neues Passwort · Mindflux</title>
<link rel="stylesheet" href="/css/theme.css?v=3">
<script src="/js/theme.js?v=4"></script>
</head>
<body>
<div class="mf-theme-float"><button class="mf-theme"></button></div>
<main class="mf-container" style="max-width:34rem; padding-top:5rem">
<section class="mf-panel">
<div class="mf-eyebrow">Account-Zugang</div>
<h1>Neues Passwort vergeben</h1>
<div th:if="${param.error == 'policy'}" class="mf-alert err">Das Passwort muss mindestens 8 Zeichen lang sein.</div>
<div th:if="${param.error == 'mismatch'}" class="mf-alert err">Die Passwörter stimmen nicht überein.</div>
<form th:action="@{/reset-password}" method="post">
<input type="hidden" name="token" th:value="${token}">
<div class="mf-field">
<label class="mf-label" for="password">Neues Passwort</label>
<input id="password" class="mf-input" type="password" name="password" required minlength="8" autocomplete="new-password">
</div>
<div class="mf-field">
<label class="mf-label" for="confirm-password">Passwort wiederholen</label>
<input id="confirm-password" class="mf-input" type="password" name="confirmPassword" required minlength="8" autocomplete="new-password">
</div>
<button class="mf-btn" type="submit">Passwort speichern</button>
</form>
</section>
</main>
</body>
</html>