vibe-cli-tool
Version:
VibeCLI v1.9 - 模板商店生态,智能部署生成,多语言支持,交互式学习系统,企业级Web全栈应用CLI工具
389 lines (327 loc) • 9.62 kB
text/typescript
import { User, UserSession } from '@prisma/client'
import { prisma } from '@/lib/db/prisma'
import { hashPassword, verifyPassword } from '@/lib/utils/password'
import {
generateAccessToken,
generateRefreshToken,
getRefreshTokenExpirationTime
} from '@/lib/utils/jwt'
import { AuthTokens, RegisterData, LoginCredentials } from '@/types/auth'
import { randomBytes } from 'crypto'
export class AuthService {
async register(data: RegisterData): Promise<{
user: Omit<User, 'passwordHash'>
tokens: AuthTokens
}> {
// 检查用户是否已存在
const existingUser = await prisma.user.findUnique({
where: { email: data.email }
})
if (existingUser) {
throw new Error('该邮箱已被注册')
}
// 创建用户
const passwordHash = await hashPassword(data.password)
const user = await prisma.user.create({
data: {
email: data.email,
passwordHash,
name: data.name,
status: 'PENDING'
}
})
// 生成令牌
const tokens = await this.generateTokens(user)
// 发送验证邮件
await this.sendVerificationEmail(user.id, user.email)
return {
user: this.sanitizeUser(user),
tokens
}
}
async login(credentials: LoginCredentials, deviceInfo?: {
ipAddress?: string
userAgent?: string
deviceInfo?: string
}): Promise<{
user: Omit<User, 'passwordHash'>
tokens: AuthTokens
}> {
// 查找用户
const user = await prisma.user.findUnique({
where: { email: credentials.email }
})
if (!user) {
throw new Error('邮箱或密码错误')
}
// 验证密码
const isPasswordValid = await verifyPassword(credentials.password, user.passwordHash)
if (!isPasswordValid) {
throw new Error('邮箱或密码错误')
}
// 检查用户状态
if (user.status === 'SUSPENDED') {
throw new Error('账户已被暂停,请联系管理员')
}
if (user.status === 'INACTIVE') {
throw new Error('账户已被停用')
}
// 更新最后活跃时间
await prisma.user.update({
where: { id: user.id },
data: { lastActiveAt: new Date() }
})
// 生成令牌
const tokens = await this.generateTokens(user, deviceInfo)
return {
user: this.sanitizeUser(user),
tokens
}
}
async refreshTokens(refreshToken: string): Promise<AuthTokens> {
// 查找会话
const session = await prisma.userSession.findUnique({
where: { refreshToken },
include: { user: true }
})
if (!session) {
throw new Error('无效的刷新令牌')
}
// 检查令牌是否过期
if (session.expiresAt < new Date()) {
// 删除过期的会话
await prisma.userSession.delete({
where: { id: session.id }
})
throw new Error('刷新令牌已过期')
}
// 检查用户状态
if (session.user.status !== 'ACTIVE') {
throw new Error('用户账户状态异常')
}
// 生成新的令牌
const accessToken = generateAccessToken(
session.user.id,
session.user.email,
session.user.role
)
const newRefreshToken = generateRefreshToken()
const expiresAt = getRefreshTokenExpirationTime()
// 更新会话
await prisma.userSession.update({
where: { id: session.id },
data: {
refreshToken: newRefreshToken,
expiresAt,
lastUsedAt: new Date()
}
})
return {
accessToken,
refreshToken: newRefreshToken,
expiresIn: 3600 // 1小时
}
}
async logout(refreshToken: string): Promise<void> {
// 删除会话
await prisma.userSession.deleteMany({
where: { refreshToken }
})
}
async logoutAllDevices(userId: string): Promise<void> {
// 删除用户的所有会话
await prisma.userSession.deleteMany({
where: { userId }
})
}
async generateTokens(user: User, deviceInfo?: {
ipAddress?: string
userAgent?: string
deviceInfo?: string
}): Promise<AuthTokens> {
const accessToken = generateAccessToken(user.id, user.email, user.role)
const refreshToken = generateRefreshToken()
const expiresAt = getRefreshTokenExpirationTime()
// 创建会话记录
await prisma.userSession.create({
data: {
userId: user.id,
refreshToken,
expiresAt,
ipAddress: deviceInfo?.ipAddress,
userAgent: deviceInfo?.userAgent,
deviceInfo: deviceInfo?.deviceInfo
}
})
return {
accessToken,
refreshToken,
expiresIn: 3600 // 1小时
}
}
async sendVerificationEmail(userId: string, email: string): Promise<void> {
// 生成验证令牌
const token = randomBytes(32).toString('hex')
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000) // 24小时
// 保存验证记录
await prisma.emailVerification.create({
data: {
userId,
token,
expiresAt
}
})
// TODO: 发送邮件
console.log(`Email verification token for ${email}: ${token}`)
}
async verifyEmail(token: string): Promise<void> {
// 查找验证记录
const verification = await prisma.emailVerification.findUnique({
where: { token },
include: { user: true }
})
if (!verification) {
throw new Error('无效的验证令牌')
}
if (verification.expiresAt < new Date()) {
throw new Error('验证令牌已过期')
}
if (verification.verifiedAt) {
throw new Error('邮箱已经验证过了')
}
// 更新用户状态
await prisma.$transaction([
prisma.user.update({
where: { id: verification.userId },
data: {
status: 'ACTIVE',
emailVerified: new Date()
}
}),
prisma.emailVerification.update({
where: { id: verification.id },
data: { verifiedAt: new Date() }
})
])
}
async requestPasswordReset(email: string): Promise<void> {
// 查找用户
const user = await prisma.user.findUnique({
where: { email }
})
if (!user) {
// 为了安全,不暴露用户是否存在
return
}
// 生成重置令牌
const token = randomBytes(32).toString('hex')
const expiresAt = new Date(Date.now() + 1 * 60 * 60 * 1000) // 1小时
// 保存重置记录
await prisma.passwordReset.create({
data: {
userId: user.id,
token,
expiresAt
}
})
// TODO: 发送重置邮件
console.log(`Password reset token for ${email}: ${token}`)
}
async resetPassword(token: string, newPassword: string): Promise<void> {
// 查找重置记录
const reset = await prisma.passwordReset.findUnique({
where: { token }
})
if (!reset) {
throw new Error('无效的重置令牌')
}
if (reset.expiresAt < new Date()) {
throw new Error('重置令牌已过期')
}
if (reset.usedAt) {
throw new Error('重置令牌已使用过')
}
// 更新密码
const passwordHash = await hashPassword(newPassword)
await prisma.$transaction([
prisma.user.update({
where: { id: reset.userId },
data: { passwordHash }
}),
prisma.passwordReset.update({
where: { id: reset.id },
data: { usedAt: new Date() }
}),
// 删除用户的所有会话,强制重新登录
prisma.userSession.deleteMany({
where: { userId: reset.userId }
})
])
}
async updateProfile(userId: string, data: { name?: string; avatar?: string }): Promise<User> {
const user = await prisma.user.update({
where: { id: userId },
data
})
return this.sanitizeUser(user)
}
async changePassword(userId: string, currentPassword: string, newPassword: string): Promise<void> {
// 获取用户信息
const user = await prisma.user.findUnique({
where: { id: userId }
})
if (!user) {
throw new Error('用户不存在')
}
// 验证当前密码
const isCurrentPasswordValid = await verifyPassword(currentPassword, user.passwordHash)
if (!isCurrentPasswordValid) {
throw new Error('当前密码错误')
}
// 更新密码
const passwordHash = await hashPassword(newPassword)
await prisma.user.update({
where: { id: userId },
data: { passwordHash }
})
// 删除除当前会话外的所有会话
// 注意:在实际应用中,你需要传入当前的refreshToken来保留当前会话
}
async getUserSessions(userId: string): Promise<UserSession[]> {
return prisma.userSession.findMany({
where: { userId },
orderBy: { lastUsedAt: 'desc' }
})
}
async revokeSession(sessionId: string, userId: string): Promise<void> {
await prisma.userSession.deleteMany({
where: {
id: sessionId,
userId
}
})
}
private sanitizeUser(user: User): Omit<User, 'passwordHash'> {
const { passwordHash, ...sanitizedUser } = user
return sanitizedUser
}
async getUserById(userId: string): Promise<Omit<User, 'passwordHash'> | null> {
const user = await prisma.user.findUnique({
where: { id: userId }
})
if (!user) {
return null
}
return this.sanitizeUser(user)
}
async getUserByEmail(email: string): Promise<Omit<User, 'passwordHash'> | null> {
const user = await prisma.user.findUnique({
where: { email }
})
if (!user) {
return null
}
return this.sanitizeUser(user)
}
}
export const authService = new AuthService()