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 <noreply@anthropic.com>
This commit is contained in:
richardtekula
2026-01-23 07:21:58 +01:00
parent d85f6761cf
commit dd15be93a9
3 changed files with 78 additions and 7 deletions

View File

@@ -6,13 +6,15 @@ import {
logLogin, logLogin,
logLogout, logLogout,
} from '../services/audit.service.js'; } 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 * KROK 1: Login s temporary password
* POST /api/auth/login * POST /api/auth/login
*/ */
export const login = async (req, res, next) => { 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 ipAddress = req.ip || req.connection.remoteAddress;
const userAgent = req.headers['user-agent']; const userAgent = req.headers['user-agent'];
@@ -37,12 +39,15 @@ export const login = async (req, res, next) => {
maxAge: 60 * 60 * 1000, // 1 hodina maxAge: 60 * 60 * 1000, // 1 hodina
}); });
// Refresh token iba ak user chce zostať prihlásený
if (rememberMe) {
res.cookie('refreshToken', result.tokens.refreshToken, { res.cookie('refreshToken', result.tokens.refreshToken, {
httpOnly: true, httpOnly: true,
secure: isProduction, secure: isProduction,
sameSite: isProduction ? 'strict' : 'lax', sameSite: isProduction ? 'strict' : 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 dní maxAge: 7 * 24 * 60 * 60 * 1000, // 7 dní
}); });
}
res.status(200).json({ res.status(200).json({
success: true, 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 * KROK 2: Nastavenie nového hesla
* POST /api/auth/set-password * POST /api/auth/set-password

View File

@@ -25,6 +25,9 @@ router.post(
authController.login authController.login
); );
// Refresh access token (public - uses refresh token from cookie)
router.post('/refresh', authController.refreshToken);
/** /**
* Protected routes (vyžadujú autentifikáciu) * Protected routes (vyžadujú autentifikáciu)
*/ */

View File

@@ -15,6 +15,7 @@ export const loginSchema = z.object({
invalid_type_error: 'Heslo musí byť text', invalid_type_error: 'Heslo musí byť text',
}) })
.min(1, 'Heslo nemôže byť prázdne'), .min(1, 'Heslo nemôže byť prázdne'),
rememberMe: z.boolean().optional().default(false),
}); });
// Set new password schema (krok 2) // Set new password schema (krok 2)