mirror of
https://github.com/rmoren97/mc-manager.git
synced 2026-02-10 17:40:30 -08:00
152 lines
4.9 KiB
TypeScript
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)
|
|
}
|