Void-Genesis: JWT-Handling, Szenenwechsel & Lobby-UI
This commit is contained in:
@@ -146,7 +146,8 @@ export const login = async (req, res) => {
|
||||
}
|
||||
if (error.code === 'ERR_EMAIL_NOT_VERIFIED') {
|
||||
return res.status(403).json({
|
||||
error: 'ERR_EMAIL_NOT_VERIFIED'
|
||||
error: 'ERR_EMAIL_NOT_VERIFIED',
|
||||
email: error.email
|
||||
});
|
||||
}
|
||||
|
||||
@@ -179,7 +180,8 @@ export const refresh = async (req, res) => {
|
||||
// Das neue Access-Token an das Frontend zurück geben
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
accessToken: result.accessToken
|
||||
accessToken: result.accessToken,
|
||||
user: result.user
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@@ -198,4 +200,119 @@ export const refresh = async (req, res) => {
|
||||
error: 'ERR_INTERNAL_SERVER'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller-Methode für die Resend-OTP-Route
|
||||
* Wird aufgerufen, wenn der Nutzer im Frontend auf "Code erneut senden" klickt
|
||||
*/
|
||||
export const resendOTP = async (req, res) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({
|
||||
error: 'ERR_MISSING_FIELDS'
|
||||
});
|
||||
}
|
||||
|
||||
// Aufruf unseres Service
|
||||
await authService.resendOTP(email);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
// MSG_OTP_RESENT: Ein neuer Code wurde an deine E-Mail gesendet.
|
||||
message: 'MSG_OTP_RESENT'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
if (['ERR_USER_NOT_FOUND', 'ERR_ALREADY_VERIFIED', 'ERR_COOLDOWN_ACTIVE'].includes(error.code)) {
|
||||
const statusCode = error.code === 'ERR_COOLDOWN_ACTIVE' ? 429 : 400;
|
||||
return res.status(error.code).json({
|
||||
error: error.code
|
||||
});
|
||||
}
|
||||
|
||||
console.error('[AuthController] Fehler beim erneuten Senden des OTP:', error);
|
||||
return res.status(500).json({
|
||||
error: 'ERR_INTERNAL_SERVER'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Controller-Methode für die Passwort vergessen anfrage
|
||||
*/
|
||||
export const requestPasswordReset = async (req, res) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({
|
||||
error: 'ERR_MISSING_FIELDS'
|
||||
});
|
||||
}
|
||||
|
||||
// Auch wenn die E-Mail nicht existiert, geben wir oft ein Success zurück,
|
||||
// damit Angreifer nicht testen können, welche E-Mails registriert sind.
|
||||
// In unserem Service werfen wir aber ERR_USER_NOT_FOUND. Wir fangen das unten ab.
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'MSG_RESET_EMAIL_SENT' // Muss später ins i18n
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// Wenn der Nutzer nicht existiert, tun wir für das Frontend trotzdem so, als wäre
|
||||
// die E-Mail rausgegangen (Sicherheitsbest-Practice).
|
||||
if (error.code === 'ERR_USER_NOT_FOUND') {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'MSG_RESET_EMAIL_SENT'
|
||||
});
|
||||
}
|
||||
|
||||
if (error.code === 'ERR_COOLDOWN_ACTIVE') {
|
||||
return res.status(429).json({
|
||||
error: error.code
|
||||
});
|
||||
}
|
||||
|
||||
console.error('[AuthController] Fehler bei requestPasswordReset:', error);
|
||||
return res.status(500).json({ error: 'ERR_INTERNAL_SERVER' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Controller-Methode für das finale Setzen des neuen Passworts.
|
||||
*/
|
||||
export const resetPassword = async (req, res) => {
|
||||
try {
|
||||
const { email, otpCode, newPassword } = req.body;
|
||||
|
||||
// Basis-Validierung
|
||||
if (!email || !otpCode || !newPassword) {
|
||||
return res.status(400).json({ error: 'ERR_MISSING_FIELDS' });
|
||||
}
|
||||
|
||||
// Passwort-Richtlinie prüfen (wie bei der Registrierung)
|
||||
if (newPassword.length < 8) {
|
||||
return res.status(400).json({ error: 'ERR_PASSWORD_TOO_SHORT' });
|
||||
}
|
||||
|
||||
// Service aufrufen
|
||||
await authService.resetPassword(email, otpCode, newPassword);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'MSG_PASSWORD_RESET_SUCCESS' // Muss später ins i18n
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
if (['ERR_USER_NOT_FOUND', 'ERR_INVALID_OTP', 'ERR_OTP_EXPIRED'].includes(error.code)) {
|
||||
return res.status(400).json({ error: error.code });
|
||||
}
|
||||
|
||||
console.error('[AuthController] Fehler bei resetPassword:', error);
|
||||
return res.status(500).json({ error: 'ERR_INTERNAL_SERVER' });
|
||||
}
|
||||
};
|
||||
@@ -7,5 +7,8 @@ router.post('/register', authController.register);
|
||||
router.post('/verify-email', authController.verifyEmail);
|
||||
router.post('/login', authController.login);
|
||||
router.post('/refresh', authController.refresh);
|
||||
router.post('/resend-otp', authController.resendOTP);
|
||||
router.post('/forgot-password', authController.requestPasswordReset);
|
||||
router.post('/reset-password', authController.resetPassword);
|
||||
|
||||
export default router;
|
||||
@@ -202,7 +202,7 @@ export const loginUser = async (identifier, password) => {
|
||||
try {
|
||||
// Nutzer suchen
|
||||
const accountResult = await client.query(`
|
||||
SELECT id, username, email, password_hash, is_email_verified, is_admin
|
||||
SELECT id, username, email, password_hash, is_email_verified, is_admin, res_premium
|
||||
FROM lobby_accounts
|
||||
WHERE LOWER(username) = LOWER($1) OR email = LOWER($1)`,
|
||||
[identifier]
|
||||
@@ -223,6 +223,7 @@ export const loginUser = async (identifier, password) => {
|
||||
// ERR_EMAIL_NOT_VERIFIED: Bitte bestätige zuerst deine E-Mail-Addresse
|
||||
const error = new Error('Email not verified');
|
||||
error.code = 'ERR_EMAIL_NOT_VERIFIED';
|
||||
error.email = account.email;
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -267,7 +268,9 @@ export const loginUser = async (identifier, password) => {
|
||||
refreshToken,
|
||||
user: {
|
||||
id: account.id,
|
||||
username: account.username
|
||||
username: account.username,
|
||||
email: account.email,
|
||||
res_premium: account.res_premium
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
@@ -308,7 +311,7 @@ export const refreshAccessToken = async (refreshToken) => {
|
||||
// Das behebt das "Stale Data" Problem. Falls der Admin Status geändert wurde,
|
||||
// steht er im neuen Token sofort korrekt drin, ohne die DB bei jedem API.Call zu belasten
|
||||
const accountResult = await client.query(
|
||||
'SELECT id, username, is_admin FROM lobby_accounts WHERE id = $1',
|
||||
'SELECT id, username, email, is_admin, res_premium FROM lobby_accounts WHERE id = $1',
|
||||
[accountId]
|
||||
);
|
||||
|
||||
@@ -333,7 +336,15 @@ export const refreshAccessToken = async (refreshToken) => {
|
||||
{ expiresIn: '15m' }
|
||||
);
|
||||
|
||||
return { accessToken };
|
||||
return {
|
||||
accessToken,
|
||||
user: {
|
||||
id: account.id,
|
||||
username: account.username,
|
||||
email: account.email,
|
||||
res_premium: res_premium
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Abfangen von JWT-spezifischen Fehlern (z.B. Token abgelaufen oder Signatur falsch)
|
||||
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError' || error.name === 'SyntaxError') {
|
||||
@@ -347,4 +358,249 @@ export const refreshAccessToken = async (refreshToken) => {
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet einen neuen OTP-Code an den Nutzer, falls der alte abgelaufen ist oder nicht ankam.
|
||||
* Vorherige, ungenutzte Codes werdeb zur Sicherheit invalidiert (gelöscht)
|
||||
*
|
||||
* @param {string} email - Die E-Mail Addresse des Nutzers
|
||||
* @returns {boolean} - true, wenn die E-Mail erfolgreich versendet wurde.
|
||||
*/
|
||||
export const resendOTP = async (email) => {
|
||||
const client = await pool.connect();
|
||||
const lowerCaseEmail = email.toLowerCase();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Prüfen, ob der Nutzer überhaupt existiert
|
||||
const accountResult = await client.query(
|
||||
'SELECT id, username, is_email_verified FROM lobby_accounts WHERE email = $1',
|
||||
[lowerCaseEmail]
|
||||
);
|
||||
|
||||
if (accountResult.rows.length === 0) {
|
||||
// Aus Sicherheitsgründen verraten wir dem Frontend nicht zwingend,
|
||||
// dass die E-Mail nicht existieren, aber für unser internes Error-Handling
|
||||
// werfen wir einen spezifischen Fehler.
|
||||
const error = new Error('User nor found');
|
||||
error.code = 'ERR:USER_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const account = accountResult.rows[0];
|
||||
|
||||
// Prüfen, ob der Account nicht vielleicht schon verifiziert ist
|
||||
// Wenn ja, macht ein neuer Code keinen Sinn.
|
||||
if (account.is_email_verified) {
|
||||
const error = new Error('Already verified');
|
||||
error.code = 'ERR_ALREADY_VERIFIED';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const lastOtpResult = await client.query(`
|
||||
SELECT created_at FROM auth_otps WHERE account_id = $1 AND type = 'register' ORDER BY created_at DESC LIMIT 1`,
|
||||
[account.id]
|
||||
);
|
||||
|
||||
if (lastOtpResult.rows.length > 0) {
|
||||
const lastCreatedTime = new Date(lastOtpResult.rows[0].created_at).getTime();
|
||||
const currentTime = new Date().getTime();
|
||||
const timeDifference = currentTime - lastCreatedTime;
|
||||
|
||||
if (timeDifference < 120000) {
|
||||
const error = new Error('Cooldown active');
|
||||
error.code = 'ERR_COOLDOWN_ACTIVE';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Zur Sichherheit alle alten, noch aktiven Registierungs-OTPs dieses Nutzers löschen.
|
||||
// So verhindern wir, dass alte Codes in Postfächern noch funktionieren und minimieren
|
||||
// Spam-Risiken.
|
||||
await client.query(
|
||||
`DELETE FROM auth_otps WHERE account_id = $1 AND type = 'register'`,
|
||||
[account.id]
|
||||
);
|
||||
|
||||
// Einen frischen, 6-stelligen Cide generieren
|
||||
const newOtpCode = generateOTP();
|
||||
|
||||
// Den neuen Code in die Datenbank eintragen
|
||||
await client.query(`
|
||||
INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
|
||||
VALUES ($1, $2, 'register', NOW() + INTERVAL '15 minutes')`,
|
||||
[account.id, newOtpCode]
|
||||
);
|
||||
|
||||
// 6. Die neue E-Mail zusammenbauen und versenden
|
||||
const mailHtml = `
|
||||
<h2>Hallo ${account.username}, hier ist dein neuer Code!</h2>
|
||||
<p>Du hast einen neuen Verifizierungscode für Void-Genesis angefordert:</p>
|
||||
<h1 style="color: #00f0ff; letter-spacing: 5px;">${newOtpCode}</h1>
|
||||
<p>Dieser Code ist ab jetzt wieder 15 Minuten lang gültig.</p>
|
||||
`;
|
||||
|
||||
await sendEmail(lowerCaseEmail, account.username, 'Void-Genesis - Dein neue Code', mailHtml);
|
||||
|
||||
await client.query('COMMIT');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Startet den Prozess zum Zurücksetzten des Passworts.
|
||||
* Generiert einen speziellen OTP-Code und sendet ihn per E-Mail.
|
||||
*
|
||||
* @param {string} email - Die Email-Adresse des Nutzers
|
||||
* @returns {boolean} - true, wenn die E-Mail erfolgreich versendet wurde.
|
||||
*/
|
||||
export const requestPasswordReset = async (email) => {
|
||||
const client = await pool.connect();
|
||||
const lowerCaseEmail = email.toLowerCase();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
const accountResult = await client.query(
|
||||
'SELECT id, username FROM lobby_accounts WHERE email = $1',
|
||||
[lowerCaseEmail]
|
||||
);
|
||||
|
||||
if (accountResult.rows.length === 0) {
|
||||
const error = new Error('User not found');
|
||||
error.code = 'ERR_USER_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const lastOtpResult = await client.query(`
|
||||
SELECT created_at FROM auth_otps WHERE account_id = $1 AND type = 'password_reset' ORDER BY created_at DESC LIMIT 1`,
|
||||
[account.id]
|
||||
);
|
||||
|
||||
if (lastOtpResult.rows.length > 0) {
|
||||
const lastCreatedTime = new Date(lastOtpResult.rows[0].created_at).getTime();
|
||||
const currentTime = new Date().getTime();
|
||||
const timeDifference = currentTime - lastCreatedTime;
|
||||
|
||||
if (timeDifference < 120000) {
|
||||
const error = new Error('Cooldown active');
|
||||
error.code = 'ERR_COOLDOWN_ACTIVE';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const account = accountResult.rows[0];
|
||||
|
||||
await client.query(
|
||||
`DELETE FROM auth_otps WHERE account_id = $1 AND type = 'password_reset'`,
|
||||
[account.id]
|
||||
);
|
||||
|
||||
const resetCode = generateOTP();
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
|
||||
VALUES ($1, $2m 'password_reset' NOW() + INTERVAL '15 minutes')`,
|
||||
[account.id, resetCode]
|
||||
);
|
||||
|
||||
const mailHtml = `
|
||||
<h2>Hallo ${account.username},</h2>
|
||||
<p>Du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.</p>
|
||||
<p>Dein Code lautet:</p>
|
||||
<h1 style="color: #00f0ff; letter-spacing: 5px;">${resetCode}</h1>
|
||||
<p>Dieser Code ist 15 Minuten lang gültig. Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren.</p>
|
||||
`;
|
||||
|
||||
await sendEmail(lowerCaseEmail, account.username, 'Void-Genesis - Passwort zurücksetzen', mailHtml);
|
||||
|
||||
await client.query('COMMIT');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Setzt das neue Passwort, soffern der OTP-Code korrekt und nicht abgelaufen ist
|
||||
*
|
||||
* @param {string} email - Die E-Mail Addresse des Nutzers
|
||||
* @param {string} otpCode - Der 6-stellige Code aus der E-Mail
|
||||
* @param {string} newPassword - das neue Klartext passwort
|
||||
* @returns {boolean} - true wenn das Passwort erfolgreich geändert wurde
|
||||
*/
|
||||
export const resetPassword = async (email, otpCode, newPassword) => {
|
||||
const client = await pool.connect();
|
||||
const lowerCaseEmail = email.toLowerCase();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
const accountResult = await client.query(
|
||||
'SELECT id FROM lobby_accounts WHERE email = $1'
|
||||
[lowerCaseEmail]
|
||||
);
|
||||
|
||||
if (accountResult.rows.length === 0) {
|
||||
const error = new Error('User not found');
|
||||
error.code = 'ERR_USER_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const account = accountResult.rows[0];
|
||||
|
||||
const otpResult = await client.query(`
|
||||
SELECT id, expires_at FROM auth_otps
|
||||
WHERE account_id = $1, otp_code = $2, AND type = 'password_reset'`,
|
||||
[account.id, otpCo0de]
|
||||
);
|
||||
|
||||
if (otpResult.rows.length === 0) {
|
||||
const error = new Error('Invalid OTP');
|
||||
error.code = 'ERR_INVALID_OTP';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const otpRecord = otpResult.rows[0];
|
||||
|
||||
if (new Date() > new Date(otpRecord.expires_at)) {
|
||||
const error = new Error('OTP expired');
|
||||
error.code = 'ERR_OTP_EXPIRED';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const passwordHash = await argon2.hash(newPassword);
|
||||
|
||||
await client.query(
|
||||
'UPDATE lobby_accounts SET password_hash = $1 WHERE id = $2',
|
||||
[passwordHash, account.id]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
'DELETE FROM refresh_tokens WHERE account_id = $1',
|
||||
[account.id]
|
||||
);
|
||||
|
||||
await client.query('COMMIT');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user