From dd15be93a9336098c6360bfc26876c74a0c515be Mon Sep 17 00:00:00 2001 From: richardtekula Date: Fri, 23 Jan 2026 07:21:58 +0100 Subject: [PATCH] feat: Add refresh token endpoint and remember me support - Add POST /auth/refresh endpoint for token renewal - Only set refresh token cookie when rememberMe is true - Add rememberMe field to login validator schema Co-Authored-By: Claude Opus 4.5 --- src/controllers/auth.controller.js | 81 +++++++++++++++++++++++++++--- src/routes/auth.routes.js | 3 ++ src/validators/auth.validators.js | 1 + 3 files changed, 78 insertions(+), 7 deletions(-) diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 56ab88a..409577d 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -6,13 +6,15 @@ import { logLogin, logLogout, } from '../services/audit.service.js'; +import { verifyRefreshToken, generateAccessToken } from '../utils/jwt.js'; +import { getUserById } from '../services/auth.service.js'; /** * KROK 1: Login s temporary password * POST /api/auth/login */ export const login = async (req, res, next) => { - const { username, password } = req.body; + const { username, password, rememberMe } = req.body; const ipAddress = req.ip || req.connection.remoteAddress; const userAgent = req.headers['user-agent']; @@ -37,12 +39,15 @@ export const login = async (req, res, next) => { maxAge: 60 * 60 * 1000, // 1 hodina }); - res.cookie('refreshToken', result.tokens.refreshToken, { - httpOnly: true, - secure: isProduction, - sameSite: isProduction ? 'strict' : 'lax', - maxAge: 7 * 24 * 60 * 60 * 1000, // 7 dní - }); + // Refresh token iba ak user chce zostať prihlásený + if (rememberMe) { + res.cookie('refreshToken', result.tokens.refreshToken, { + httpOnly: true, + secure: isProduction, + sameSite: isProduction ? 'strict' : 'lax', + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 dní + }); + } res.status(200).json({ success: true, @@ -62,6 +67,68 @@ export const login = async (req, res, next) => { } }; +/** + * Refresh access token using refresh token + * POST /api/auth/refresh + */ +export const refreshToken = async (req, res, next) => { + try { + const token = req.cookies?.refreshToken; + + if (!token) { + return res.status(401).json({ + success: false, + error: { + message: 'Refresh token nie je k dispozícii', + statusCode: 401, + }, + }); + } + + // Verify refresh token + const decoded = verifyRefreshToken(token); + + // Get user from DB + const user = await getUserById(decoded.id); + + // Generate new access token + const newAccessToken = generateAccessToken({ + id: user.id, + username: user.username, + role: user.role, + }); + + // Set new access token cookie + const isProduction = process.env.NODE_ENV === 'production'; + res.cookie('accessToken', newAccessToken, { + httpOnly: true, + secure: isProduction, + sameSite: isProduction ? 'strict' : 'lax', + maxAge: 60 * 60 * 1000, // 1 hodina + }); + + res.status(200).json({ + success: true, + data: { + accessToken: newAccessToken, + }, + message: 'Token obnovený', + }); + } catch (error) { + // Clear invalid cookies + res.clearCookie('accessToken'); + res.clearCookie('refreshToken'); + + return res.status(401).json({ + success: false, + error: { + message: 'Refresh token expiroval alebo je neplatný', + statusCode: 401, + }, + }); + } +}; + /** * KROK 2: Nastavenie nového hesla * POST /api/auth/set-password diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js index 54e3b00..6a7d31b 100644 --- a/src/routes/auth.routes.js +++ b/src/routes/auth.routes.js @@ -25,6 +25,9 @@ router.post( authController.login ); +// Refresh access token (public - uses refresh token from cookie) +router.post('/refresh', authController.refreshToken); + /** * Protected routes (vyžadujú autentifikáciu) */ diff --git a/src/validators/auth.validators.js b/src/validators/auth.validators.js index bcc5173..5e3378e 100644 --- a/src/validators/auth.validators.js +++ b/src/validators/auth.validators.js @@ -15,6 +15,7 @@ export const loginSchema = z.object({ invalid_type_error: 'Heslo musí byť text', }) .min(1, 'Heslo nemôže byť prázdne'), + rememberMe: z.boolean().optional().default(false), }); // Set new password schema (krok 2)