274 lines
5.7 KiB
TypeScript
274 lines
5.7 KiB
TypeScript
import bcrypt from 'bcryptjs'
|
|
import jwt from 'jsonwebtoken'
|
|
import { cookies } from 'next/headers'
|
|
import { prisma } from './prisma'
|
|
|
|
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'
|
|
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'your-refresh-secret-change-in-production'
|
|
const SALT_ROUNDS = 10
|
|
|
|
export interface JWTPayload {
|
|
userId: string
|
|
email: string
|
|
role: string
|
|
}
|
|
|
|
export interface RefreshTokenPayload {
|
|
userId: string
|
|
tokenType: 'refresh'
|
|
}
|
|
|
|
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 generateToken(payload: JWTPayload): string {
|
|
return jwt.sign(payload, JWT_SECRET, { expiresIn: '15m' }) // Access token for 15 minutes
|
|
}
|
|
|
|
export function generateRefreshToken(): string {
|
|
const token = jwt.sign(
|
|
{ tokenType: 'refresh' } as RefreshTokenPayload,
|
|
JWT_REFRESH_SECRET,
|
|
{ expiresIn: '7d' }
|
|
)
|
|
// Remove the header and signature to get a cleaner token
|
|
return token.split('.')[2] || token
|
|
}
|
|
|
|
export function verifyToken(token: string): JWTPayload | null {
|
|
try {
|
|
return jwt.verify(token, JWT_SECRET) as JWTPayload
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function verifyRefreshToken(token: string): RefreshTokenPayload | null {
|
|
try {
|
|
// Reconstruct the full JWT token
|
|
const fullToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.${token}.signature`
|
|
return jwt.verify(fullToken, JWT_REFRESH_SECRET) as RefreshTokenPayload
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export async function getCurrentUser() {
|
|
const cookieStore = await cookies()
|
|
const token = cookieStore.get('auth-token')?.value
|
|
|
|
if (!token) {
|
|
return null
|
|
}
|
|
|
|
const payload = verifyToken(token)
|
|
if (!payload) {
|
|
return null
|
|
}
|
|
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: payload.userId },
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
username: true,
|
|
displayName: true,
|
|
avatarUrl: true,
|
|
bio: true,
|
|
role: true,
|
|
createdAt: true,
|
|
artist: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
slug: true,
|
|
verified: true,
|
|
},
|
|
},
|
|
label: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
slug: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
return user
|
|
}
|
|
|
|
export async function requireAuth() {
|
|
const user = await getCurrentUser()
|
|
if (!user) {
|
|
throw new Error('Unauthorized')
|
|
}
|
|
return user
|
|
}
|
|
|
|
export async function requireArtist() {
|
|
const user = await requireAuth()
|
|
if (!user.artist) {
|
|
throw new Error('Artist profile required')
|
|
}
|
|
return { user, artist: user.artist }
|
|
}
|
|
|
|
export function generateResetToken(): string {
|
|
return crypto.randomUUID()
|
|
}
|
|
|
|
export async function createSession(
|
|
userId: string,
|
|
deviceInfo?: Record<string, unknown>,
|
|
ipAddress?: string,
|
|
userAgent?: string
|
|
): Promise<string> {
|
|
const sessionToken = crypto.randomUUID()
|
|
|
|
await prisma.session.create({
|
|
data: {
|
|
userId,
|
|
token: sessionToken,
|
|
deviceInfo: deviceInfo ? JSON.stringify(deviceInfo) : undefined,
|
|
ipAddress,
|
|
userAgent,
|
|
},
|
|
})
|
|
|
|
return sessionToken
|
|
}
|
|
|
|
export async function validateSession(sessionToken: string) {
|
|
const session = await prisma.session.findUnique({
|
|
where: { token: sessionToken },
|
|
include: {
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
username: true,
|
|
displayName: true,
|
|
avatarUrl: true,
|
|
role: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if (!session) {
|
|
return null
|
|
}
|
|
|
|
// Update last activity
|
|
await prisma.session.update({
|
|
where: { id: session.id },
|
|
data: { lastActivity: new Date() },
|
|
})
|
|
|
|
return session.user
|
|
}
|
|
|
|
export async function revokeSession(sessionToken: string) {
|
|
await prisma.session.delete({
|
|
where: { token: sessionToken },
|
|
})
|
|
}
|
|
|
|
export async function revokeAllSessions(userId: string) {
|
|
await prisma.session.deleteMany({
|
|
where: { userId },
|
|
})
|
|
}
|
|
|
|
export async function getUserSessions(userId: string) {
|
|
return prisma.session.findMany({
|
|
where: { userId },
|
|
select: {
|
|
id: true,
|
|
deviceInfo: true,
|
|
ipAddress: true,
|
|
userAgent: true,
|
|
lastActivity: true,
|
|
createdAt: true,
|
|
},
|
|
orderBy: { lastActivity: 'desc' },
|
|
})
|
|
}
|
|
|
|
export async function createRefreshToken(userId: string): Promise<string> {
|
|
const token = generateRefreshToken()
|
|
const expiresAt = new Date()
|
|
expiresAt.setDate(expiresAt.getDate() + 7) // 7 days
|
|
|
|
await prisma.refreshToken.create({
|
|
data: {
|
|
token,
|
|
userId,
|
|
expiresAt,
|
|
},
|
|
})
|
|
|
|
return token
|
|
}
|
|
|
|
export async function validateRefreshToken(token: string) {
|
|
const refreshToken = await prisma.refreshToken.findFirst({
|
|
where: {
|
|
token,
|
|
isRevoked: false,
|
|
expiresAt: {
|
|
gt: new Date(),
|
|
},
|
|
},
|
|
include: {
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
username: true,
|
|
role: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if (!refreshToken) {
|
|
return null
|
|
}
|
|
|
|
return refreshToken.user
|
|
}
|
|
|
|
export async function revokeRefreshToken(token: string) {
|
|
await prisma.refreshToken.updateMany({
|
|
where: { token },
|
|
data: { isRevoked: true },
|
|
})
|
|
}
|
|
|
|
export async function revokeAllRefreshTokens(userId: string) {
|
|
await prisma.refreshToken.updateMany({
|
|
where: { userId },
|
|
data: { isRevoked: true },
|
|
})
|
|
}
|
|
|
|
export function generateEmailToken(): string {
|
|
return crypto.randomUUID()
|
|
}
|
|
|
|
export function slugify(text: string): string {
|
|
return text
|
|
.toLowerCase()
|
|
.replace(/[^\w\s-]/g, '')
|
|
.replace(/\s+/g, '-')
|
|
.replace(/-+/g, '-')
|
|
.trim()
|
|
}
|