Refresh-Token-System: kurzlebiger Access-Token (15m) + rotierender Refresh-Token (90d)
All checks were successful
Deploy GSM / deploy (push) Successful in 29s

Bisher: 7d JWT im localStorage. Jeder 403 loggte den User aus — auch "Insufficient
permissions"-Responses fuer Background-Requests — wodurch User sich effektiv taeglich
neu einloggen mussten.

Backend:
- Neue refresh_tokens Tabelle (SHA256-gehasht, mit Rotation + Revoke)
- /auth/refresh tauscht Refresh-Token gegen frischen Access-Token (rotiert beide)
- /auth/logout revoked den Refresh-Token
- Access-Token-TTL auf 15m; Guest-Refresh 7d, Discord-Refresh 90d
- Daily cleanup-Job fuer abgelaufene/revoked Tokens

Frontend:
- api.js refreshed bei 401 automatisch (single-flight) und retryt die Anfrage
- 403 wird nicht mehr als Session-Ablauf behandelt
- Discord-Callback + Guest-Login liefern beide Tokens
- App.jsx synced React-State per gsm_token_refreshed CustomEvent

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 11:21:38 +02:00
parent dfbcc2da82
commit 7bc93f3b70
7 changed files with 328 additions and 55 deletions

View File

@@ -0,0 +1,61 @@
import crypto from 'crypto';
import { db } from '../db/init.js';
// Refresh token lifetime
const DISCORD_REFRESH_TTL_DAYS = 90;
const GUEST_REFRESH_TTL_DAYS = 7;
function hashToken(token) {
return crypto.createHash('sha256').update(token).digest('hex');
}
function generateToken() {
return crypto.randomBytes(48).toString('base64url');
}
function ttlDaysFor(userType) {
return userType === 'guest' ? GUEST_REFRESH_TTL_DAYS : DISCORD_REFRESH_TTL_DAYS;
}
export function createRefreshToken({ userType, discordUserId = null, guestId = null, guestUsername = null }) {
const token = generateToken();
const tokenHash = hashToken(token);
const expiresAt = new Date(Date.now() + ttlDaysFor(userType) * 24 * 60 * 60 * 1000).toISOString();
db.prepare(`
INSERT INTO refresh_tokens (token_hash, user_type, discord_user_id, guest_id, guest_username, expires_at)
VALUES (?, ?, ?, ?, ?, ?)
`).run(tokenHash, userType, discordUserId, guestId, guestUsername, expiresAt);
return token;
}
export function verifyRefreshToken(token) {
if (!token) return null;
const tokenHash = hashToken(token);
const row = db.prepare(`
SELECT * FROM refresh_tokens
WHERE token_hash = ?
AND revoked = 0
AND expires_at > CURRENT_TIMESTAMP
`).get(tokenHash);
return row || null;
}
export function revokeRefreshToken(token) {
if (!token) return;
const tokenHash = hashToken(token);
db.prepare('UPDATE refresh_tokens SET revoked = 1 WHERE token_hash = ?').run(tokenHash);
}
export function revokeRefreshTokenById(id) {
db.prepare('UPDATE refresh_tokens SET revoked = 1 WHERE id = ?').run(id);
}
export function touchRefreshToken(id) {
db.prepare('UPDATE refresh_tokens SET last_used_at = CURRENT_TIMESTAMP WHERE id = ?').run(id);
}
export function revokeAllForDiscordUser(discordUserId) {
db.prepare('UPDATE refresh_tokens SET revoked = 1 WHERE discord_user_id = ?').run(discordUserId);
}