/
foult080
/
college-schedule-app
Обзор
Документация
Войти
/
foult080
/
college-schedule-app
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/controllers/auth.controller.ts
254 строки
7 KB
Foult080
feat(kimi): big update from kimi. Fix voulnerabilities and css styles
07 июл 2026, 07:07
07 июл 2026, 07:07
e07f0d7
Код
Авторство
О чём код?
import { NextFunction, Request, Response } from 'express'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import { generateTokens, verifyRefreshToken } from '../utils/token.utils.js'; import { DatabaseHelper } from '../utils/database.js'; import { storeRefreshToken, verifyRefreshTokenInDb, revokeRefreshToken, revokeAllUserTokens, } from '../utils/refresh-token.service.js'; import { setAuthCookies, clearAuthCookies, REFRESH_TOKEN_COOKIE, } from '../utils/cookie.utils.js'; import { ForbiddenError, UnauthorizedError } from '../utils/http-errors.js'; import { RegisterBody, LoginBody } from '../schemas/validation.schemas.js'; import { IAuthRequest, ITokenPayload } from '../types/types.js'; const BCRYPT_SALT_ROUNDS = 12; interface IUserWithPassword extends ITokenPayload { password?: string; } /** * Parse JWT expiry (e.g. '7d', '15m') into milliseconds */ function parseJwtExpiry(expiresIn: string): number { const match = expiresIn.match(/^(\d+)(s|m|h|d)$/); if (!match) return 7 * 24 * 60 * 60 * 1000; // fallback 7d const value = parseInt(match[1], 10); const unit = match[2]; switch (unit) { case 's': return value * 1000; case 'm': return value * 60 * 1000; case 'h': return value * 60 * 60 * 1000; case 'd': return value * 24 * 60 * 60 * 1000; default: return 7 * 24 * 60 * 60 * 1000; } } /** * Register new user * POST /auth/register */ export const register = async (req: Request, res: Response, next: NextFunction): Promise<void> => { try { const { email, password, firstName, lastName }: RegisterBody = req.body; const normalizedEmail = email.toLowerCase().trim(); // Check user exists in database const userExists = ( await DatabaseHelper.select<ITokenPayload>('users', ['email'], { email: normalizedEmail }) ).rows[0]; if (userExists) throw new ForbiddenError(`User with ${normalizedEmail} already exists`); // Hash password const hashedPassword = await bcrypt.hash(password, BCRYPT_SALT_ROUNDS); // Save user to database const newUserData = { email: normalizedEmail, password: hashedPassword, first_name: firstName, last_name: lastName, }; const result = await DatabaseHelper.insert<ITokenPayload>( 'users', newUserData, ['id', 'email', 'first_name', 'last_name', 'role', 'is_active'], ); const newUser = result.rows[0]; // Generate tokens const tokens = generateTokens(newUser); // Store refresh token for blacklist const refreshTokenExpiresMs = parseJwtExpiry(process.env.JWT_REFRESH_EXPIRES_IN || '7d'); await storeRefreshToken(tokens.refreshToken, newUser.id, new Date(Date.now() + refreshTokenExpiresMs)); // Set HTTP-only cookies and return user info setAuthCookies(res, tokens.accessToken, tokens.refreshToken); res.status(201).json({ user: newUser }); } catch (error) { next(error); } }; /** * User login * POST /auth/login */ export const login = async (req: Request, res: Response, next: NextFunction): Promise<void> => { try { const { email, password }: LoginBody = req.body; const normalizedEmail = email.toLowerCase().trim(); // Find user in database const user = ( await DatabaseHelper.select<IUserWithPassword>( 'users', ['id', 'password', 'email', 'first_name', 'last_name', 'role', 'is_active'], { email: normalizedEmail }, ) ).rows[0]; if (!user || !user.password) throw new UnauthorizedError('Invalid email or password'); // Check if account is active if (!user.is_active) throw new UnauthorizedError('Account is deactivated'); // Check password const checkPasswords = await bcrypt.compare(password, user.password); if (!checkPasswords) throw new UnauthorizedError('Invalid email or password'); // Revoke all previous tokens for security (fresh login) await revokeAllUserTokens(user.id); delete user.password; const tokenUser = user as ITokenPayload; // Generate tokens const tokens = generateTokens(tokenUser); // Store refresh token for blacklist const refreshTokenExpiresMs = parseJwtExpiry(process.env.JWT_REFRESH_EXPIRES_IN || '7d'); await storeRefreshToken(tokens.refreshToken, tokenUser.id, new Date(Date.now() + refreshTokenExpiresMs)); // Set HTTP-only cookies and return user info setAuthCookies(res, tokens.accessToken, tokens.refreshToken); res.json({ user: tokenUser }); } catch (error) { next(error); } }; /** * Refresh access and refresh tokens (with rotation) * POST /auth/refresh */ export const refreshToken = async (req: Request, res: Response, next: NextFunction): Promise<void> => { try { const oldRefreshToken = req.cookies?.[REFRESH_TOKEN_COOKIE] as string | undefined; if (!oldRefreshToken) { throw new UnauthorizedError('Refresh token is required'); } // Verify JWT signature const payload = verifyRefreshToken(oldRefreshToken); // Check token in database (blacklist check) const userId = await verifyRefreshTokenInDb(oldRefreshToken); if (!userId) { // Token not found or revoked — revoke the entire chain as a security measure const { id } = payload as ITokenPayload; await revokeAllUserTokens(id); throw new UnauthorizedError('Invalid or revoked refresh token'); } // Token rotation: revoke old token before issuing new one await revokeRefreshToken(oldRefreshToken); const { iat: _iat, exp: _exp, jti: _jti, ...userData } = payload as ITokenPayload & { jti?: string; exp?: number; iat?: number; }; // Generate new pair of tokens const tokens = generateTokens(userData); // Store new refresh token const refreshTokenExpiresMs = parseJwtExpiry(process.env.JWT_REFRESH_EXPIRES_IN || '7d'); await storeRefreshToken(tokens.refreshToken, userId, new Date(Date.now() + refreshTokenExpiresMs)); // Set new cookies setAuthCookies(res, tokens.accessToken, tokens.refreshToken); res.json({ success: true }); } catch (error) { if (error instanceof jwt.JsonWebTokenError) { switch (error.name) { case 'TokenExpiredError': return next(new UnauthorizedError('Refresh token expired')); default: return next(new UnauthorizedError('Invalid refresh token')); } } return next(error); } }; /** * Get current user information from database * GET /auth/me */ export const getMe = async (req: IAuthRequest, res: Response, next: NextFunction): Promise<void> => { try { const { user } = req; if (!user?.id) { throw new UnauthorizedError('User not authenticated'); } // Fetch fresh user data from database const result = await DatabaseHelper.select<ITokenPayload>( 'users', ['id', 'email', 'first_name', 'last_name', 'role', 'is_active'], { id: user.id }, ); const dbUser = result.rows[0]; if (!dbUser) { throw new UnauthorizedError('User not found'); } res.json({ user: dbUser }); } catch (error) { next(error); } }; /** * User logout (revoke refresh token) * POST /auth/logout */ export const logout = async (req: Request, res: Response, next: NextFunction): Promise<void> => { try { const tokenToRevoke = req.cookies?.[REFRESH_TOKEN_COOKIE] as string | undefined; // Revoke the refresh token if present if (tokenToRevoke) { await revokeRefreshToken(tokenToRevoke); } // Clear auth cookies clearAuthCookies(res); res.status(204).send(); } catch (error) { next(error); } };