diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts new file mode 100644 index 0000000..6ceda64 --- /dev/null +++ b/server/api/auth/login.post.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; +import { authService } from "../service/auth"; +import { checkRateLimit } from "../service/auth/lib/rate-limit"; + +const LoginSchema = z.object({ + email: z.string().email(), + password: z.string(), +}); + +export default defineEventHandler(async (event) => { + const ip = getHeader(event, "x-forwarded-for") ?? "unknown"; + const userAgent = getHeader(event, "user-agent") ?? undefined; + + const { allowed } = checkRateLimit(ip); + if (!allowed) { + setResponseStatus(event, 429); + return { error: { code: "RATE_LIMITED", message: "操作过于频繁,请稍后再试" } }; + } + + const body = await readBody(event); + const parsed = LoginSchema.safeParse(body); + if (!parsed.success) { + setResponseStatus(event, 400); + return { error: { code: "BAD_REQUEST", message: "参数错误" } }; + } + + try { + const { user, accessToken, refreshToken } = await authService.login({ + ...parsed.data, + ip, + userAgent, + }); + + setCookie(event, "refresh_token", refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60, + path: "/", + }); + + return { user, accessToken }; + } catch (err: unknown) { + const e = err as { code?: string; message?: string }; + const statusMap: Record = { + ACCOUNT_LOCKED: 423, + INVALID_CREDENTIALS: 401, + }; + setResponseStatus(event, statusMap[e.code ?? "UNKNOWN"] ?? 400); + return { error: { code: e.code ?? "UNKNOWN", message: e.message ?? "登录失败" } }; + } +}); \ No newline at end of file diff --git a/server/api/auth/logout.post.ts b/server/api/auth/logout.post.ts new file mode 100644 index 0000000..0fec1af --- /dev/null +++ b/server/api/auth/logout.post.ts @@ -0,0 +1,12 @@ +import { authService } from "../service/auth"; + +export default defineEventHandler(async (event) => { + const refreshToken = getCookie(event, "refresh_token"); + if (refreshToken) { + await authService.logout(refreshToken); + } + + deleteCookie(event, "refresh_token", { path: "/" }); + + return { success: true }; +}); \ No newline at end of file diff --git a/server/api/auth/me.get.ts b/server/api/auth/me.get.ts new file mode 100644 index 0000000..05bec6d --- /dev/null +++ b/server/api/auth/me.get.ts @@ -0,0 +1,37 @@ +import { verifyAccessToken } from "../service/auth/lib/jwt"; +import { dbGlobal } from "@/drizzle-pkg/lib/db"; +import { users } from "@/drizzle-pkg/lib/schema/auth"; +import { eq } from "drizzle-orm"; + +export default defineEventHandler(async (event) => { + const accessToken = getHeader(event, "authorization")?.replace("Bearer ", ""); + if (!accessToken) { + setResponseStatus(event, 401); + return { error: { code: "TOKEN_EXPIRED", message: "未登录" } }; + } + + const payload = await verifyAccessToken(accessToken); + if (!payload) { + setResponseStatus(event, 401); + return { error: { code: "TOKEN_EXPIRED", message: "Token 无效" } }; + } + + const [user] = await dbGlobal + .select({ + id: users.id, + email: users.email, + username: users.username, + role: users.role, + status: users.status, + }) + .from(users) + .where(eq(users.id, payload.userId)) + .limit(1); + + if (!user) { + setResponseStatus(event, 404); + return { error: { code: "NOT_FOUND", message: "用户不存在" } }; + } + + return { user }; +}); \ No newline at end of file diff --git a/server/api/auth/refresh.post.ts b/server/api/auth/refresh.post.ts new file mode 100644 index 0000000..0fc3e5f --- /dev/null +++ b/server/api/auth/refresh.post.ts @@ -0,0 +1,27 @@ +import { authService } from "../service/auth"; + +export default defineEventHandler(async (event) => { + const refreshToken = getCookie(event, "refresh_token"); + if (!refreshToken) { + setResponseStatus(event, 401); + return { error: { code: "TOKEN_EXPIRED", message: "未登录" } }; + } + + try { + const { accessToken, newRefreshToken } = await authService.refreshToken(refreshToken); + + setCookie(event, "refresh_token", newRefreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60, + path: "/", + }); + + return { accessToken }; + } catch (err: unknown) { + const e = err as { code?: string; message?: string }; + setResponseStatus(event, 401); + return { error: { code: e.code ?? "TOKEN_EXPIRED", message: e.message ?? "Token 无效" } }; + } +}); \ No newline at end of file diff --git a/server/api/auth/register.post.ts b/server/api/auth/register.post.ts new file mode 100644 index 0000000..0a40785 --- /dev/null +++ b/server/api/auth/register.post.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; +import { authService } from "../service/auth"; +import { checkRateLimit } from "../service/auth/lib/rate-limit"; + +const RegisterSchema = z.object({ + email: z.string().email(), + password: z.string(), + username: z.string().min(2).max(32), +}); + +export default defineEventHandler(async (event) => { + const ip = getHeader(event, "x-forwarded-for") ?? "unknown"; + const userAgent = getHeader(event, "user-agent") ?? undefined; + + const { allowed, retryAfterMs } = checkRateLimit(ip); + if (!allowed) { + setResponseStatus(event, 429); + return { error: { code: "RATE_LIMITED", message: "操作过于频繁,请稍后再试" } }; + } + + const body = await readBody(event); + const parsed = RegisterSchema.safeParse(body); + if (!parsed.success) { + setResponseStatus(event, 400); + return { error: { code: "BAD_REQUEST", message: "参数错误" } }; + } + + try { + const user = await authService.register({ + ...parsed.data, + ip, + userAgent, + }); + setResponseStatus(event, 201); + return { user }; + } catch (err: unknown) { + const e = err as { code?: string; message?: string }; + setResponseStatus(event, e.code === "EMAIL_EXISTS" ? 409 : 400); + return { error: { code: e.code ?? "UNKNOWN", message: e.message ?? "注册失败" } }; + } +}); \ No newline at end of file