mc-manager/src/lib/auth.ts
2026-02-07 15:33:08 -08:00

152 lines
4.9 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import jwt from 'jsonwebtoken'
import bcrypt from 'bcryptjs'
import crypto from 'crypto'
import connectToDatabase from './mongodb'
import { User } from './models'
import type { AuthUser, SessionPayload } from '@/types/user'
// ─── Password Hashing ────────────────────────────────────────────
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 12)
}
export async function verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
return bcrypt.compare(password, hashedPassword)
}
// ─── JWT Tokens ──────────────────────────────────────────────────
export function generateAccessToken(payload: SessionPayload): string {
return jwt.sign(payload, process.env.JWT_ACCESS_SECRET!, { expiresIn: '1h' })
}
export function generateRefreshToken(payload: SessionPayload): string {
return jwt.sign(payload, process.env.JWT_REFRESH_SECRET!, { expiresIn: '7d' })
}
export function verifyAccessToken(token: string): SessionPayload | null {
try {
return jwt.verify(token, process.env.JWT_ACCESS_SECRET!) as SessionPayload
} catch {
return null
}
}
export function verifyRefreshToken(token: string): SessionPayload | null {
try {
return jwt.verify(token, process.env.JWT_REFRESH_SECRET!) as SessionPayload
} catch {
return null
}
}
// ─── 2FA Code ────────────────────────────────────────────────────
export function generate2FACode(): string {
return crypto.randomInt(100000, 999999).toString()
}
export function hash2FACode(code: string): string {
const hmac = crypto.createHmac('sha256', process.env.TWO_FACTOR_SECRET!)
hmac.update(code)
return hmac.digest('hex')
}
export function verify2FACode(inputCode: string, hashedCode: string): boolean {
const inputHash = hash2FACode(inputCode.trim())
try {
return crypto.timingSafeEqual(
Buffer.from(inputHash, 'hex'),
Buffer.from(hashedCode, 'hex')
)
} catch {
return false
}
}
// ─── Session Validation ──────────────────────────────────────────
export async function validateSession(request: NextRequest): Promise<AuthUser | null> {
const token = request.cookies.get('session-token')?.value
if (!token) return null
const payload = verifyAccessToken(token)
if (!payload) return null
try {
await connectToDatabase()
const user = await User.findById(payload.userId)
.populate('roles')
.lean()
if (!user || user.status !== 'active') return null
const permissions = new Set<string>()
const roleNames: string[] = []
if (Array.isArray(user.roles)) {
for (const role of user.roles) {
if (typeof role === 'object' && role !== null) {
const r = role as unknown as { name: string; permissions: { resource: string; actions: string[] }[] }
roleNames.push(r.name)
for (const perm of r.permissions) {
for (const action of perm.actions) {
permissions.add(`${perm.resource}:${action}`)
}
}
}
}
}
return {
_id: user._id.toString(),
id: user._id.toString(),
username: user.username,
email: user.email,
permissions: Array.from(permissions),
roles: roleNames,
}
} catch {
return null
}
}
// ─── Cookie Helpers ──────────────────────────────────────────────
export function setAuthCookies(response: NextResponse, accessToken: string, refreshToken: string): void {
const isProduction = process.env.NODE_ENV === 'production'
response.cookies.set('session-token', accessToken, {
httpOnly: true,
secure: isProduction,
sameSite: 'strict',
maxAge: 3600, // 1 hour
path: '/',
})
response.cookies.set('refresh-token', refreshToken, {
httpOnly: true,
secure: isProduction,
sameSite: 'strict',
maxAge: 7 * 24 * 3600, // 7 days
path: '/',
})
}
export function clearAuthCookies(response: NextResponse): void {
response.cookies.set('session-token', '', { maxAge: 0, path: '/' })
response.cookies.set('refresh-token', '', { maxAge: 0, path: '/' })
}
// ─── Permission Check ────────────────────────────────────────────
export function hasPermission(user: AuthUser, permission: string): boolean {
if (user.permissions.includes('*:*')) return true
return user.permissions.includes(permission)
}