feat(认证): 实现用户认证API和工具库
- 实现 JWT Token 生成和验证 - 实现登录、注册、登出、重置密码 API - 实现邮箱验证码发送功能(配置从环境变量读取) - 实现密码加密和验证工具 - 支持获取当前用户信息
This commit is contained in:
parent
629ff540fc
commit
733c93a91c
170
src/app/api/auth/login/route.ts
Normal file
170
src/app/api/auth/login/route.ts
Normal file
@ -0,0 +1,170 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/drizzle/db';
|
||||
import { users, verificationCodes } from '@/drizzle/schema';
|
||||
import { eq, and, gt } from 'drizzle-orm';
|
||||
import { verifyPassword, validateEmail } from '@/lib/password';
|
||||
import { generateToken, setAuthCookie } from '@/lib/auth';
|
||||
|
||||
// 请求体验证
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email('邮箱格式不正确'),
|
||||
loginType: z.enum(['password', 'code']),
|
||||
password: z.string().optional(),
|
||||
code: z.string().optional(),
|
||||
}).refine((data) => {
|
||||
if (data.loginType === 'password') {
|
||||
return !!data.password;
|
||||
}
|
||||
if (data.loginType === 'code') {
|
||||
return !!data.code && data.code.length === 6;
|
||||
}
|
||||
return false;
|
||||
}, {
|
||||
message: '请提供密码或验证码',
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// 验证请求体
|
||||
const result = loginSchema.safeParse(body);
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: result.error.issues[0].message },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { email, loginType, password, code } = result.data;
|
||||
|
||||
// 验证邮箱格式
|
||||
if (!validateEmail(email)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '邮箱格式不正确' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, email))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '该邮箱未注册' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (user.status === 'banned') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '账号已被禁用' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
if (user.status === 'inactive') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '账号未激活' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// 密码登录
|
||||
if (loginType === 'password') {
|
||||
if (!password) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '请输入密码' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const isValidPassword = await verifyPassword(password, user.password);
|
||||
if (!isValidPassword) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '密码错误' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证码登录
|
||||
if (loginType === 'code') {
|
||||
if (!code) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '请输入验证码' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const validCode = await db
|
||||
.select()
|
||||
.from(verificationCodes)
|
||||
.where(
|
||||
and(
|
||||
eq(verificationCodes.email, email),
|
||||
eq(verificationCodes.code, code),
|
||||
eq(verificationCodes.type, 'login'),
|
||||
eq(verificationCodes.used, false),
|
||||
gt(verificationCodes.expiresAt, now)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (validCode.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '验证码无效或已过期' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 标记验证码为已使用
|
||||
await db
|
||||
.update(verificationCodes)
|
||||
.set({ used: true })
|
||||
.where(eq(verificationCodes.id, validCode[0].id));
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
await db
|
||||
.update(users)
|
||||
.set({ lastLoginAt: new Date(), updatedAt: new Date() })
|
||||
.where(eq(users.userId, user.userId));
|
||||
|
||||
// 生成 JWT Token
|
||||
const token = await generateToken({
|
||||
userId: user.userId,
|
||||
email: user.email,
|
||||
nickname: user.nickname,
|
||||
plan: user.plan || 'free',
|
||||
});
|
||||
|
||||
// 设置认证 Cookie
|
||||
await setAuthCookie(token);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.userId,
|
||||
email: user.email,
|
||||
nickname: user.nickname,
|
||||
plan: user.plan,
|
||||
avatar: user.avatar,
|
||||
},
|
||||
message: '登录成功',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '服务器错误' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
20
src/app/api/auth/logout/route.ts
Normal file
20
src/app/api/auth/logout/route.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { removeAuthCookie } from '@/lib/auth';
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
// 删除认证 Cookie
|
||||
await removeAuthCookie();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '登出成功',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('登出失败:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '服务器错误' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
64
src/app/api/auth/me/route.ts
Normal file
64
src/app/api/auth/me/route.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/drizzle/db';
|
||||
import { users } from '@/drizzle/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { getCurrentUser } from '@/lib/auth';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// 获取当前用户信息
|
||||
const jwtPayload = await getCurrentUser();
|
||||
|
||||
if (!jwtPayload) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '未登录' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// 查询用户详细信息
|
||||
const [user] = await db
|
||||
.select({
|
||||
userId: users.userId,
|
||||
email: users.email,
|
||||
nickname: users.nickname,
|
||||
avatar: users.avatar,
|
||||
plan: users.plan,
|
||||
status: users.status,
|
||||
emailVerified: users.emailVerified,
|
||||
lastLoginAt: users.lastLoginAt,
|
||||
createdAt: users.createdAt,
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.userId, jwtPayload.userId))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '用户不存在' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.userId,
|
||||
email: user.email,
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar,
|
||||
plan: user.plan,
|
||||
status: user.status,
|
||||
emailVerified: user.emailVerified,
|
||||
lastLoginAt: user.lastLoginAt,
|
||||
createdAt: user.createdAt,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '服务器错误' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
158
src/app/api/auth/register/route.ts
Normal file
158
src/app/api/auth/register/route.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { db } from '@/drizzle/db';
|
||||
import { users, verificationCodes, userSettings } from '@/drizzle/schema';
|
||||
import { eq, and, gt } from 'drizzle-orm';
|
||||
import { hashPassword, validatePassword, validateEmail, validateNickname } from '@/lib/password';
|
||||
import { generateToken, setAuthCookie } from '@/lib/auth';
|
||||
|
||||
// 请求体验证
|
||||
const registerSchema = z.object({
|
||||
email: z.string().email('邮箱格式不正确'),
|
||||
password: z.string().min(8, '密码长度不能少于8位'),
|
||||
confirmPassword: z.string(),
|
||||
nickname: z.string().min(2, '昵称长度不能少于2位').max(20, '昵称长度不能超过20位'),
|
||||
code: z.string().length(6, '验证码必须是6位'),
|
||||
}).refine((data) => data.password === data.confirmPassword, {
|
||||
message: '两次输入的密码不一致',
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// 验证请求体
|
||||
const result = registerSchema.safeParse(body);
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: result.error.issues[0].message },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { email, password, nickname, code } = result.data;
|
||||
|
||||
// 验证邮箱格式
|
||||
if (!validateEmail(email)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '邮箱格式不正确' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 验证密码格式
|
||||
const passwordValidation = validatePassword(password);
|
||||
if (!passwordValidation.valid) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: passwordValidation.errors[0] },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 验证昵称格式
|
||||
const nicknameValidation = validateNickname(nickname);
|
||||
if (!nicknameValidation.valid) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: nicknameValidation.error },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 检查邮箱是否已注册
|
||||
const existingUser = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, email))
|
||||
.limit(1);
|
||||
|
||||
if (existingUser.length > 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '该邮箱已被注册' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
const now = new Date();
|
||||
const validCode = await db
|
||||
.select()
|
||||
.from(verificationCodes)
|
||||
.where(
|
||||
and(
|
||||
eq(verificationCodes.email, email),
|
||||
eq(verificationCodes.code, code),
|
||||
eq(verificationCodes.type, 'register'),
|
||||
eq(verificationCodes.used, false),
|
||||
gt(verificationCodes.expiresAt, now)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (validCode.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '验证码无效或已过期' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 标记验证码为已使用
|
||||
await db
|
||||
.update(verificationCodes)
|
||||
.set({ used: true })
|
||||
.where(eq(verificationCodes.id, validCode[0].id));
|
||||
|
||||
// 加密密码
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
// 生成用户ID
|
||||
const userId = nanoid();
|
||||
|
||||
// 创建用户
|
||||
const [newUser] = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
userId,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
nickname,
|
||||
emailVerified: true, // 已通过邮箱验证
|
||||
lastLoginAt: now,
|
||||
})
|
||||
.returning();
|
||||
|
||||
// 创建默认用户设置
|
||||
await db.insert(userSettings).values({
|
||||
userId,
|
||||
});
|
||||
|
||||
// 生成 JWT Token
|
||||
const token = await generateToken({
|
||||
userId: newUser.userId,
|
||||
email: newUser.email,
|
||||
nickname: newUser.nickname,
|
||||
plan: newUser.plan || 'free',
|
||||
});
|
||||
|
||||
// 设置认证 Cookie
|
||||
await setAuthCookie(token);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: newUser.userId,
|
||||
email: newUser.email,
|
||||
nickname: newUser.nickname,
|
||||
plan: newUser.plan,
|
||||
},
|
||||
message: '注册成功',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('注册失败:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '服务器错误' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
114
src/app/api/auth/reset-password/route.ts
Normal file
114
src/app/api/auth/reset-password/route.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/drizzle/db';
|
||||
import { users, verificationCodes } from '@/drizzle/schema';
|
||||
import { eq, and, gt } from 'drizzle-orm';
|
||||
import { hashPassword, validatePassword, validateEmail } from '@/lib/password';
|
||||
|
||||
// 请求体验证
|
||||
const resetPasswordSchema = z.object({
|
||||
email: z.string().email('邮箱格式不正确'),
|
||||
code: z.string().length(6, '验证码必须是6位'),
|
||||
newPassword: z.string().min(8, '密码长度不能少于8位'),
|
||||
confirmPassword: z.string(),
|
||||
}).refine((data) => data.newPassword === data.confirmPassword, {
|
||||
message: '两次输入的密码不一致',
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// 验证请求体
|
||||
const result = resetPasswordSchema.safeParse(body);
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: result.error.issues[0].message },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { email, code, newPassword } = result.data;
|
||||
|
||||
// 验证邮箱格式
|
||||
if (!validateEmail(email)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '邮箱格式不正确' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 验证密码格式
|
||||
const passwordValidation = validatePassword(newPassword);
|
||||
if (!passwordValidation.valid) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: passwordValidation.errors[0] },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, email))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '该邮箱未注册' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
const now = new Date();
|
||||
const validCode = await db
|
||||
.select()
|
||||
.from(verificationCodes)
|
||||
.where(
|
||||
and(
|
||||
eq(verificationCodes.email, email),
|
||||
eq(verificationCodes.code, code),
|
||||
eq(verificationCodes.type, 'reset'),
|
||||
eq(verificationCodes.used, false),
|
||||
gt(verificationCodes.expiresAt, now)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (validCode.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '验证码无效或已过期' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 标记验证码为已使用
|
||||
await db
|
||||
.update(verificationCodes)
|
||||
.set({ used: true })
|
||||
.where(eq(verificationCodes.id, validCode[0].id));
|
||||
|
||||
// 加密新密码
|
||||
const hashedPassword = await hashPassword(newPassword);
|
||||
|
||||
// 更新密码
|
||||
await db
|
||||
.update(users)
|
||||
.set({ password: hashedPassword, updatedAt: now })
|
||||
.where(eq(users.userId, user.userId));
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '密码重置成功',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('重置密码失败:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '服务器错误' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
127
src/app/api/auth/send-code/route.ts
Normal file
127
src/app/api/auth/send-code/route.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/drizzle/db';
|
||||
import { users, verificationCodes } from '@/drizzle/schema';
|
||||
import { eq, and, gt } from 'drizzle-orm';
|
||||
import { sendVerificationEmail, generateVerificationCode } from '@/lib/email';
|
||||
import { validateEmail } from '@/lib/password';
|
||||
|
||||
// 请求体验证
|
||||
const sendCodeSchema = z.object({
|
||||
email: z.string().email('邮箱格式不正确'),
|
||||
type: z.enum(['register', 'login', 'reset']),
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// 验证请求体
|
||||
const result = sendCodeSchema.safeParse(body);
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: result.error.issues[0].message },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { email, type } = result.data;
|
||||
|
||||
// 验证邮箱格式
|
||||
if (!validateEmail(email)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '邮箱格式不正确' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 检查邮箱是否已注册(仅注册时检查)
|
||||
if (type === 'register') {
|
||||
const existingUser = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, email))
|
||||
.limit(1);
|
||||
|
||||
if (existingUser.length > 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '该邮箱已被注册' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查邮箱是否存在(登录和重置密码时检查)
|
||||
if (type === 'login' || type === 'reset') {
|
||||
const existingUser = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, email))
|
||||
.limit(1);
|
||||
|
||||
if (existingUser.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '该邮箱未注册' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否频繁发送(1分钟内只能发送1次)
|
||||
const oneMinuteAgo = new Date(Date.now() - 60 * 1000);
|
||||
const recentCode = await db
|
||||
.select()
|
||||
.from(verificationCodes)
|
||||
.where(
|
||||
and(
|
||||
eq(verificationCodes.email, email),
|
||||
eq(verificationCodes.type, type),
|
||||
gt(verificationCodes.createdAt, oneMinuteAgo)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (recentCode.length > 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '发送太频繁,请稍后再试' },
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
const code = generateVerificationCode();
|
||||
|
||||
// 计算过期时间(5分钟后)
|
||||
const expiresAt = new Date(Date.now() + 5 * 60 * 1000);
|
||||
|
||||
// 保存验证码到数据库
|
||||
await db.insert(verificationCodes).values({
|
||||
email,
|
||||
code,
|
||||
type,
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
// 发送邮件
|
||||
const emailResult = await sendVerificationEmail(email, code, type);
|
||||
|
||||
if (!emailResult.success) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: emailResult.error || '发送邮件失败' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '验证码已发送,请查收邮件',
|
||||
expiresIn: 300, // 5分钟
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('发送验证码失败:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '服务器错误' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
81
src/lib/auth.ts
Normal file
81
src/lib/auth.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { SignJWT, jwtVerify } from 'jose';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
// JWT 密钥(生产环境应使用环境变量)
|
||||
const JWT_SECRET = new TextEncoder().encode(
|
||||
process.env.JWT_SECRET || 'lioncode-jwt-secret-key-2024'
|
||||
);
|
||||
|
||||
// Token 有效期:7 天
|
||||
const TOKEN_EXPIRY = '7d';
|
||||
|
||||
// Cookie 名称
|
||||
const AUTH_COOKIE_NAME = 'lioncode_auth_token';
|
||||
|
||||
// JWT Payload 类型
|
||||
export interface JWTPayload {
|
||||
userId: string;
|
||||
email: string;
|
||||
nickname: string;
|
||||
plan: string;
|
||||
}
|
||||
|
||||
// 生成 JWT Token
|
||||
export async function generateToken(payload: JWTPayload): Promise<string> {
|
||||
const token = await new SignJWT({ ...payload })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(TOKEN_EXPIRY)
|
||||
.sign(JWT_SECRET);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
// 验证 JWT Token
|
||||
export async function verifyToken(token: string): Promise<JWTPayload | null> {
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, JWT_SECRET);
|
||||
return payload as unknown as JWTPayload;
|
||||
} catch (error) {
|
||||
console.error('Token 验证失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置认证 Cookie
|
||||
export async function setAuthCookie(token: string): Promise<void> {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set(AUTH_COOKIE_NAME, token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 7 * 24 * 60 * 60, // 7 天
|
||||
path: '/',
|
||||
});
|
||||
}
|
||||
|
||||
// 获取认证 Cookie
|
||||
export async function getAuthCookie(): Promise<string | undefined> {
|
||||
const cookieStore = await cookies();
|
||||
return cookieStore.get(AUTH_COOKIE_NAME)?.value;
|
||||
}
|
||||
|
||||
// 删除认证 Cookie
|
||||
export async function removeAuthCookie(): Promise<void> {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete(AUTH_COOKIE_NAME);
|
||||
}
|
||||
|
||||
// 获取当前用户信息(从 Cookie 中)
|
||||
export async function getCurrentUser(): Promise<JWTPayload | null> {
|
||||
const token = await getAuthCookie();
|
||||
if (!token) return null;
|
||||
|
||||
return verifyToken(token);
|
||||
}
|
||||
|
||||
// 检查用户是否已登录
|
||||
export async function isAuthenticated(): Promise<boolean> {
|
||||
const user = await getCurrentUser();
|
||||
return user !== null;
|
||||
}
|
||||
136
src/lib/email.ts
Normal file
136
src/lib/email.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
// 邮箱配置(从环境变量读取)
|
||||
const SMTP_HOST = process.env.SMTP_HOST || 'smtp.qq.com';
|
||||
const SMTP_PORT = parseInt(process.env.SMTP_PORT || '465');
|
||||
const SMTP_USER = process.env.SMTP_USER || '';
|
||||
const SMTP_PASS = process.env.SMTP_PASS || '';
|
||||
|
||||
// SMTP 配置
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: SMTP_PORT,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: SMTP_USER,
|
||||
pass: SMTP_PASS,
|
||||
},
|
||||
});
|
||||
|
||||
// 邮件模板类型
|
||||
type EmailType = 'register' | 'login' | 'reset';
|
||||
|
||||
// 获取邮件主题
|
||||
function getSubject(type: EmailType): string {
|
||||
const subjects: Record<EmailType, string> = {
|
||||
register: 'LionCode - 注册验证码',
|
||||
login: 'LionCode - 登录验证码',
|
||||
reset: 'LionCode - 重置密码验证码',
|
||||
};
|
||||
return subjects[type];
|
||||
}
|
||||
|
||||
// 获取邮件内容
|
||||
function getEmailContent(type: EmailType, code: string): string {
|
||||
const typeText: Record<EmailType, string> = {
|
||||
register: '注册账号',
|
||||
login: '登录账号',
|
||||
reset: '重置密码',
|
||||
};
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>验证码</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
||||
<table role="presentation" style="width: 100%; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 0;">
|
||||
<table role="presentation" style="width: 100%; max-width: 600px; border-collapse: collapse; background-color: #ffffff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);">
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="padding: 40px 40px 20px; text-align: center;">
|
||||
<h1 style="margin: 0; font-size: 28px; font-weight: 700; color: #1a1a1a;">LionCode</h1>
|
||||
<p style="margin: 8px 0 0; font-size: 14px; color: #666;">智能 AI 对话助手</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Content -->
|
||||
<tr>
|
||||
<td style="padding: 20px 40px;">
|
||||
<p style="margin: 0 0 20px; font-size: 16px; color: #333; line-height: 1.6;">
|
||||
您好!
|
||||
</p>
|
||||
<p style="margin: 0 0 20px; font-size: 16px; color: #333; line-height: 1.6;">
|
||||
您正在进行<strong>${typeText[type]}</strong>操作,验证码如下:
|
||||
</p>
|
||||
|
||||
<!-- Code Box -->
|
||||
<div style="background: linear-gradient(135deg, #E06B3E 0%, #D4643E 100%); border-radius: 8px; padding: 24px; text-align: center; margin: 24px 0;">
|
||||
<span style="font-size: 36px; font-weight: 700; letter-spacing: 8px; color: #ffffff;">${code}</span>
|
||||
</div>
|
||||
|
||||
<p style="margin: 20px 0 0; font-size: 14px; color: #666; line-height: 1.6;">
|
||||
验证码有效期为 <strong>5 分钟</strong>,请尽快完成验证。
|
||||
</p>
|
||||
<p style="margin: 12px 0 0; font-size: 14px; color: #999; line-height: 1.6;">
|
||||
如果这不是您本人的操作,请忽略此邮件。
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="padding: 20px 40px 40px; border-top: 1px solid #eee;">
|
||||
<p style="margin: 0; font-size: 12px; color: #999; text-align: center;">
|
||||
此邮件由系统自动发送,请勿回复。
|
||||
</p>
|
||||
<p style="margin: 8px 0 0; font-size: 12px; color: #999; text-align: center;">
|
||||
© ${new Date().getFullYear()} LionCode. All rights reserved.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`.trim();
|
||||
}
|
||||
|
||||
// 发送验证码邮件
|
||||
export async function sendVerificationEmail(
|
||||
to: string,
|
||||
code: string,
|
||||
type: EmailType
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: {
|
||||
name: 'LionCode',
|
||||
address: SMTP_USER,
|
||||
},
|
||||
to,
|
||||
subject: getSubject(type),
|
||||
html: getEmailContent(type, code),
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('发送邮件失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '发送邮件失败',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 生成6位数字验证码
|
||||
export function generateVerificationCode(): string {
|
||||
return Math.floor(100000 + Math.random() * 900000).toString();
|
||||
}
|
||||
80
src/lib/password.ts
Normal file
80
src/lib/password.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
// 加密强度(越高越安全,但速度越慢)
|
||||
const SALT_ROUNDS = 12;
|
||||
|
||||
// 密码规则
|
||||
export const PASSWORD_RULES = {
|
||||
minLength: 8,
|
||||
maxLength: 50,
|
||||
requireLetter: true,
|
||||
requireNumber: true,
|
||||
};
|
||||
|
||||
// 加密密码
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return bcrypt.hash(password, SALT_ROUNDS);
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
export async function verifyPassword(
|
||||
password: string,
|
||||
hashedPassword: string
|
||||
): Promise<boolean> {
|
||||
return bcrypt.compare(password, hashedPassword);
|
||||
}
|
||||
|
||||
// 验证密码格式
|
||||
export function validatePassword(password: string): {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
} {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (password.length < PASSWORD_RULES.minLength) {
|
||||
errors.push(`密码长度不能少于 ${PASSWORD_RULES.minLength} 位`);
|
||||
}
|
||||
|
||||
if (password.length > PASSWORD_RULES.maxLength) {
|
||||
errors.push(`密码长度不能超过 ${PASSWORD_RULES.maxLength} 位`);
|
||||
}
|
||||
|
||||
if (PASSWORD_RULES.requireLetter && !/[a-zA-Z]/.test(password)) {
|
||||
errors.push('密码必须包含字母');
|
||||
}
|
||||
|
||||
if (PASSWORD_RULES.requireNumber && !/\d/.test(password)) {
|
||||
errors.push('密码必须包含数字');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
// 验证邮箱格式
|
||||
export function validateEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
// 验证昵称格式
|
||||
export function validateNickname(nickname: string): {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
} {
|
||||
if (!nickname || nickname.trim().length === 0) {
|
||||
return { valid: false, error: '昵称不能为空' };
|
||||
}
|
||||
|
||||
if (nickname.length < 2) {
|
||||
return { valid: false, error: '昵称长度不能少于 2 位' };
|
||||
}
|
||||
|
||||
if (nickname.length > 20) {
|
||||
return { valid: false, error: '昵称长度不能超过 20 位' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user