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 (
- )
+ );
}
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 (
- )
+ );
}
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 `
`
+`;
}
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).*)"],
-}
+};