f1b1aa5975
Define full Prisma schema (10 models), run initial migration, build core services (auth, user, group, app, board, permission), Zod validators, type definitions, API response envelope, constants, and seed script.
118 lines
3.0 KiB
TypeScript
118 lines
3.0 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()
|
|
});
|
|
}
|
|
|
|
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 };
|
|
}
|