diff --git a/package.json b/package.json index 8486f0c..7dc0cac 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "build": "next build", "start": "next start -p 6020", "lint": "eslint .", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,scss,md}\"" }, "dependencies": { "@radix-ui/react-checkbox": "^1.3.0", diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index e8f760c..e177552 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -64,8 +64,8 @@ export async function POST(req: NextRequest) { const res = NextResponse.json( { - redirectTo: issued.redirectTo, - expiresAt: issued.expiresAt, + redirectTo: issued.redirect_to, + expiresAt: issued.expires_at, }, { status: 200, headers: { "Cache-Control": "no-store" } }, ); diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts index c044800..c7d530d 100644 --- a/src/app/api/auth/logout/route.ts +++ b/src/app/api/auth/logout/route.ts @@ -1,23 +1,26 @@ -import { NextRequest, NextResponse } from "next/server" +import { NextRequest, NextResponse } from "next/server"; -import { mustGetEnv } from "@/lib/env" +import { mustGetEnv } from "@/lib/env"; -export const runtime = "nodejs" +export const runtime = "nodejs"; export async function POST(req: NextRequest) { - const auth = req.headers.get("authorization") ?? "" + const auth = req.headers.get("authorization") ?? ""; if (auth.toLowerCase().startsWith("bearer ")) { - const base = mustGetEnv("IAM_SERVICE_BASE_URL").replace(/\/$/, "") - const url = `${base}/auth/logout` + const base = mustGetEnv("IAM_SERVICE_BASE_URL").replace(/\/$/, ""); + const url = `${base}/auth/logout`; await fetch(url, { method: "POST", headers: { Authorization: auth }, cache: "no-store", - }).catch(() => {}) + }).catch(() => {}); } - const res = NextResponse.json({ ok: true }, { headers: { "Cache-Control": "no-store" } }) - res.cookies.delete("iam_remember_email") - res.cookies.delete("iam_captcha") - return res + const res = NextResponse.json( + { ok: true }, + { headers: { "Cache-Control": "no-store" } }, + ); + res.cookies.delete("iam_remember_email"); + res.cookies.delete("iam_captcha"); + return res; } diff --git a/src/app/api/captcha/route.ts b/src/app/api/captcha/route.ts index 6101037..e4e5daf 100644 --- a/src/app/api/captcha/route.ts +++ b/src/app/api/captcha/route.ts @@ -1,19 +1,19 @@ -import { NextResponse } from "next/server" +import { NextResponse } from "next/server"; -import { captchaSvg, generateCaptcha, signCaptchaCookie } from "@/lib/captcha" +import { captchaSvg, generateCaptcha, signCaptchaCookie } from "@/lib/captcha"; -export const runtime = "nodejs" +export const runtime = "nodejs"; export async function GET() { - const payload = generateCaptcha(120) - const svg = captchaSvg(payload.code) + const payload = generateCaptcha(120); + const svg = captchaSvg(payload.code); const res = new NextResponse(svg, { headers: { "Content-Type": "image/svg+xml", "Cache-Control": "no-store", }, - }) + }); res.cookies.set("iam_captcha", signCaptchaCookie(payload), { httpOnly: true, @@ -21,8 +21,7 @@ export async function GET() { sameSite: "strict", path: "/", maxAge: 120, - }) + }); - return res + return res; } - diff --git a/src/app/forgot-password/page.tsx b/src/app/forgot-password/page.tsx index 1050f1a..5271dbf 100644 --- a/src/app/forgot-password/page.tsx +++ b/src/app/forgot-password/page.tsx @@ -5,6 +5,5 @@ export default function ForgotPasswordPage() { 请联系租户管理员或通过业务系统的找回流程重置密码。 - ) + ); } - diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c93c220..fb9c104 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,16 +1,19 @@ -import type { Metadata } from "next" -import "./globals.css" +import type { Metadata } from "next"; +import "./globals.css"; export const metadata: Metadata = { title: "IAM SSO", description: "Unified login for all services", -} +}; -export default function RootLayout({ children }: { children: React.ReactNode }) { +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { return ( {children} - ) + ); } - diff --git a/src/app/logout/page.tsx b/src/app/logout/page.tsx index 356f0a2..71079f7 100644 --- a/src/app/logout/page.tsx +++ b/src/app/logout/page.tsx @@ -1,16 +1,17 @@ -import Link from "next/link" +import Link from "next/link"; export default function LogoutPage() { return (
已退出登录
-
你可以关闭此页面,或重新登录。
+
+ 你可以关闭此页面,或重新登录。 +
返回登录页
- ) + ); } - diff --git a/src/app/page.tsx b/src/app/page.tsx index de4dfd8..9f85280 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,5 @@ -import { redirect } from "next/navigation" +import { redirect } from "next/navigation"; export default function Home() { - redirect("/login") + redirect("/login"); } - diff --git a/src/components/auth/login-form.tsx b/src/components/auth/login-form.tsx index c1cd9e8..2724d37 100644 --- a/src/components/auth/login-form.tsx +++ b/src/components/auth/login-form.tsx @@ -1,50 +1,52 @@ -import * as React from "react" -import Link from "next/link" +import * as React from "react"; +import Link from "next/link"; -import { Button } from "@/components/ui/button" -import { Checkbox } from "@/components/ui/checkbox" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { CaptchaField } from "@/components/auth/captcha-field" +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { CaptchaField } from "@/components/auth/captcha-field"; export function LoginForm(props: { - clientId: string - tenantId: string - callback: string - initialEmail: string - prefillPassword?: string - disabled?: boolean - externalError?: string | null - onClearExternalError?: () => void - captcha: string - onCaptchaChange: (next: string) => void - captchaKey: string - onRefreshCaptcha: () => void + clientId: string; + tenantId: string; + callback: string; + initialEmail: string; + prefillPassword?: string; + disabled?: boolean; + externalError?: string | null; + onClearExternalError?: () => void; + captcha: string; + onCaptchaChange: (next: string) => void; + captchaKey: string; + onRefreshCaptcha: () => void; }) { - const [email, setEmail] = React.useState(props.initialEmail) - const [password, setPassword] = React.useState("") - const [rememberMe, setRememberMe] = React.useState(Boolean(props.initialEmail)) - const [submitting, setSubmitting] = React.useState(false) - const [error, setError] = React.useState(null) + const [email, setEmail] = React.useState(props.initialEmail); + const [password, setPassword] = React.useState(""); + const [rememberMe, setRememberMe] = React.useState( + Boolean(props.initialEmail), + ); + const [submitting, setSubmitting] = React.useState(false); + const [error, setError] = React.useState(null); React.useEffect(() => { - setEmail(props.initialEmail) - setRememberMe(Boolean(props.initialEmail)) - }, [props.initialEmail]) + setEmail(props.initialEmail); + setRememberMe(Boolean(props.initialEmail)); + }, [props.initialEmail]); React.useEffect(() => { if (props.prefillPassword) { - setPassword(props.prefillPassword) + setPassword(props.prefillPassword); } - }, [props.prefillPassword]) + }, [props.prefillPassword]); - const missingParams = !props.clientId || !props.tenantId || !props.callback + const missingParams = !props.clientId || !props.tenantId || !props.callback; async function onSubmit(e: React.FormEvent) { - e.preventDefault() - setError(null) - props.onClearExternalError?.() - setSubmitting(true) + e.preventDefault(); + setError(null); + props.onClearExternalError?.(); + setSubmitting(true); try { const res = await fetch("/api/auth/login", { method: "POST", @@ -58,22 +60,25 @@ export function LoginForm(props: { captcha: props.captcha, rememberMe, }), - }) - const json = (await res.json()) as { redirectTo?: string; message?: string } + }); + const json = (await res.json()) as { + redirectTo?: string; + message?: string; + }; if (!res.ok || !json.redirectTo) { - throw new Error(json.message || "登录失败") + throw new Error(json.message || "登录失败"); } - window.location.href = json.redirectTo + window.location.href = json.redirectTo; } catch (err) { - setError(err instanceof Error ? err.message : "登录失败") - props.onCaptchaChange("") - props.onRefreshCaptcha() + setError(err instanceof Error ? err.message : "登录失败"); + props.onCaptchaChange(""); + props.onRefreshCaptcha(); } finally { - setSubmitting(false) + setSubmitting(false); } } - const disabled = Boolean(props.disabled) || submitting + const disabled = Boolean(props.disabled) || submitting; return (
@@ -146,5 +151,5 @@ export function LoginForm(props: { {submitting ? "登录中..." : "登录"}
- ) + ); } diff --git a/src/components/auth/register-form.tsx b/src/components/auth/register-form.tsx index 30cd0f0..a772ad4 100644 --- a/src/components/auth/register-form.tsx +++ b/src/components/auth/register-form.tsx @@ -1,54 +1,60 @@ -import * as React from "react" +import * as React from "react"; -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { CaptchaField } from "@/components/auth/captcha-field" +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { CaptchaField } from "@/components/auth/captcha-field"; export function RegisterForm(props: { - tenantId: string - disabled?: boolean - captcha: string - onCaptchaChange: (next: string) => void - captchaKey: string - onRefreshCaptcha: () => void - onRegisterSuccessPrefillLogin: (payload: { email: string; password: string }) => void - onLoginAfterRegister: (payload: { email: string; password: string }) => Promise + tenantId: string; + disabled?: boolean; + captcha: string; + onCaptchaChange: (next: string) => void; + captchaKey: string; + onRefreshCaptcha: () => void; + onRegisterSuccessPrefillLogin: (payload: { + email: string; + password: string; + }) => void; + onLoginAfterRegister: (payload: { + email: string; + password: string; + }) => Promise; }) { - const [email, setEmail] = React.useState("") - const [password, setPassword] = React.useState("") - const [confirmPassword, setConfirmPassword] = React.useState("") - const [submitting, setSubmitting] = React.useState(false) - const [error, setError] = React.useState(null) + const [email, setEmail] = React.useState(""); + const [password, setPassword] = React.useState(""); + const [confirmPassword, setConfirmPassword] = React.useState(""); + const [submitting, setSubmitting] = React.useState(false); + const [error, setError] = React.useState(null); const passwordMismatch = - confirmPassword.length > 0 && password !== confirmPassword + confirmPassword.length > 0 && password !== confirmPassword; - const missingTenant = !props.tenantId + const missingTenant = !props.tenantId; async function onSubmit(e: React.FormEvent) { - e.preventDefault() - setError(null) + e.preventDefault(); + setError(null); - const nextEmail = email.trim() + const nextEmail = email.trim(); if (!props.tenantId) { - setError("缺少 tenantId 参数,无法注册") - return + setError("缺少 tenantId 参数,无法注册"); + return; } if (!nextEmail || !password || !confirmPassword) { - setError("请填写邮箱与密码") - return + setError("请填写邮箱与密码"); + return; } if (passwordMismatch) { - setError("两次输入的密码不一致") - return + setError("两次输入的密码不一致"); + return; } if (!props.captcha.trim()) { - setError("请填写验证码") - return + setError("请填写验证码"); + return; } - setSubmitting(true) + setSubmitting(true); try { const res = await fetch("/api/auth/register", { method: "POST", @@ -58,31 +64,31 @@ export function RegisterForm(props: { email: nextEmail, password, }), - }) + }); - const json = (await res.json()) as { message?: string; id?: string } + const json = (await res.json()) as { message?: string; id?: string }; if (!res.ok) { if (res.status === 409) { - throw new Error("该邮箱已注册,请直接登录") + throw new Error("该邮箱已注册,请直接登录"); } - throw new Error(json.message || "注册失败") + throw new Error(json.message || "注册失败"); } if (!json.id) { - throw new Error("注册失败") + throw new Error("注册失败"); } - props.onRegisterSuccessPrefillLogin({ email: nextEmail, password }) - await props.onLoginAfterRegister({ email: nextEmail, password }) + props.onRegisterSuccessPrefillLogin({ email: nextEmail, password }); + await props.onLoginAfterRegister({ email: nextEmail, password }); } catch (err) { - setError(err instanceof Error ? err.message : "注册失败") - props.onCaptchaChange("") - props.onRefreshCaptcha() + setError(err instanceof Error ? err.message : "注册失败"); + props.onCaptchaChange(""); + props.onRefreshCaptcha(); } finally { - setSubmitting(false) + setSubmitting(false); } } - const disabled = Boolean(props.disabled) || submitting + const disabled = Boolean(props.disabled) || submitting; return (
@@ -151,5 +157,5 @@ export function RegisterForm(props: { {submitting ? "注册中..." : "注册并登录"}
- ) + ); } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index daea2e8..498492c 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const buttonVariants = cva( "inline-flex items-center justify-center whitespace-nowrap rounded-md text-base md:text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", @@ -13,7 +13,8 @@ const buttonVariants = cva( secondary: "bg-secondary text-secondary-foreground hover:opacity-90", outline: "border border-input bg-background hover:bg-secondary", ghost: "hover:bg-secondary", - destructive: "bg-destructive text-destructive-foreground hover:opacity-90", + destructive: + "bg-destructive text-destructive-foreground hover:opacity-90", }, size: { default: "h-10 px-4 py-2", @@ -26,27 +27,27 @@ const buttonVariants = cva( size: "default", }, }, -) +); export interface ButtonProps - extends React.ButtonHTMLAttributes, + extends + React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean + asChild?: boolean; } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : "button"; return ( - ) + ); }, -) -Button.displayName = "Button" - -export { Button, buttonVariants } +); +Button.displayName = "Button"; +export { Button, buttonVariants }; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index e39a1ab..5cae366 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,52 +1,86 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const Card = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ), -) -Card.displayName = "Card" +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; -const CardHeader = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ), -) -CardHeader.displayName = "CardHeader" +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; -const CardTitle = React.forwardRef>( - ({ className, ...props }, ref) => ( -

- ), -) -CardTitle.displayName = "CardTitle" +const CardTitle = React.forwardRef< + HTMLHeadingElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardTitle.displayName = "CardTitle"; -const CardDescription = React.forwardRef>( - ({ className, ...props }, ref) => ( -

- ), -) -CardDescription.displayName = "CardDescription" +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = "CardDescription"; -const CardContent = React.forwardRef>( - ({ className, ...props }, ref) => ( -

- ), -) -CardContent.displayName = "CardContent" +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardContent.displayName = "CardContent"; -const CardFooter = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ), -) -CardFooter.displayName = "CardFooter" - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx index 10eb433..1415ec0 100644 --- a/src/components/ui/checkbox.tsx +++ b/src/components/ui/checkbox.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import * as CheckboxPrimitive from "@radix-ui/react-checkbox" -import { Check } from "lucide-react" +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { Check } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Checkbox = React.forwardRef< React.ElementRef, @@ -20,8 +20,7 @@ const Checkbox = React.forwardRef< -)) -Checkbox.displayName = CheckboxPrimitive.Root.displayName - -export { Checkbox } +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; +export { Checkbox }; diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx index f3c8815..b9744f4 100644 --- a/src/components/ui/label.tsx +++ b/src/components/ui/label.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Label = React.forwardRef< React.ElementRef, @@ -15,8 +15,7 @@ const Label = React.forwardRef< )} {...props} /> -)) -Label.displayName = LabelPrimitive.Root.displayName - -export { Label } +)); +Label.displayName = LabelPrimitive.Root.displayName; +export { Label }; diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index cabd6a4..e33d196 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -1,9 +1,9 @@ -import * as React from "react" -import * as TabsPrimitive from "@radix-ui/react-tabs" +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const Tabs = TabsPrimitive.Root +const Tabs = TabsPrimitive.Root; const TabsList = React.forwardRef< React.ElementRef, @@ -17,8 +17,8 @@ const TabsList = React.forwardRef< )} {...props} /> -)) -TabsList.displayName = TabsPrimitive.List.displayName +)); +TabsList.displayName = TabsPrimitive.List.displayName; const TabsTrigger = React.forwardRef< React.ElementRef, @@ -32,8 +32,8 @@ const TabsTrigger = React.forwardRef< )} {...props} /> -)) -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; const TabsContent = React.forwardRef< React.ElementRef, @@ -41,10 +41,13 @@ const TabsContent = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -TabsContent.displayName = TabsPrimitive.Content.displayName +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; -export { Tabs, TabsList, TabsTrigger, TabsContent } +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/src/lib/auth-code.ts b/src/lib/auth-code.ts index 7f4daaa..600b034 100644 --- a/src/lib/auth-code.ts +++ b/src/lib/auth-code.ts @@ -1,36 +1,39 @@ -import crypto from "crypto" -import { SignJWT } from "jose" +import crypto from "crypto"; +import { SignJWT } from "jose"; -import { mustGetEnv } from "@/lib/env" +import { mustGetEnv } from "@/lib/env"; export type AccessTokenClaims = { - sub?: string - tenant_id?: string -} + sub?: string; + tenant_id?: string; +}; export function unsafeDecodeJwtPayload(token: string): T { - const parts = token.split(".") - if (parts.length < 2) throw new Error("Invalid JWT") - const payload = Buffer.from(parts[1], "base64url").toString("utf8") - return JSON.parse(payload) as T + const parts = token.split("."); + if (parts.length < 2) throw new Error("Invalid JWT"); + const payload = Buffer.from(parts[1], "base64url").toString("utf8"); + return JSON.parse(payload) as T; } -export async function issueAuthCode(params: { userId: string; tenantId: string }): Promise<{ - code: string - jti: string - exp: number +export async function issueAuthCode(params: { + userId: string; + tenantId: string; +}): Promise<{ + code: string; + jti: string; + exp: number; }> { - const secret = new TextEncoder().encode(mustGetEnv("AUTH_CODE_JWT_SECRET")) - const now = Math.floor(Date.now() / 1000) - const exp = now + 5 * 60 - const jti = crypto.randomUUID() + const secret = new TextEncoder().encode(mustGetEnv("AUTH_CODE_JWT_SECRET")); + const now = Math.floor(Date.now() / 1000); + const exp = now + 5 * 60; + const jti = crypto.randomUUID(); const code = await new SignJWT({ tenant_id: params.tenantId, jti }) .setProtectedHeader({ alg: "HS256", typ: "JWT" }) .setIssuedAt(now) .setExpirationTime(exp) .setIssuer("iam-front") .setSubject(params.userId) - .sign(secret) + .sign(secret); - return { code, jti, exp } + return { code, jti, exp }; } diff --git a/src/lib/captcha.ts b/src/lib/captcha.ts index 004b3e6..b9fe785 100644 --- a/src/lib/captcha.ts +++ b/src/lib/captcha.ts @@ -1,67 +1,70 @@ -import crypto from "crypto" +import crypto from "crypto"; -import { mustGetEnv } from "@/lib/env" +import { mustGetEnv } from "@/lib/env"; export type CaptchaPayload = { - code: string - exp: number - nonce: string -} + code: string; + exp: number; + nonce: string; +}; export function generateCaptcha(ttlSeconds: number): CaptchaPayload { - const code = String(crypto.randomInt(1000, 9999)) - const exp = Math.floor(Date.now() / 1000) + ttlSeconds - const nonce = crypto.randomBytes(12).toString("hex") - return { code, exp, nonce } + const code = String(crypto.randomInt(1000, 9999)); + const exp = Math.floor(Date.now() / 1000) + ttlSeconds; + const nonce = crypto.randomBytes(12).toString("hex"); + return { code, exp, nonce }; } export function signCaptchaCookie(payload: CaptchaPayload): string { - const data = `${payload.exp}.${payload.nonce}.${payload.code}` + const data = `${payload.exp}.${payload.nonce}.${payload.code}`; const mac = crypto .createHmac("sha256", mustGetEnv("CAPTCHA_SECRET")) .update(data) - .digest("hex") - return `${payload.exp}.${payload.nonce}.${mac}` + .digest("hex"); + return `${payload.exp}.${payload.nonce}.${mac}`; } -export function verifyCaptchaCookie(cookieValue: string | undefined, userInput: string): boolean { - if (!cookieValue) return false - const parts = cookieValue.split(".") - if (parts.length !== 3) return false +export function verifyCaptchaCookie( + cookieValue: string | undefined, + userInput: string, +): boolean { + if (!cookieValue) return false; + const parts = cookieValue.split("."); + if (parts.length !== 3) return false; - const exp = Number(parts[0]) - const nonce = parts[1] - const mac = parts[2] - if (!Number.isFinite(exp)) return false - if (Math.floor(Date.now() / 1000) > exp) return false + const exp = Number(parts[0]); + const nonce = parts[1]; + const mac = parts[2]; + if (!Number.isFinite(exp)) return false; + if (Math.floor(Date.now() / 1000) > exp) return false; - const code = userInput.trim() - if (!/^\d{4}$/.test(code)) return false + const code = userInput.trim(); + if (!/^\d{4}$/.test(code)) return false; - const data = `${exp}.${nonce}.${code}` + const data = `${exp}.${nonce}.${code}`; const expected = crypto .createHmac("sha256", mustGetEnv("CAPTCHA_SECRET")) .update(data) - .digest("hex") - return crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(expected)) + .digest("hex"); + return crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(expected)); } export function captchaSvg(code: string): string { - const width = 120 - const height = 40 + const width = 120; + const height = 40; const noise = Array.from({ length: 6 }).map(() => ({ x1: crypto.randomInt(0, width), y1: crypto.randomInt(0, height), x2: crypto.randomInt(0, width), y2: crypto.randomInt(0, height), - })) + })); const lines = noise .map( (l) => ``, ) - .join("") + .join(""); return ` @@ -70,5 +73,5 @@ export function captchaSvg(code: string): string { ${code} -` +`; } diff --git a/src/lib/env.ts b/src/lib/env.ts index 778af74..23dc277 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -1,5 +1,5 @@ export function mustGetEnv(name: string): string { - const v = process.env[name] - if (!v) throw new Error(`${name} is required`) - return v + const v = process.env[name]; + if (!v) throw new Error(`${name} is required`); + return v; } diff --git a/src/lib/iam.ts b/src/lib/iam.ts index f124970..ffa29a6 100644 --- a/src/lib/iam.ts +++ b/src/lib/iam.ts @@ -26,8 +26,8 @@ export type IamLoginResponse = { }; export type IamLoginCodeResponse = { - redirectTo: string; - expiresAt: number; + redirect_to: string; + expires_at: number; }; export type IamRegisterResponse = { @@ -54,7 +54,11 @@ export async function iamLogin(params: { const body = (await res.json()) as AppResponse; if (!res.ok || body.code !== 0) { - throw new IamApiError(body.message || "Login failed", res.status, body.code); + throw new IamApiError( + body.message || "Login failed", + res.status, + body.code, + ); } return body.data; } @@ -78,7 +82,11 @@ export async function iamRegister(params: { const body = (await res.json()) as AppResponse; if (!res.ok || body.code !== 0) { - throw new IamApiError(body.message || "Register failed", res.status, body.code); + throw new IamApiError( + body.message || "Register failed", + res.status, + body.code, + ); } return body.data; } @@ -99,8 +107,8 @@ export async function iamLoginCode(params: { "X-Tenant-ID": params.tenantId, }, body: JSON.stringify({ - clientId: params.clientId, - redirectUri: params.redirectUri, + client_id: params.clientId, + redirect_uri: params.redirectUri, email: params.email, password: params.password, }), @@ -110,7 +118,11 @@ export async function iamLoginCode(params: { const body = (await res.json()) as AppResponse; if (!res.ok || body.code !== 0) { - throw new IamApiError(body.message || "Login failed", res.status, body.code); + throw new IamApiError( + body.message || "Login failed", + res.status, + body.code, + ); } return body.data; } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e8ed525..a5ef193 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,7 +1,6 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } - diff --git a/src/proxy.ts b/src/proxy.ts index 26bed14..a736ffc 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,17 +1,17 @@ -import { NextRequest, NextResponse } from "next/server" +import { NextRequest, NextResponse } from "next/server"; export function proxy(req: NextRequest) { if (process.env.NODE_ENV === "production") { - const proto = req.headers.get("x-forwarded-proto") + const proto = req.headers.get("x-forwarded-proto"); if (proto && proto !== "https") { - const url = req.nextUrl.clone() - url.protocol = "https" - return NextResponse.redirect(url, 308) + const url = req.nextUrl.clone(); + url.protocol = "https"; + return NextResponse.redirect(url, 308); } } - return NextResponse.next() + return NextResponse.next(); } export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], -} +};