This commit is contained in:
@@ -1,11 +1,21 @@
|
||||
import "@refinedev/antd/dist/reset.css";
|
||||
import "antd/dist/reset.css";
|
||||
import type { ReactNode } from "react";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { AdminApp } from "@/components/admin/admin-app";
|
||||
import { AdminAuthorizationError, requireAdminSession } from "@/lib/admin/auth";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default function AdminLayout({ children }: { children: ReactNode }) {
|
||||
export default async function AdminLayout({ children }: { children: ReactNode }) {
|
||||
try {
|
||||
await requireAdminSession("read");
|
||||
} catch (error) {
|
||||
if (error instanceof AdminAuthorizationError) {
|
||||
redirect(error.status === 401 ? "/login" : "/");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return <AdminApp>{children}</AdminApp>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
export function GET() {
|
||||
return new Response(null, {
|
||||
status: 307,
|
||||
headers: { location: "/admin/codes" },
|
||||
});
|
||||
import { AdminAuthorizationError, requireAdminSession } from "@/lib/admin/auth";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
await requireAdminSession("read");
|
||||
return new Response(null, {
|
||||
status: 307,
|
||||
headers: { location: "/admin/codes" },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AdminAuthorizationError) {
|
||||
return new Response(null, {
|
||||
status: 307,
|
||||
headers: { location: error.status === 401 ? "/login" : "/" },
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
isSupabaseConfigurationError,
|
||||
} from "@/lib/supabase/config";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { readIdentityConfig } from "@/modules/identity/config";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
@@ -111,12 +110,7 @@ export async function GET() {
|
||||
Array.isArray(rectificationCaseRows) ? rectificationCaseRows : [],
|
||||
);
|
||||
const isAdmin = await isAdminUser(user);
|
||||
const identityConfig = readIdentityConfig(process.env);
|
||||
const adminUrl = isAdmin
|
||||
? identityConfig.provider === "self-hosted"
|
||||
? new URL("/admin/codes", identityConfig.adminOrigin).toString()
|
||||
: "/admin/codes"
|
||||
: null;
|
||||
const adminUrl = isAdmin ? "/admin/codes" : null;
|
||||
|
||||
return NextResponse.json({
|
||||
user: { id: user.id, email: user.email ?? null },
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { createAdminSupabaseClient, isAdminUser } from "@/lib/supabase/admin";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { requireAdminSession } from "@/lib/admin/auth";
|
||||
import { adminErrorResponse } from "@/lib/admin/http";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
const schema = z.object({ name: z.string().trim().min(1).max(80), description: z.string().trim().max(500), priceCents: z.number().int().positive().max(100_000_000), credits: z.number().int().positive().max(10_000_000), sortOrder: z.number().int().min(-100_000).max(100_000), enabled: z.boolean() });
|
||||
async function requireAdmin() {
|
||||
const client = await createServerSupabaseClient();
|
||||
const { data: { user } } = await client.auth.getUser();
|
||||
if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 });
|
||||
if (!(await isAdminUser(user))) return NextResponse.json({ error: "无管理员权限" }, { status: 403 });
|
||||
return user;
|
||||
}
|
||||
function output(row: Record<string, unknown>) { return { id: row.id, name: row.name, description: row.description, priceCents: row.price_cents, credits: row.credits, sortOrder: row.sort_order, enabled: row.enabled, createdAt: row.created_at, updatedAt: row.updated_at }; }
|
||||
export async function GET() { const auth = await requireAdmin(); if (auth instanceof NextResponse) return auth; const { data, error } = await createAdminSupabaseClient().from("payment_packages").select("*").order("sort_order").order("created_at"); if (error) return NextResponse.json({ error: "暂时无法读取套餐" }, { status: 500 }); return NextResponse.json({ packages: (data || []).map(output) }); }
|
||||
export async function POST(request: Request) { const auth = await requireAdmin(); if (auth instanceof NextResponse) return auth; const parsed = schema.safeParse(await request.json().catch(() => null)); if (!parsed.success) return NextResponse.json({ error: "套餐参数不正确" }, { status: 400 }); const p = parsed.data; const { data, error } = await createAdminSupabaseClient().from("payment_packages").insert({ name: p.name, description: p.description, price_cents: p.priceCents, credits: p.credits, sort_order: p.sortOrder, enabled: p.enabled, created_by: auth.id }).select().single(); if (error) return NextResponse.json({ error: "创建套餐失败" }, { status: 500 }); return NextResponse.json({ package: output(data) }, { status: 201 }); }
|
||||
export async function PATCH(request: Request) { const auth = await requireAdmin(); if (auth instanceof NextResponse) return auth; const body = await request.json().catch(() => null); const id = typeof body?.id === "string" ? body.id : ""; const parsed = schema.safeParse(body); if (!id || !parsed.success) return NextResponse.json({ error: "套餐参数不正确" }, { status: 400 }); const p = parsed.data; const { data, error } = await createAdminSupabaseClient().from("payment_packages").update({ name: p.name, description: p.description, price_cents: p.priceCents, credits: p.credits, sort_order: p.sortOrder, enabled: p.enabled, updated_at: new Date().toISOString() }).eq("id", id).select().single(); if (error) return NextResponse.json({ error: "更新套餐失败" }, { status: 500 }); return NextResponse.json({ package: output(data) }); }
|
||||
export async function DELETE(request: Request) { const auth = await requireAdmin(); if (auth instanceof NextResponse) return auth; const body = await request.json().catch(() => null); if (typeof body?.id !== "string") return NextResponse.json({ error: "套餐参数不正确" }, { status: 400 }); const { error } = await createAdminSupabaseClient().from("payment_packages").update({ enabled: false, updated_at: new Date().toISOString() }).eq("id", body.id); if (error) return NextResponse.json({ error: "停用套餐失败" }, { status: 500 }); return NextResponse.json({ ok: true }); }
|
||||
export async function GET() { try { await requireAdminSession("read"); const { data, error } = await createAdminSupabaseClient().from("payment_packages").select("*").order("sort_order").order("created_at"); if (error) return NextResponse.json({ error: "暂时无法读取套餐" }, { status: 500 }); return NextResponse.json({ packages: (data || []).map(output) }); } catch (error) { return adminErrorResponse(error); } }
|
||||
export async function POST(request: Request) { try { const auth = await requireAdminSession("write"); const parsed = schema.safeParse(await request.json().catch(() => null)); if (!parsed.success) return NextResponse.json({ error: "套餐参数不正确" }, { status: 400 }); const p = parsed.data; const { data, error } = await createAdminSupabaseClient().from("payment_packages").insert({ name: p.name, description: p.description, price_cents: p.priceCents, credits: p.credits, sort_order: p.sortOrder, enabled: p.enabled, created_by: auth.user.id }).select().single(); if (error) return NextResponse.json({ error: "创建套餐失败" }, { status: 500 }); return NextResponse.json({ package: output(data) }, { status: 201 }); } catch (error) { return adminErrorResponse(error); } }
|
||||
export async function PATCH(request: Request) { try { await requireAdminSession("write"); const body = await request.json().catch(() => null); const id = typeof body?.id === "string" ? body.id : ""; const parsed = schema.safeParse(body); if (!id || !parsed.success) return NextResponse.json({ error: "套餐参数不正确" }, { status: 400 }); const p = parsed.data; const { data, error } = await createAdminSupabaseClient().from("payment_packages").update({ name: p.name, description: p.description, price_cents: p.priceCents, credits: p.credits, sort_order: p.sortOrder, enabled: p.enabled, updated_at: new Date().toISOString() }).eq("id", id).select().single(); if (error) return NextResponse.json({ error: "更新套餐失败" }, { status: 500 }); return NextResponse.json({ package: output(data) }); } catch (error) { return adminErrorResponse(error); } }
|
||||
export async function DELETE(request: Request) { try { await requireAdminSession("write"); const body = await request.json().catch(() => null); if (typeof body?.id !== "string") return NextResponse.json({ error: "套餐参数不正确" }, { status: 400 }); const { error } = await createAdminSupabaseClient().from("payment_packages").update({ enabled: false, updated_at: new Date().toISOString() }).eq("id", body.id); if (error) return NextResponse.json({ error: "停用套餐失败" }, { status: 500 }); return NextResponse.json({ ok: true }); } catch (error) { return adminErrorResponse(error); } }
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { createAdminSupabaseClient, isAdminUser } from "@/lib/supabase/admin";
|
||||
import { requireAdminSession } from "@/lib/admin/auth";
|
||||
import { adminErrorResponse } from "@/lib/admin/http";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { isSupabaseConfigurationError } from "@/lib/supabase/config";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
@@ -14,18 +15,9 @@ const querySchema = z.object({
|
||||
offset: z.coerce.number().int().min(0).default(0),
|
||||
});
|
||||
|
||||
async function requireAdmin() {
|
||||
const supabase = await createServerSupabaseClient();
|
||||
const { data: { user }, error } = await supabase.auth.getUser();
|
||||
if (error || !user) return NextResponse.json({ error: "请先登录" }, { status: 401 });
|
||||
if (!(await isAdminUser(user))) return NextResponse.json({ error: "无管理员权限" }, { status: 403 });
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const auth = await requireAdmin();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
await requireAdminSession("read");
|
||||
|
||||
const url = new URL(request.url);
|
||||
const parsed = querySchema.safeParse(Object.fromEntries(url.searchParams));
|
||||
@@ -83,6 +75,8 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json({ orders, stats, pagination: { limit, offset, total, hasMore: offset + orders.length < total } });
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
const authError = adminErrorResponse(error);
|
||||
if (authError.status === 401 || authError.status === 403) return authError;
|
||||
return NextResponse.json({ error: "支付记录服务暂时不可用" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ async function dispatch(
|
||||
const services = getIdentityAuthServices();
|
||||
const handlers = createHostIsolatedAuthHandlers(config, {
|
||||
user: toNextJsHandler(services.user),
|
||||
admin: toNextJsHandler(services.admin),
|
||||
});
|
||||
return handlers[method](request);
|
||||
}
|
||||
|
||||
@@ -1,35 +1,14 @@
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { EmailOtpLogin } from "@/components/email-otp-login";
|
||||
import {
|
||||
isSelfHostedIdentityEnabled,
|
||||
readIdentityConfig,
|
||||
readSelfHostedIdentityConfig,
|
||||
} from "@/modules/identity/config";
|
||||
import { resolveIdentitySurface } from "@/modules/identity/host";
|
||||
import { readIdentityConfig } from "@/modules/identity/config";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function LoginPage() {
|
||||
const config = readIdentityConfig(process.env);
|
||||
let provider = config.provider;
|
||||
let passwordEnabled = false;
|
||||
let passwordOnly = false;
|
||||
if (isSelfHostedIdentityEnabled(process.env)) {
|
||||
const selfHosted = readSelfHostedIdentityConfig(process.env);
|
||||
const surface = resolveIdentitySurface(
|
||||
(await headers()).get("host"),
|
||||
selfHosted,
|
||||
);
|
||||
if (surface === "admin") provider = "self-hosted";
|
||||
passwordEnabled = provider === "self-hosted";
|
||||
passwordOnly = surface === "admin";
|
||||
}
|
||||
return (
|
||||
<EmailOtpLogin
|
||||
provider={provider}
|
||||
passwordEnabled={passwordEnabled}
|
||||
passwordOnly={passwordOnly}
|
||||
provider={config.provider}
|
||||
passwordEnabled={config.provider === "self-hosted"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ const statusColors: Record<CodeRecord["status"], string> = {
|
||||
};
|
||||
|
||||
export default function CodesPage() {
|
||||
const { data: role } = usePermissions<"admin" | "viewer">({});
|
||||
const { data: role } = usePermissions<"admin">({});
|
||||
const { data: identity } = useGetIdentity<AdminIdentity>();
|
||||
const { mutate: createCodes, mutation: createMutation } = useCreate<{ id: string; generated: CodeRecord[] }>();
|
||||
const { mutate: updateCode, mutation: updateMutation } = useUpdate<CodeRecord>();
|
||||
@@ -137,7 +137,7 @@ export default function CodesPage() {
|
||||
{ label: "已兑换", value: "redeemed" },
|
||||
{ label: "已撤销", value: "revoked" },
|
||||
]}
|
||||
extra={writable ? <Button type="primary" onClick={() => setCreateOpen(true)}>批量生成</Button> : <Tag>viewer 只读</Tag>}
|
||||
extra={writable ? <Button type="primary" onClick={() => setCreateOpen(true)}>批量生成</Button> : null}
|
||||
/>
|
||||
|
||||
<Modal title="批量生成兑换码" open={createOpen} onCancel={() => setCreateOpen(false)} footer={null} destroyOnHidden>
|
||||
|
||||
@@ -132,7 +132,7 @@ export function EmailOtpLogin({
|
||||
});
|
||||
if (otpError) throw otpError;
|
||||
}
|
||||
window.location.assign(window.location.hostname.startsWith("admin.") && window.location.hostname.includes("staging") ? "/admin" : "/");
|
||||
window.location.assign("/");
|
||||
} catch (caught) {
|
||||
if (!(caught instanceof Error)) throw caught;
|
||||
setError(authMessage(caught));
|
||||
@@ -149,7 +149,7 @@ export function EmailOtpLogin({
|
||||
setNotice("");
|
||||
try {
|
||||
await selfHostedAuthActions.signInWithPassword(email, password);
|
||||
window.location.assign(window.location.hostname.startsWith("admin.") && window.location.hostname.includes("staging") ? "/admin" : "/");
|
||||
window.location.assign("/");
|
||||
} catch (caught) {
|
||||
if (!(caught instanceof Error)) throw caught;
|
||||
setError(authMessage(caught));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IdentityUser } from "@/modules/identity/contracts";
|
||||
|
||||
export type AdminRole = "admin" | "viewer";
|
||||
export type AdminRole = "admin";
|
||||
|
||||
export type AdminAccessResult =
|
||||
| { allowed: true; role: AdminRole }
|
||||
@@ -11,13 +11,8 @@ export function authorizeAdminAccess(
|
||||
access: "read" | "write",
|
||||
): AdminAccessResult {
|
||||
if (!user) return { allowed: false, status: 401 };
|
||||
const role: AdminRole | null = user.role.includes("admin")
|
||||
? "admin"
|
||||
: user.role.includes("viewer")
|
||||
? "viewer"
|
||||
: null;
|
||||
if (!role || (access === "write" && role !== "admin")) {
|
||||
if (!user.role.includes("admin")) {
|
||||
return { allowed: false, status: 403 };
|
||||
}
|
||||
return { allowed: true, role };
|
||||
return { allowed: true, role: "admin" };
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function requireAdminSession(
|
||||
|
||||
try {
|
||||
const user = await requireIdentityUser(
|
||||
getIdentityAuthServices().admin.api,
|
||||
getIdentityAuthServices().user.api,
|
||||
new Headers(await headers()),
|
||||
);
|
||||
const authorization = authorizeAdminAccess(user, access);
|
||||
|
||||
@@ -18,7 +18,7 @@ export type AdminIdentity = {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: "admin" | "viewer";
|
||||
role: "admin";
|
||||
};
|
||||
|
||||
const apiBase = "/api/admin";
|
||||
@@ -165,7 +165,7 @@ export const adminAccessControlProvider: AccessControlProvider = {
|
||||
}
|
||||
return role === "admin"
|
||||
? { can: true }
|
||||
: { can: false, reason: "viewer 仅可查看" };
|
||||
: { can: false, reason: "无管理员权限" };
|
||||
},
|
||||
options: {
|
||||
buttons: { enableAccessControl: true, hideIfUnauthorized: true },
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function isAdminUser(user: { id?: string; email?: string | null })
|
||||
return rows[0]?.role
|
||||
.split(",")
|
||||
.map((role) => role.trim())
|
||||
.some((role) => role === "admin" || role === "viewer") ?? false;
|
||||
.some((role) => role === "admin") ?? false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7,20 +7,15 @@ import { createLocalPostgresDataClient } from "@/lib/db/local-postgres-client";
|
||||
import { readDatabaseUrl } from "@/lib/db/config";
|
||||
import { getIdentityAuthServices } from "@/modules/identity/auth";
|
||||
import { readIdentitySession } from "@/modules/identity/session";
|
||||
import { readSelfHostedIdentityConfig } from "@/modules/identity/config";
|
||||
import { resolveIdentitySurface } from "@/modules/identity/host";
|
||||
import { getSupabasePublicConfig } from "./config";
|
||||
|
||||
export async function createServerSupabaseClient() {
|
||||
if (process.env.AUTH_PROVIDER?.trim() === "self-hosted") {
|
||||
const requestHeaders = new Headers(await headers());
|
||||
const services = getIdentityAuthServices();
|
||||
const surface = resolveIdentitySurface(
|
||||
requestHeaders.get("host"),
|
||||
readSelfHostedIdentityConfig(process.env),
|
||||
const session = await readIdentitySession(
|
||||
getIdentityAuthServices().user.api,
|
||||
requestHeaders,
|
||||
);
|
||||
const auth = surface === "admin" ? services.admin : services.user;
|
||||
const session = await readIdentitySession(auth.api, requestHeaders);
|
||||
return createLocalPostgresDataClient(
|
||||
readDatabaseUrl(process.env, "APP_DATABASE_URL"),
|
||||
session ? { id: session.user.id, email: session.user.email } : null,
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { createHmac } from "node:crypto";
|
||||
import type { Pool } from "pg";
|
||||
import { APIError, type BetterAuthOptions } from "better-auth";
|
||||
import type { BetterAuthOptions } from "better-auth";
|
||||
import { admin, emailOTP, type EmailOTPOptions } from "better-auth/plugins";
|
||||
|
||||
import type { SelfHostedIdentityConfig } from "./config.ts";
|
||||
import type { EmailOtpSender, IdentitySurface } from "./contracts.ts";
|
||||
import type { EmailOtpSender } from "./contracts.ts";
|
||||
import { identityModelMapping } from "./model.ts";
|
||||
|
||||
export type AdminUserAuthorizer = (userId: string) => Promise<boolean>;
|
||||
|
||||
interface BuildAuthOptionsInput {
|
||||
surface: IdentitySurface;
|
||||
config: SelfHostedIdentityConfig;
|
||||
database: Pool;
|
||||
emailSender: EmailOtpSender;
|
||||
authorizeAdminUser?: AdminUserAuthorizer;
|
||||
}
|
||||
|
||||
function otpIdempotencyKey(
|
||||
@@ -58,26 +56,17 @@ export function createEmailOtpOptions(
|
||||
}
|
||||
|
||||
export function buildAuthOptions({
|
||||
surface,
|
||||
config,
|
||||
database,
|
||||
emailSender,
|
||||
authorizeAdminUser,
|
||||
}: BuildAuthOptionsInput): BetterAuthOptions {
|
||||
if (surface === "admin" && !authorizeAdminUser) {
|
||||
throw new Error("admin user authorizer is required");
|
||||
}
|
||||
|
||||
const origin = surface === "user" ? config.userOrigin : config.adminOrigin;
|
||||
const secret = surface === "user" ? config.userSecret : config.adminSecret;
|
||||
|
||||
return {
|
||||
appName: "Jyotisha",
|
||||
baseURL: origin,
|
||||
baseURL: config.userOrigin,
|
||||
basePath: "/api/auth",
|
||||
secret,
|
||||
secret: config.userSecret,
|
||||
database,
|
||||
trustedOrigins: [origin],
|
||||
trustedOrigins: [config.userOrigin],
|
||||
telemetry: { enabled: false },
|
||||
user: identityModelMapping.user,
|
||||
session: identityModelMapping.session,
|
||||
@@ -91,8 +80,7 @@ export function buildAuthOptions({
|
||||
},
|
||||
advanced: {
|
||||
database: { generateId: "uuid" },
|
||||
cookiePrefix:
|
||||
surface === "user" ? "jyotisha-user" : "jyotisha-admin",
|
||||
cookiePrefix: "jyotisha-user",
|
||||
defaultCookieAttributes: {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
@@ -108,31 +96,12 @@ export function buildAuthOptions({
|
||||
revokeSessionsOnPasswordReset: true,
|
||||
},
|
||||
plugins: [
|
||||
...(surface === "user"
|
||||
? [emailOTP(createEmailOtpOptions(emailSender, secret, false))]
|
||||
: []),
|
||||
emailOTP(createEmailOtpOptions(emailSender, config.userSecret, false)),
|
||||
admin({
|
||||
defaultRole: "user",
|
||||
adminRoles: ["admin"],
|
||||
schema: identityModelMapping.admin,
|
||||
}),
|
||||
],
|
||||
...(surface === "admin"
|
||||
? {
|
||||
databaseHooks: {
|
||||
session: {
|
||||
create: {
|
||||
async before(session: { userId: string }) {
|
||||
if (!(await authorizeAdminUser!(session.userId))) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "Administrator access required",
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ interface AdminRoleRow {
|
||||
ban_expires: Date | null;
|
||||
}
|
||||
|
||||
export type IdentityAdminSurfaceRole = "admin" | "viewer";
|
||||
export type IdentityAdminSurfaceRole = "admin";
|
||||
|
||||
export function createIdentityPool(databaseUrl: string): Pool {
|
||||
return new Pool({
|
||||
@@ -68,19 +68,17 @@ export function createDatabaseAdminAuthorizer(
|
||||
export function createDatabaseAdminSurfaceAuthorizer(
|
||||
pool: Pool,
|
||||
): AdminUserAuthorizer {
|
||||
return createDatabaseRoleAuthorizer(pool, new Set(["admin", "viewer"]));
|
||||
return createDatabaseAdminAuthorizer(pool);
|
||||
}
|
||||
|
||||
export interface IdentityAuthServices {
|
||||
pool: Pool;
|
||||
user: ReturnType<typeof betterAuth>;
|
||||
admin: ReturnType<typeof betterAuth>;
|
||||
}
|
||||
|
||||
interface IdentityAuthDependencies {
|
||||
pool?: Pool;
|
||||
emailSender?: EmailOtpSender;
|
||||
authorizeAdminUser?: AdminUserAuthorizer;
|
||||
}
|
||||
|
||||
export function createIdentityAuthServices(
|
||||
@@ -94,28 +92,15 @@ export function createIdentityAuthServices(
|
||||
apiKey: config.resendApiKey,
|
||||
from: config.resendFrom,
|
||||
});
|
||||
const authorizeAdminUser =
|
||||
dependencies.authorizeAdminUser ?? createDatabaseAdminSurfaceAuthorizer(pool);
|
||||
|
||||
return {
|
||||
pool,
|
||||
user: betterAuth(
|
||||
buildAuthOptions({
|
||||
surface: "user",
|
||||
config,
|
||||
database: pool,
|
||||
emailSender,
|
||||
}),
|
||||
),
|
||||
admin: betterAuth(
|
||||
buildAuthOptions({
|
||||
surface: "admin",
|
||||
config,
|
||||
database: pool,
|
||||
emailSender,
|
||||
authorizeAdminUser,
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,7 @@ export interface SelfHostedIdentityConfig {
|
||||
provider: "self-hosted";
|
||||
databaseUrl: string;
|
||||
userOrigin: string;
|
||||
adminOrigin: string;
|
||||
userSecret: string;
|
||||
adminSecret: string;
|
||||
resendApiKey: string;
|
||||
resendFrom: string;
|
||||
}
|
||||
@@ -95,24 +93,13 @@ export function readSelfHostedIdentityConfig(
|
||||
env: IdentityEnvironment,
|
||||
): SelfHostedIdentityConfig {
|
||||
const userOrigin = readOrigin(env, "AUTH_USER_ORIGIN");
|
||||
const adminOrigin = readOrigin(env, "AUTH_ADMIN_ORIGIN");
|
||||
if (userOrigin === adminOrigin) {
|
||||
throw new Error("user and admin origins must be different");
|
||||
}
|
||||
|
||||
const userSecret = readSecret(env, "BETTER_AUTH_USER_SECRET");
|
||||
const adminSecret = readSecret(env, "BETTER_AUTH_ADMIN_SECRET");
|
||||
if (userSecret === adminSecret) {
|
||||
throw new Error("user and admin secrets must be different");
|
||||
}
|
||||
|
||||
return {
|
||||
provider: "self-hosted",
|
||||
databaseUrl: readPostgresUrl(env),
|
||||
userOrigin,
|
||||
adminOrigin,
|
||||
userSecret,
|
||||
adminSecret,
|
||||
resendApiKey: required(env, "RESEND_API_KEY"),
|
||||
resendFrom: readSender(env),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export type IdentitySurface = "user" | "admin";
|
||||
|
||||
export type EmailOtpType =
|
||||
| "sign-in"
|
||||
| "email-verification"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { SelfHostedIdentityConfig } from "./config.ts";
|
||||
import type { IdentitySurface } from "./contracts.ts";
|
||||
|
||||
export type IdentityRequestHandler = (
|
||||
request: Request,
|
||||
@@ -33,15 +32,11 @@ function normalizeHost(value: string | null): string | null {
|
||||
export function resolveIdentitySurface(
|
||||
hostHeader: string | null,
|
||||
config: SelfHostedIdentityConfig,
|
||||
): IdentitySurface | null {
|
||||
): "user" | null {
|
||||
const host = normalizeHost(hostHeader);
|
||||
if (!host) return null;
|
||||
|
||||
const userHost = new URL(config.userOrigin).host.toLowerCase();
|
||||
const adminHost = new URL(config.adminOrigin).host.toLowerCase();
|
||||
if (host === userHost) return "user";
|
||||
if (host === adminHost) return "admin";
|
||||
return null;
|
||||
return host === new URL(config.userOrigin).host.toLowerCase() ? "user" : null;
|
||||
}
|
||||
|
||||
function isAdminEndpoint(request: Request): boolean {
|
||||
@@ -55,7 +50,7 @@ function isAdminEndpoint(request: Request): boolean {
|
||||
|
||||
export function createHostIsolatedAuthHandlers(
|
||||
config: SelfHostedIdentityConfig,
|
||||
handlers: Record<IdentitySurface, IdentityAuthHandlers>,
|
||||
handlers: { user: IdentityAuthHandlers },
|
||||
): IdentityAuthHandlers {
|
||||
const dispatch =
|
||||
(method: keyof IdentityAuthHandlers): IdentityRequestHandler =>
|
||||
@@ -64,10 +59,10 @@ export function createHostIsolatedAuthHandlers(
|
||||
if (!surface) {
|
||||
return new Response("Unrecognized identity host", { status: 421 });
|
||||
}
|
||||
if (surface === "user" && isAdminEndpoint(request)) {
|
||||
if (isAdminEndpoint(request)) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
return handlers[surface][method](request);
|
||||
return handlers.user[method](request);
|
||||
};
|
||||
|
||||
return { GET: dispatch("GET"), POST: dispatch("POST") };
|
||||
|
||||
Reference in New Issue
Block a user