project-standalo-sonic-cloud/lib/auth.ts

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()
}