5 changed files with 169 additions and 0 deletions
@ -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<string, number> = { |
||||
|
ACCOUNT_LOCKED: 423, |
||||
|
INVALID_CREDENTIALS: 401, |
||||
|
}; |
||||
|
setResponseStatus(event, statusMap[e.code ?? "UNKNOWN"] ?? 400); |
||||
|
return { error: { code: e.code ?? "UNKNOWN", message: e.message ?? "登录失败" } }; |
||||
|
} |
||||
|
}); |
||||
@ -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 }; |
||||
|
}); |
||||
@ -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 }; |
||||
|
}); |
||||
@ -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 无效" } }; |
||||
|
} |
||||
|
}); |
||||
@ -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 ?? "注册失败" } }; |
||||
|
} |
||||
|
}); |
||||
Loading…
Reference in new issue