feat(auth): implement password reset functionality

- Added new endpoints for requesting and resetting passwords in the authController.
- Updated User model to include resetToken and resetTokenExpires fields for managing password reset requests.
- Enhanced emailService to send password reset emails with secure links.
- Updated frontend routes and views to support password reset flow, including new ForgotPassword and ResetPassword components.
- Improved internationalization files with new translation keys for password reset messages across multiple languages.
This commit is contained in:
Torsten Schulz (local)
2026-02-09 08:40:27 +01:00
parent 76f1b1a12f
commit e22e3257ef
25 changed files with 722 additions and 22 deletions

View File

@@ -1,4 +1,4 @@
import { register, activateUser, login, logout } from '../services/authService.js';
import { register, activateUser, login, logout, requestPasswordReset, resetPassword } from '../services/authService.js';
const registerUser = async (req, res, next) => {
try {
@@ -45,4 +45,24 @@ const logoutUser = async (req, res, next) => {
}
};
export { registerUser, activate, loginUser, logoutUser };
const forgotPassword = async (req, res, next) => {
try {
const { email } = req.body;
const result = await requestPasswordReset(email);
res.status(200).json(result);
} catch (error) {
next(error);
}
};
const resetUserPassword = async (req, res, next) => {
try {
const { token, password } = req.body;
const result = await resetPassword(token, password);
res.status(200).json(result);
} catch (error) {
next(error);
}
};
export { registerUser, activate, loginUser, logoutUser, forgotPassword, resetUserPassword };

View File

@@ -0,0 +1,8 @@
-- Felder für "Passwort vergessen"-Funktion
ALTER TABLE user
ADD COLUMN reset_token VARCHAR(255) NULL DEFAULT NULL
COMMENT 'Token für Passwort-Reset';
ALTER TABLE user
ADD COLUMN reset_token_expires DATETIME NULL DEFAULT NULL
COMMENT 'Ablaufzeitpunkt des Reset-Tokens';

View File

@@ -37,6 +37,16 @@ const User = sequelize.define('User', {
authCode: {
type: DataTypes.STRING,
allowNull: true
},
resetToken: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Token für Passwort-Reset'
},
resetTokenExpires: {
type: DataTypes.DATE,
allowNull: true,
comment: 'Ablaufzeitpunkt des Reset-Tokens'
}
}, {
underscored: true,

View File

@@ -1,11 +1,13 @@
import express from 'express';
import { registerUser, activate, loginUser, logoutUser } from '../controllers/authController.js';
import { registerUser, activate, loginUser, logoutUser, forgotPassword, resetUserPassword } from '../controllers/authController.js';
const router = express.Router();
router.post('/register', registerUser);
router.get('/activate/:activationCode', activate);
router.post('/login', loginUser);
router.post('/logout', logoutUser); // Ändere GET zu POST
router.post('/logout', logoutUser);
router.post('/forgot-password', forgotPassword);
router.post('/reset-password', resetUserPassword);
export default router;

View File

@@ -2,7 +2,8 @@ import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import User from '../models/User.js';
import UserToken from '../models/UserToken.js';
import { sendActivationEmail } from './emailService.js';
import crypto from 'crypto';
import { sendActivationEmail, sendPasswordResetEmail } from './emailService.js';
import { devLog, errorLog } from '../utils/logger.js';
@@ -101,4 +102,80 @@ const logout = async (token) => {
return { message: 'Logout erfolgreich' };
};
export { register, activateUser, login, logout };
const requestPasswordReset = async (email) => {
if (!email) {
const err = new Error('E-Mail-Adresse ist erforderlich.');
err.status = 400;
throw err;
}
const user = await User.findOne({ where: { email } });
// Aus Sicherheitsgründen IMMER Erfolg melden (verhindert E-Mail-Enumeration)
if (!user) {
return { message: 'Falls ein Konto mit dieser E-Mail existiert, wurde ein Link zum Zurücksetzen gesendet.' };
}
// Token erzeugen (kryptographisch sicher)
const resetToken = crypto.randomBytes(32).toString('hex');
const resetTokenExpires = new Date(Date.now() + 60 * 60 * 1000); // 1 Stunde gültig
user.resetToken = resetToken;
user.resetTokenExpires = resetTokenExpires;
await user.save();
try {
await sendPasswordResetEmail(email, resetToken);
} catch (mailError) {
errorLog('[authService.requestPasswordReset] Fehler beim E-Mail-Versand', {
message: mailError?.message,
code: mailError?.code,
});
const err = new Error('E-Mail konnte nicht gesendet werden. Bitte Administrator kontaktieren.');
err.status = 500;
throw err;
}
return { message: 'Falls ein Konto mit dieser E-Mail existiert, wurde ein Link zum Zurücksetzen gesendet.' };
};
const resetPassword = async (token, newPassword) => {
if (!token || !newPassword) {
const err = new Error('Token und neues Passwort sind erforderlich.');
err.status = 400;
throw err;
}
if (newPassword.length < 6) {
const err = new Error('Das Passwort muss mindestens 6 Zeichen lang sein.');
err.status = 400;
throw err;
}
const { Op } = await import('sequelize');
const user = await User.findOne({
where: {
resetToken: token,
resetTokenExpires: { [Op.gt]: new Date() }
}
});
if (!user) {
const err = new Error('Der Link ist ungültig oder abgelaufen. Bitte fordere einen neuen an.');
err.status = 400;
throw err;
}
// Neues Passwort setzen (wird automatisch durch beforeUpdate-Hook gehasht)
user.password = newPassword;
user.resetToken = null;
user.resetTokenExpires = null;
await user.save();
// Alle bestehenden Tokens des Users löschen (erzwingt Neuanmeldung)
await UserToken.destroy({ where: { userId: user.id } });
return { message: 'Passwort wurde erfolgreich geändert.' };
};
export { register, activateUser, login, logout, requestPasswordReset, resetPassword };

View File

@@ -18,4 +18,36 @@ const sendActivationEmail = async (email, activationCode) => {
await transporter.sendMail(mailOptions);
};
export { sendActivationEmail };
const sendPasswordResetEmail = async (email, resetToken) => {
const resetLink = `${process.env.BASE_URL}/reset-password/${resetToken}`;
const mailOptions = {
from: process.env.EMAIL_USER,
to: email,
subject: 'Passwort zurücksetzen',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #333;">Passwort zurücksetzen</h2>
<p>Du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.</p>
<p>Klicke auf den folgenden Link, um ein neues Passwort zu vergeben:</p>
<p style="margin: 20px 0;">
<a href="${resetLink}"
style="background-color: #4CAF50; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">
Passwort zurücksetzen
</a>
</p>
<p style="color: #666; font-size: 14px;">
Dieser Link ist <strong>1 Stunde</strong> gültig.<br>
Falls du diese Anfrage nicht gestellt hast, ignoriere diese E-Mail einfach.
</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="color: #999; font-size: 12px;">
Falls der Button nicht funktioniert, kopiere diesen Link in deinen Browser:<br>
<a href="${resetLink}">${resetLink}</a>
</p>
</div>
`,
};
await transporter.sendMail(mailOptions);
};
export { sendActivationEmail, sendPasswordResetEmail };

View File

@@ -65,7 +65,25 @@
"rememberMe": "Aagmeldet bliibe",
"loginSuccess": "Erfolgriich aagmeldet",
"logoutSuccess": "Erfolgriich abgmeldet",
"sessionExpired": "Dini Sitzig isch abglaufe. Du wirsch abgmeldet."
"sessionExpired": "Dini Sitzig isch abglaufe. Du wirsch abgmeldet.",
"noAccount": "No kei Konto?",
"toLogin": "Zum Login",
"loginFailed": "Login fehlgschlage. Bitte Zuegangsdaten prüefe.",
"activationFailed": "Aktivierig fehlgschlage. Bitte de Link überprüefe.",
"forgotPasswordDescription": "Gib dini E-Mail-Adrässe ii. Du bechunsch en Link, zum dis Passwort zruggsetze.",
"sendResetLink": "Link schicke",
"sending": "Wird gschickt...",
"resetEmailSent": "Falls es Konto mit dere E-Mail git, isch en Link zum Zruggsetze gschickt worde. Bitte prüef dis Postfach (au de Spam-Ordner).",
"resetRequestFailed": "Aafrag fehlgschlage. Bitte probiers nomal.",
"resetPassword": "Nöis Passwort vergee",
"newPassword": "Nöis Passwort",
"confirmPassword": "Passwort bestätige",
"saveNewPassword": "Passwort speichere",
"saving": "Wird gspeicheret...",
"passwordResetSuccess": "Dis Passwort isch erfolgriich gänderet worde. Du chasch dich jetzt aaloge.",
"passwordsDoNotMatch": "D'Passwörter stimed nöd überein.",
"passwordTooShort": "S'Passwort muess mindestens 6 Zeiche lang sii.",
"resetFailed": "Passwort het nöd chönne gänderet werde. De Link isch villicht abglaufe."
},
"settings": {
"title": "Iistellige",

View File

@@ -110,7 +110,24 @@
"loginFailed": "Login fehlgeschlagen. Bitte Zugangsdaten prüfen und erneut versuchen.",
"registerSuccess": "Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mails, um den Account zu aktivieren.",
"registerFailed": "Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut.",
"activate": "Aktivieren"
"activate": "Aktivieren",
"activateAccount": "Account aktivieren",
"accountActivated": "Account aktiviert! Du kannst dich jetzt anmelden.",
"activationFailed": "Aktivierung fehlgeschlagen. Bitte überprüfe den Link oder versuche es erneut.",
"forgotPasswordDescription": "Gib deine E-Mail-Adresse ein. Du erhältst einen Link, um dein Passwort zurückzusetzen.",
"sendResetLink": "Link senden",
"sending": "Wird gesendet...",
"resetEmailSent": "Falls ein Konto mit dieser E-Mail existiert, wurde ein Link zum Zurücksetzen gesendet. Bitte prüfe dein Postfach (auch den Spam-Ordner).",
"resetRequestFailed": "Anfrage fehlgeschlagen. Bitte versuche es erneut.",
"resetPassword": "Neues Passwort vergeben",
"newPassword": "Neues Passwort",
"confirmPassword": "Passwort bestätigen",
"saveNewPassword": "Passwort speichern",
"saving": "Wird gespeichert...",
"passwordResetSuccess": "Dein Passwort wurde erfolgreich geändert. Du kannst dich jetzt mit deinem neuen Passwort anmelden.",
"passwordsDoNotMatch": "Die Passwörter stimmen nicht überein.",
"passwordTooShort": "Das Passwort muss mindestens 6 Zeichen lang sein.",
"resetFailed": "Passwort konnte nicht geändert werden. Der Link ist möglicherweise abgelaufen."
},
"settings": {
"title": "Einstellungen",

View File

@@ -124,7 +124,21 @@
"activate": "Aktivieren",
"activateAccount": "Account aktivieren",
"accountActivated": "Account aktiviert! Du kannst dich jetzt anmelden.",
"activationFailed": "Aktivierung fehlgeschlagen. Bitte überprüfe den Link oder versuche es erneut."
"activationFailed": "Aktivierung fehlgeschlagen. Bitte überprüfe den Link oder versuche es erneut.",
"forgotPasswordDescription": "Gib deine E-Mail-Adresse ein. Du erhältst einen Link, um dein Passwort zurückzusetzen.",
"sendResetLink": "Link senden",
"sending": "Wird gesendet...",
"resetEmailSent": "Falls ein Konto mit dieser E-Mail existiert, wurde ein Link zum Zurücksetzen gesendet. Bitte prüfe dein Postfach (auch den Spam-Ordner).",
"resetRequestFailed": "Anfrage fehlgeschlagen. Bitte versuche es erneut.",
"resetPassword": "Neues Passwort vergeben",
"newPassword": "Neues Passwort",
"confirmPassword": "Passwort bestätigen",
"saveNewPassword": "Passwort speichern",
"saving": "Wird gespeichert...",
"passwordResetSuccess": "Dein Passwort wurde erfolgreich geändert. Du kannst dich jetzt mit deinem neuen Passwort anmelden.",
"passwordsDoNotMatch": "Die Passwörter stimmen nicht überein.",
"passwordTooShort": "Das Passwort muss mindestens 6 Zeichen lang sein.",
"resetFailed": "Passwort konnte nicht geändert werden. Der Link ist möglicherweise abgelaufen."
},
"settings": {
"title": "Einstellungen",

View File

@@ -65,7 +65,25 @@
"rememberMe": "Remember me",
"loginSuccess": "Successfully logged in",
"logoutSuccess": "Successfully logged out",
"sessionExpired": "Your session has expired. You will be logged out."
"sessionExpired": "Your session has expired. You will be logged out.",
"noAccount": "Don't have an account?",
"toLogin": "Go to login",
"loginFailed": "Login failed. Please check your credentials and try again.",
"activationFailed": "Activation failed. Please check the link or try again.",
"forgotPasswordDescription": "Enter your email address. You will receive a link to reset your password.",
"sendResetLink": "Send link",
"sending": "Sending...",
"resetEmailSent": "If an account with this email exists, a reset link has been sent. Please check your inbox (and spam folder).",
"resetRequestFailed": "Request failed. Please try again.",
"resetPassword": "Set new password",
"newPassword": "New password",
"confirmPassword": "Confirm password",
"saveNewPassword": "Save password",
"saving": "Saving...",
"passwordResetSuccess": "Your password has been changed successfully. You can now log in with your new password.",
"passwordsDoNotMatch": "Passwords do not match.",
"passwordTooShort": "Password must be at least 6 characters long.",
"resetFailed": "Password could not be changed. The link may have expired."
},
"settings": {
"title": "Settings",

View File

@@ -65,7 +65,31 @@
"rememberMe": "Remember me",
"loginSuccess": "Successfully logged in",
"logoutSuccess": "Successfully logged out",
"sessionExpired": "Your session has expired. You will be logged out."
"sessionExpired": "Your session has expired. You will be logged out.",
"noAccount": "Don't have an account?",
"hasAccount": "Already have an account?",
"toLogin": "Go to login",
"loginFailed": "Login failed. Please check your credentials and try again.",
"registerSuccess": "Registration successful! Please check your email to activate your account.",
"registerFailed": "Registration failed. Please try again.",
"activate": "Activate",
"activateAccount": "Activate account",
"accountActivated": "Account activated! You can now log in.",
"activationFailed": "Activation failed. Please check the link or try again.",
"forgotPasswordDescription": "Enter your email address. You will receive a link to reset your password.",
"sendResetLink": "Send link",
"sending": "Sending...",
"resetEmailSent": "If an account with this email exists, a reset link has been sent. Please check your inbox (and spam folder).",
"resetRequestFailed": "Request failed. Please try again.",
"resetPassword": "Set new password",
"newPassword": "New password",
"confirmPassword": "Confirm password",
"saveNewPassword": "Save password",
"saving": "Saving...",
"passwordResetSuccess": "Your password has been changed successfully. You can now log in with your new password.",
"passwordsDoNotMatch": "Passwords do not match.",
"passwordTooShort": "Password must be at least 6 characters long.",
"resetFailed": "Password could not be changed. The link may have expired."
},
"settings": {
"title": "Settings",

View File

@@ -65,7 +65,25 @@
"rememberMe": "Remember me",
"loginSuccess": "Successfully logged in",
"logoutSuccess": "Successfully logged out",
"sessionExpired": "Your session has expired. You will be logged out."
"sessionExpired": "Your session has expired. You will be logged out.",
"noAccount": "Don't have an account?",
"toLogin": "Go to login",
"loginFailed": "Login failed. Please check your credentials and try again.",
"activationFailed": "Activation failed. Please check the link or try again.",
"forgotPasswordDescription": "Enter your email address. You will receive a link to reset your password.",
"sendResetLink": "Send link",
"sending": "Sending...",
"resetEmailSent": "If an account with this email exists, a reset link has been sent. Please check your inbox (and spam folder).",
"resetRequestFailed": "Request failed. Please try again.",
"resetPassword": "Set new password",
"newPassword": "New password",
"confirmPassword": "Confirm password",
"saveNewPassword": "Save password",
"saving": "Saving...",
"passwordResetSuccess": "Your password has been changed successfully. You can now log in with your new password.",
"passwordsDoNotMatch": "Passwords do not match.",
"passwordTooShort": "Password must be at least 6 characters long.",
"resetFailed": "Password could not be changed. The link may have expired."
},
"settings": {
"title": "Settings",

View File

@@ -65,7 +65,25 @@
"rememberMe": "Recordarme",
"loginSuccess": "Sesión iniciada correctamente",
"logoutSuccess": "Sesión cerrada correctamente",
"sessionExpired": "Tu sesión ha expirado. Serás desconectado."
"sessionExpired": "Tu sesión ha expirado. Serás desconectado.",
"noAccount": "¿No tienes cuenta?",
"toLogin": "Ir al inicio de sesión",
"loginFailed": "Inicio de sesión fallido. Por favor verifica tus credenciales.",
"activationFailed": "Activación fallida. Por favor verifica el enlace.",
"forgotPasswordDescription": "Introduce tu dirección de correo electrónico. Recibirás un enlace para restablecer tu contraseña.",
"sendResetLink": "Enviar enlace",
"sending": "Enviando...",
"resetEmailSent": "Si existe una cuenta con este correo, se ha enviado un enlace de restablecimiento. Revisa tu bandeja de entrada (y la carpeta de spam).",
"resetRequestFailed": "Solicitud fallida. Inténtalo de nuevo.",
"resetPassword": "Establecer nueva contraseña",
"newPassword": "Nueva contraseña",
"confirmPassword": "Confirmar contraseña",
"saveNewPassword": "Guardar contraseña",
"saving": "Guardando...",
"passwordResetSuccess": "Tu contraseña ha sido cambiada exitosamente. Ahora puedes iniciar sesión.",
"passwordsDoNotMatch": "Las contraseñas no coinciden.",
"passwordTooShort": "La contraseña debe tener al menos 6 caracteres.",
"resetFailed": "No se pudo cambiar la contraseña. El enlace puede haber expirado."
},
"settings": {
"title": "Configuración",

View File

@@ -65,7 +65,25 @@
"rememberMe": "Tandaan ako",
"loginSuccess": "Matagumpay na nag-login",
"logoutSuccess": "Matagumpay na nag-logout",
"sessionExpired": "Nag-expire na ang iyong session. Ikaw ay ma-logout."
"sessionExpired": "Nag-expire na ang iyong session. Ikaw ay ma-logout.",
"noAccount": "Wala pang account?",
"toLogin": "Pumunta sa login",
"loginFailed": "Nabigo ang pag-login. Suriin ang iyong credentials.",
"activationFailed": "Nabigo ang activation. Suriin ang link.",
"forgotPasswordDescription": "Ilagay ang iyong email address. Makakatanggap ka ng link para i-reset ang password.",
"sendResetLink": "Ipadala ang link",
"sending": "Ipinapadala...",
"resetEmailSent": "Kung may account sa email na ito, ipinadala na ang reset link. Suriin ang inbox (at spam folder).",
"resetRequestFailed": "Nabigo ang request. Subukan ulit.",
"resetPassword": "Magtakda ng bagong password",
"newPassword": "Bagong password",
"confirmPassword": "Kumpirmahin ang password",
"saveNewPassword": "I-save ang password",
"saving": "Sine-save...",
"passwordResetSuccess": "Matagumpay na nabago ang password. Maaari ka nang mag-login.",
"passwordsDoNotMatch": "Hindi magkatugma ang mga password.",
"passwordTooShort": "Ang password ay dapat hindi bababa sa 6 na character.",
"resetFailed": "Hindi nabago ang password. Maaaring nag-expire na ang link."
},
"settings": {
"title": "Mga setting",

View File

@@ -65,7 +65,25 @@
"rememberMe": "Se souvenir de moi",
"loginSuccess": "Connexion réussie",
"logoutSuccess": "Déconnexion réussie",
"sessionExpired": "Votre session a expiré. Vous allez être déconnecté."
"sessionExpired": "Votre session a expiré. Vous allez être déconnecté.",
"noAccount": "Pas encore de compte ?",
"toLogin": "Vers la connexion",
"loginFailed": "Connexion échouée. Veuillez vérifier vos identifiants.",
"activationFailed": "Activation échouée. Veuillez vérifier le lien.",
"forgotPasswordDescription": "Entrez votre adresse e-mail. Vous recevrez un lien pour réinitialiser votre mot de passe.",
"sendResetLink": "Envoyer le lien",
"sending": "Envoi en cours...",
"resetEmailSent": "Si un compte avec cette adresse existe, un lien de réinitialisation a été envoyé. Vérifiez votre boîte de réception (et les spams).",
"resetRequestFailed": "Demande échouée. Veuillez réessayer.",
"resetPassword": "Définir un nouveau mot de passe",
"newPassword": "Nouveau mot de passe",
"confirmPassword": "Confirmer le mot de passe",
"saveNewPassword": "Enregistrer le mot de passe",
"saving": "Enregistrement...",
"passwordResetSuccess": "Votre mot de passe a été modifié avec succès. Vous pouvez maintenant vous connecter.",
"passwordsDoNotMatch": "Les mots de passe ne correspondent pas.",
"passwordTooShort": "Le mot de passe doit contenir au moins 6 caractères.",
"resetFailed": "Le mot de passe n'a pas pu être modifié. Le lien a peut-être expiré."
},
"settings": {
"title": "Paramètres",

View File

@@ -65,7 +65,25 @@
"rememberMe": "Ricordami",
"loginSuccess": "Accesso effettuato con successo",
"logoutSuccess": "Uscita effettuata con successo",
"sessionExpired": "La tua sessione è scaduta. Verrai disconnesso."
"sessionExpired": "La tua sessione è scaduta. Verrai disconnesso.",
"noAccount": "Non hai un account?",
"toLogin": "Vai al login",
"loginFailed": "Accesso fallito. Controlla le credenziali.",
"activationFailed": "Attivazione fallita. Controlla il link.",
"forgotPasswordDescription": "Inserisci il tuo indirizzo email. Riceverai un link per reimpostare la password.",
"sendResetLink": "Invia link",
"sending": "Invio in corso...",
"resetEmailSent": "Se esiste un account con questa email, è stato inviato un link di reimpostazione. Controlla la posta (e lo spam).",
"resetRequestFailed": "Richiesta fallita. Riprova.",
"resetPassword": "Imposta nuova password",
"newPassword": "Nuova password",
"confirmPassword": "Conferma password",
"saveNewPassword": "Salva password",
"saving": "Salvataggio...",
"passwordResetSuccess": "La password è stata cambiata con successo. Ora puoi accedere.",
"passwordsDoNotMatch": "Le password non corrispondono.",
"passwordTooShort": "La password deve contenere almeno 6 caratteri.",
"resetFailed": "La password non è stata modificata. Il link potrebbe essere scaduto."
},
"settings": {
"title": "Impostazioni",

View File

@@ -65,7 +65,25 @@
"rememberMe": "ログイン状態を保持",
"loginSuccess": "ログインに成功しました",
"logoutSuccess": "ログアウトに成功しました",
"sessionExpired": "セッションが期限切れです。ログアウトされます。"
"sessionExpired": "セッションが期限切れです。ログアウトされます。",
"noAccount": "アカウントをお持ちでないですか?",
"toLogin": "ログインへ",
"loginFailed": "ログインに失敗しました。認証情報を確認してください。",
"activationFailed": "アクティベーションに失敗しました。リンクを確認してください。",
"forgotPasswordDescription": "メールアドレスを入力してください。パスワードリセット用のリンクが送信されます。",
"sendResetLink": "リンクを送信",
"sending": "送信中...",
"resetEmailSent": "このメールアドレスのアカウントが存在する場合、リセットリンクが送信されました。受信トレイ(迷惑メールフォルダも)を確認してください。",
"resetRequestFailed": "リクエストに失敗しました。再試行してください。",
"resetPassword": "新しいパスワードを設定",
"newPassword": "新しいパスワード",
"confirmPassword": "パスワードを確認",
"saveNewPassword": "パスワードを保存",
"saving": "保存中...",
"passwordResetSuccess": "パスワードが正常に変更されました。新しいパスワードでログインできます。",
"passwordsDoNotMatch": "パスワードが一致しません。",
"passwordTooShort": "パスワードは6文字以上必要です。",
"resetFailed": "パスワードを変更できませんでした。リンクの有効期限が切れている可能性があります。"
},
"settings": {
"title": "設定",

View File

@@ -65,7 +65,25 @@
"rememberMe": "Zapamiętaj mnie",
"loginSuccess": "Pomyślnie zalogowano",
"logoutSuccess": "Pomyślnie wylogowano",
"sessionExpired": "Twoja sesja wygasła. Zostaniesz wylogowany."
"sessionExpired": "Twoja sesja wygasła. Zostaniesz wylogowany.",
"noAccount": "Nie masz konta?",
"toLogin": "Do logowania",
"loginFailed": "Logowanie nie powiodło się. Sprawdź dane logowania.",
"activationFailed": "Aktywacja nie powiodła się. Sprawdź link.",
"forgotPasswordDescription": "Podaj swój adres e-mail. Otrzymasz link do zresetowania hasła.",
"sendResetLink": "Wyślij link",
"sending": "Wysyłanie...",
"resetEmailSent": "Jeśli konto z tym adresem istnieje, wysłano link do resetowania. Sprawdź skrzynkę (i spam).",
"resetRequestFailed": "Żądanie nie powiodło się. Spróbuj ponownie.",
"resetPassword": "Ustaw nowe hasło",
"newPassword": "Nowe hasło",
"confirmPassword": "Potwierdź hasło",
"saveNewPassword": "Zapisz hasło",
"saving": "Zapisywanie...",
"passwordResetSuccess": "Hasło zostało zmienione pomyślnie. Możesz się teraz zalogować.",
"passwordsDoNotMatch": "Hasła nie są zgodne.",
"passwordTooShort": "Hasło musi mieć co najmniej 6 znaków.",
"resetFailed": "Nie udało się zmienić hasła. Link mógł wygasnąć."
},
"settings": {
"title": "Ustawienia",

View File

@@ -65,7 +65,25 @@
"rememberMe": "จดจำฉัน",
"loginSuccess": "เข้าสู่ระบบสำเร็จ",
"logoutSuccess": "ออกจากระบบสำเร็จ",
"sessionExpired": "เซสชันของคุณหมดอายุแล้ว คุณจะถูกออกจากระบบ"
"sessionExpired": "เซสชันของคุณหมดอายุแล้ว คุณจะถูกออกจากระบบ",
"noAccount": "ยังไม่มีบัญชี?",
"toLogin": "ไปที่เข้าสู่ระบบ",
"loginFailed": "เข้าสู่ระบบล้มเหลว กรุณาตรวจสอบข้อมูลของคุณ",
"activationFailed": "การเปิดใช้งานล้มเหลว กรุณาตรวจสอบลิงก์",
"forgotPasswordDescription": "กรอกอีเมลของคุณ คุณจะได้รับลิงก์สำหรับรีเซ็ตรหัสผ่าน",
"sendResetLink": "ส่งลิงก์",
"sending": "กำลังส่ง...",
"resetEmailSent": "หากมีบัญชีที่ใช้อีเมลนี้ ลิงก์รีเซ็ตได้ถูกส่งแล้ว กรุณาตรวจสอบกล่องจดหมาย (และสแปม)",
"resetRequestFailed": "คำขอล้มเหลว กรุณาลองอีกครั้ง",
"resetPassword": "ตั้งรหัสผ่านใหม่",
"newPassword": "รหัสผ่านใหม่",
"confirmPassword": "ยืนยันรหัสผ่าน",
"saveNewPassword": "บันทึกรหัสผ่าน",
"saving": "กำลังบันทึก...",
"passwordResetSuccess": "เปลี่ยนรหัสผ่านสำเร็จ คุณสามารถเข้าสู่ระบบได้แล้ว",
"passwordsDoNotMatch": "รหัสผ่านไม่ตรงกัน",
"passwordTooShort": "รหัสผ่านต้องมีอย่างน้อย 6 ตัวอักษร",
"resetFailed": "ไม่สามารถเปลี่ยนรหัสผ่านได้ ลิงก์อาจหมดอายุแล้ว"
},
"settings": {
"title": "การตั้งค่า",

View File

@@ -65,7 +65,25 @@
"rememberMe": "Tandaan ako",
"loginSuccess": "Matagumpay na nag-login",
"logoutSuccess": "Matagumpay na nag-logout",
"sessionExpired": "Nag-expire na ang iyong session. Ikaw ay ma-logout."
"sessionExpired": "Nag-expire na ang iyong session. Ikaw ay ma-logout.",
"noAccount": "Wala pang account?",
"toLogin": "Pumunta sa login",
"loginFailed": "Nabigo ang pag-login. Suriin ang iyong credentials.",
"activationFailed": "Nabigo ang activation. Suriin ang link.",
"forgotPasswordDescription": "Ilagay ang iyong email address. Makakatanggap ka ng link para i-reset ang password.",
"sendResetLink": "Ipadala ang link",
"sending": "Ipinapadala...",
"resetEmailSent": "Kung may account sa email na ito, ipinadala na ang reset link. Suriin ang inbox (at spam folder).",
"resetRequestFailed": "Nabigo ang request. Subukan ulit.",
"resetPassword": "Magtakda ng bagong password",
"newPassword": "Bagong password",
"confirmPassword": "Kumpirmahin ang password",
"saveNewPassword": "I-save ang password",
"saving": "Sine-save...",
"passwordResetSuccess": "Matagumpay na nabago ang password. Maaari ka nang mag-login.",
"passwordsDoNotMatch": "Hindi magkatugma ang mga password.",
"passwordTooShort": "Ang password ay dapat hindi bababa sa 6 na character.",
"resetFailed": "Hindi nabago ang password. Maaaring nag-expire na ang link."
},
"settings": {
"title": "Mga setting",

View File

@@ -65,7 +65,25 @@
"rememberMe": "记住我",
"loginSuccess": "登录成功",
"logoutSuccess": "退出成功",
"sessionExpired": "您的会话已过期。您将被登出。"
"sessionExpired": "您的会话已过期。您将被登出。",
"noAccount": "还没有账号?",
"toLogin": "去登录",
"loginFailed": "登录失败。请检查您的凭据。",
"activationFailed": "激活失败。请检查链接。",
"forgotPasswordDescription": "输入您的电子邮件地址。您将收到重置密码的链接。",
"sendResetLink": "发送链接",
"sending": "发送中...",
"resetEmailSent": "如果该邮箱存在账号,已发送重置链接。请检查收件箱(和垃圾邮件文件夹)。",
"resetRequestFailed": "请求失败。请重试。",
"resetPassword": "设置新密码",
"newPassword": "新密码",
"confirmPassword": "确认密码",
"saveNewPassword": "保存密码",
"saving": "保存中...",
"passwordResetSuccess": "密码已成功更改。您现在可以使用新密码登录。",
"passwordsDoNotMatch": "密码不匹配。",
"passwordTooShort": "密码至少需要6个字符。",
"resetFailed": "无法更改密码。链接可能已过期。"
},
"settings": {
"title": "设置",

View File

@@ -2,6 +2,8 @@ import { createRouter, createWebHistory } from 'vue-router';
import Register from './views/Register.vue';
import Login from './views/Login.vue';
import Activate from './views/Activate.vue';
import ForgotPassword from './views/ForgotPassword.vue';
import ResetPassword from './views/ResetPassword.vue';
import Home from './views/Home.vue';
import CreateClub from './views/CreateClub.vue';
import ClubView from './views/ClubView.vue';
@@ -26,6 +28,8 @@ const routes = [
{ path: '/register', component: Register },
{ path: '/login', component: Login },
{ path: '/activate/:activationCode', component: Activate },
{ path: '/forgot-password', component: ForgotPassword },
{ path: '/reset-password/:token', component: ResetPassword },
{ path: '/', component: Home },
{ path: '/createclub', component: CreateClub },
{ path: '/showclub/:clubId', component: ClubView },

View File

@@ -0,0 +1,129 @@
<template>
<div class="forgot-password-container">
<h2>{{ $t('auth.forgotPassword') }}</h2>
<p class="description">{{ $t('auth.forgotPasswordDescription') }}</p>
<form v-if="!emailSent" @submit.prevent="submitRequest">
<input
v-model="email"
type="email"
:placeholder="$t('auth.email')"
required
:disabled="loading"
/>
<button type="submit" :disabled="loading">
{{ loading ? $t('auth.sending') : $t('auth.sendResetLink') }}
</button>
</form>
<div v-else class="success-message">
<p>{{ $t('auth.resetEmailSent') }}</p>
</div>
<div class="back-to-login">
<router-link to="/login"> {{ $t('auth.toLogin') }}</router-link>
</div>
<!-- Info Dialog -->
<InfoDialog
v-model="infoDialog.isOpen"
:title="infoDialog.title"
:message="infoDialog.message"
:details="infoDialog.details"
:type="infoDialog.type"
/>
</div>
</template>
<script>
import apiClient from '../apiClient.js';
import InfoDialog from '../components/InfoDialog.vue';
import { buildInfoConfig, safeErrorMessage } from '../utils/dialogUtils.js';
export default {
components: { InfoDialog },
data() {
return {
email: '',
loading: false,
emailSent: false,
infoDialog: {
isOpen: false,
title: '',
message: '',
details: '',
type: 'info'
}
};
},
methods: {
async showInfo(title, message, details = '', type = 'info') {
this.infoDialog = buildInfoConfig({ title, message, details, type });
},
async submitRequest() {
this.loading = true;
try {
await apiClient.post('/auth/forgot-password', { email: this.email });
this.emailSent = true;
} catch (error) {
const message = safeErrorMessage(error, this.$t('auth.resetRequestFailed'));
await this.showInfo(this.$t('messages.error'), message, '', 'error');
} finally {
this.loading = false;
}
}
}
};
</script>
<style scoped>
.forgot-password-container {
max-width: 400px;
margin: 2rem auto;
padding: 0 1rem;
}
.description {
color: #666;
margin-bottom: 1.5rem;
line-height: 1.5;
}
.success-message {
background-color: #e8f5e9;
border: 1px solid #4caf50;
border-radius: 6px;
padding: 1rem;
color: #2e7d32;
line-height: 1.5;
}
.back-to-login {
margin-top: 1.5rem;
text-align: center;
}
.back-to-login a {
color: #1976d2;
text-decoration: none;
}
.back-to-login a:hover {
text-decoration: underline;
}
form input {
width: 100%;
box-sizing: border-box;
margin-bottom: 1rem;
}
form button {
width: 100%;
}
form button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>

View File

@@ -6,6 +6,9 @@
<input v-model="password" type="password" :placeholder="$t('auth.password')" required />
<button type="submit">{{ $t('auth.login') }}</button>
</form>
<div class="forgot-password-link">
<router-link to="/forgot-password">{{ $t('auth.forgotPassword') }}</router-link>
</div>
<div class="register-link">
<p>{{ $t('auth.noAccount') }} <router-link to="/register">{{ $t('auth.register') }}</router-link></p>
</div>

View File

@@ -0,0 +1,144 @@
<template>
<div class="reset-password-container">
<h2>{{ $t('auth.resetPassword') }}</h2>
<form v-if="!resetDone" @submit.prevent="submitReset">
<input
v-model="newPassword"
type="password"
:placeholder="$t('auth.newPassword')"
required
minlength="6"
:disabled="loading"
/>
<input
v-model="confirmPassword"
type="password"
:placeholder="$t('auth.confirmPassword')"
required
minlength="6"
:disabled="loading"
/>
<button type="submit" :disabled="loading">
{{ loading ? $t('auth.saving') : $t('auth.saveNewPassword') }}
</button>
</form>
<div v-else class="success-message">
<p>{{ $t('auth.passwordResetSuccess') }}</p>
<router-link to="/login" class="login-button">{{ $t('auth.toLogin') }}</router-link>
</div>
<!-- Info Dialog -->
<InfoDialog
v-model="infoDialog.isOpen"
:title="infoDialog.title"
:message="infoDialog.message"
:details="infoDialog.details"
:type="infoDialog.type"
/>
</div>
</template>
<script>
import apiClient from '../apiClient.js';
import InfoDialog from '../components/InfoDialog.vue';
import { buildInfoConfig, safeErrorMessage } from '../utils/dialogUtils.js';
export default {
components: { InfoDialog },
data() {
return {
newPassword: '',
confirmPassword: '',
loading: false,
resetDone: false,
infoDialog: {
isOpen: false,
title: '',
message: '',
details: '',
type: 'info'
}
};
},
methods: {
async showInfo(title, message, details = '', type = 'info') {
this.infoDialog = buildInfoConfig({ title, message, details, type });
},
async submitReset() {
if (this.newPassword !== this.confirmPassword) {
await this.showInfo(this.$t('messages.error'), this.$t('auth.passwordsDoNotMatch'), '', 'error');
return;
}
if (this.newPassword.length < 6) {
await this.showInfo(this.$t('messages.error'), this.$t('auth.passwordTooShort'), '', 'error');
return;
}
this.loading = true;
try {
const token = this.$route.params.token;
await apiClient.post('/auth/reset-password', {
token,
password: this.newPassword
});
this.resetDone = true;
} catch (error) {
const message = safeErrorMessage(error, this.$t('auth.resetFailed'));
await this.showInfo(this.$t('messages.error'), message, '', 'error');
} finally {
this.loading = false;
}
}
}
};
</script>
<style scoped>
.reset-password-container {
max-width: 400px;
margin: 2rem auto;
padding: 0 1rem;
}
.success-message {
background-color: #e8f5e9;
border: 1px solid #4caf50;
border-radius: 6px;
padding: 1rem;
color: #2e7d32;
text-align: center;
line-height: 1.5;
}
.login-button {
display: inline-block;
margin-top: 1rem;
background-color: #4CAF50;
color: white;
padding: 0.6rem 1.5rem;
text-decoration: none;
border-radius: 4px;
}
.login-button:hover {
background-color: #43a047;
}
form input {
width: 100%;
box-sizing: border-box;
margin-bottom: 1rem;
}
form button {
width: 100%;
}
form button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>