Files
web-app-launcher/src/lib/server/services/authService.ts
T
alexei.dolgolyov e6b50fb4f1 feat(mvp): phase 8 - integration, testing & deployment
Fix all build/type/lint errors (zod 3.25 compat wrapper, Svelte 5 fixes),
write 115 unit tests across 10 test files, expand seed script with demo
data, update Docker config with migration on startup.
2026-03-24 22:09:17 +03:00

118 lines
3.1 KiB
TypeScript

import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { prisma } from '../prisma.js';
import { DEFAULTS } from '$lib/utils/constants.js';
import type { JwtPayload, TokenPair } from '$lib/types/auth.js';
const SALT_ROUNDS = 12;
function getJwtSecret(): string {
const secret = process.env.JWT_SECRET;
if (!secret) {
throw new Error('JWT_SECRET environment variable is not set');
}
return secret;
}
function getJwtExpiry(): string {
return process.env.JWT_EXPIRY || DEFAULTS.JWT_EXPIRY;
}
function getRefreshTokenExpiryDays(): number {
const envValue = process.env.REFRESH_TOKEN_EXPIRY;
if (envValue) {
const days = parseInt(envValue.replace('d', ''), 10);
if (!isNaN(days)) return days;
}
return DEFAULTS.REFRESH_TOKEN_EXPIRY_DAYS;
}
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
export function signAccessToken(payload: JwtPayload): string {
return jwt.sign(payload, getJwtSecret(), {
expiresIn: getJwtExpiry() as string & jwt.SignOptions['expiresIn']
});
}
export function verifyAccessToken(token: string): JwtPayload {
try {
const decoded = jwt.verify(token, getJwtSecret()) as JwtPayload & jwt.JwtPayload;
return {
userId: decoded.userId,
email: decoded.email,
role: decoded.role
};
} catch {
throw new Error('Invalid or expired access token');
}
}
export function generateRefreshToken(): string {
const bytes = new Uint8Array(48);
crypto.getRandomValues(bytes);
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
}
export function getRefreshTokenExpiry(): Date {
const days = getRefreshTokenExpiryDays();
const expiry = new Date();
expiry.setDate(expiry.getDate() + days);
return expiry;
}
export async function saveRefreshToken(userId: string, refreshToken: string): Promise<void> {
const hashedToken = await bcrypt.hash(refreshToken, SALT_ROUNDS);
await prisma.user.update({
where: { id: userId },
data: {
refreshToken: hashedToken,
refreshTokenExpiresAt: getRefreshTokenExpiry()
}
});
}
export async function validateRefreshToken(
userId: string,
refreshToken: string
): Promise<boolean> {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { refreshToken: true, refreshTokenExpiresAt: true }
});
if (!user?.refreshToken || !user.refreshTokenExpiresAt) {
return false;
}
if (new Date() > user.refreshTokenExpiresAt) {
return false;
}
return bcrypt.compare(refreshToken, user.refreshToken);
}
export async function revokeRefreshToken(userId: string): Promise<void> {
await prisma.user.update({
where: { id: userId },
data: {
refreshToken: null,
refreshTokenExpiresAt: null
}
});
}
export async function rotateTokens(userId: string, email: string, role: string): Promise<TokenPair> {
const accessToken = signAccessToken({ userId, email, role });
const refreshToken = generateRefreshToken();
await saveRefreshToken(userId, refreshToken);
return { accessToken, refreshToken };
}