fix(admin): remove operation-level email reauthentication
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
||||
invalidQueryResponse,
|
||||
parseListQuery,
|
||||
readAdminMfaStatus,
|
||||
requireHighRiskAdminMutation,
|
||||
requireAdminMutation,
|
||||
requestId,
|
||||
} from "@/lib/admin/http";
|
||||
|
||||
@@ -68,7 +68,6 @@ export async function GET(request: Request) {
|
||||
roles: row.roles,
|
||||
})),
|
||||
total: Number(rows[0]?.total_count ?? 0),
|
||||
highRiskWritesEnabled: mfa.highRiskWritesEnabled,
|
||||
mfa,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -78,7 +77,7 @@ export async function GET(request: Request) {
|
||||
|
||||
async function mutate(request: Request, assign: boolean) {
|
||||
try {
|
||||
const session = await requireHighRiskAdminMutation(request, "admin.users.manage_roles");
|
||||
const session = await requireAdminMutation(request, "admin.users.manage_roles");
|
||||
const parsed = mutationSchema.safeParse(await request.json().catch(() => null));
|
||||
if (!parsed.success) return invalidQueryResponse(parsed.error.flatten());
|
||||
const targetRows = parsed.data.userId
|
||||
|
||||
@@ -5,7 +5,7 @@ import { runCodeRpc } from "@/lib/admin/codes";
|
||||
import {
|
||||
adminErrorResponse,
|
||||
invalidQueryResponse,
|
||||
requireHighRiskAdminMutation,
|
||||
requireAdminMutation,
|
||||
requestId,
|
||||
} from "@/lib/admin/http";
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function PATCH(
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const session = await requireHighRiskAdminMutation(
|
||||
const session = await requireAdminMutation(
|
||||
request,
|
||||
"billing.adjustments.write",
|
||||
);
|
||||
@@ -66,7 +66,7 @@ export async function DELETE(
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const session = await requireHighRiskAdminMutation(
|
||||
const session = await requireAdminMutation(
|
||||
request,
|
||||
"billing.adjustments.write",
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
adminErrorResponse,
|
||||
invalidQueryResponse,
|
||||
parseListQuery,
|
||||
requireHighRiskAdminMutation,
|
||||
requireAdminMutation,
|
||||
requestId,
|
||||
} from "@/lib/admin/http";
|
||||
import {
|
||||
@@ -123,7 +123,7 @@ export async function GET(request: Request) {
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await requireHighRiskAdminMutation(
|
||||
const session = await requireAdminMutation(
|
||||
request,
|
||||
"billing.adjustments.write",
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { requirePermission } from "@/lib/admin/auth";
|
||||
import { isPostgresError, queryAdminRows } from "@/lib/admin/database";
|
||||
import { adminErrorResponse, requireHighRiskAdminMutation } from "@/lib/admin/http";
|
||||
import { adminErrorResponse, requireAdminMutation } from "@/lib/admin/http";
|
||||
import { suggestedEpayUrls } from "@/lib/epay/config";
|
||||
import { encryptEpayKey } from "@/lib/epay/encryption";
|
||||
import { assertConfiguredEpayUrl, assertPublicGatewayUrl } from "@/lib/epay/gateway-policy";
|
||||
@@ -99,7 +99,7 @@ export async function GET() {
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
try {
|
||||
const session = await requireHighRiskAdminMutation(request, "billing.adjustments.write");
|
||||
const session = await requireAdminMutation(request, "billing.adjustments.write");
|
||||
const parsed = settingsSchema.safeParse(await request.json().catch(() => null));
|
||||
if (!parsed.success) return NextResponse.json({ error: "易支付配置参数不正确" }, { status: 400 });
|
||||
try {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { requirePermission } from "@/lib/admin/auth";
|
||||
import { pageOffset, queryAdminRows } from "@/lib/admin/database";
|
||||
import { adminErrorResponse, invalidQueryResponse, parseListQuery, requestId, requireAdminMutation, requireHighRiskAdminMutation } from "@/lib/admin/http";
|
||||
import { adminErrorResponse, invalidQueryResponse, parseListQuery, requestId, requireAdminMutation } from "@/lib/admin/http";
|
||||
export const runtime="nodejs";
|
||||
const schema=z.discriminatedUnion("action",[
|
||||
z.object({action:z.literal("save"),id:z.string().uuid().nullable().optional(),flagKey:z.string().regex(/^[a-z][a-z0-9._-]{1,99}$/),enabled:z.boolean(),rolloutPercentage:z.number().int().min(0).max(100),config:z.record(z.string(),z.unknown()).default({}),expectedVersion:z.number().int().positive().nullable().optional(),reason:z.string().trim().min(1).max(500)}).strict(),
|
||||
@@ -10,4 +10,4 @@ const schema=z.discriminatedUnion("action",[
|
||||
]);
|
||||
type Row={id:string;flag_key:string;version:number;enabled:boolean;rollout_percentage:number;config:Record<string,unknown>;status:string;created_at:Date;published_at:Date|null;total_count:string};
|
||||
export async function GET(request:Request){try{await requirePermission("admin.access");const p=parseListQuery(request);if(!p.success)return invalidQueryResponse(p.error.flatten());const q=p.data.q?`%${p.data.q}%`:null;const rows=await queryAdminRows<Row>(`select f.*,count(*) over()::text total_count from public.feature_flags f where ($1::text is null or f.flag_key ilike $1) and ($2::text is null or f.status=$2) order by f.created_at desc limit $3 offset $4`,[q,p.data.status??null,p.data.pageSize,pageOffset(p.data.page,p.data.pageSize)]);return NextResponse.json({data:rows.map(r=>({id:r.id,flagKey:r.flag_key,version:r.version,enabled:r.enabled,rolloutPercentage:r.rollout_percentage,config:r.config,status:r.status,createdAt:r.created_at.toISOString(),publishedAt:r.published_at?.toISOString()??null})),total:Number(rows[0]?.total_count??0)});}catch(e){return adminErrorResponse(e)}}
|
||||
export async function POST(request:Request){try{const b=schema.safeParse(await request.json().catch(()=>null));if(!b.success)return invalidQueryResponse(b.error.flatten());const session=b.data.action==="publish"?await requireHighRiskAdminMutation(request,"ops.flags.write"):await requireAdminMutation(request,"ops.flags.write");const rid=requestId(request);if(b.data.action==="publish"){const rows=await queryAdminRows<{id:string}>("select public.admin_publish_feature_flag($1,$2,$3,$4,$5) id",[session.user.id,b.data.id,b.data.expectedVersion,b.data.reason,rid]);return NextResponse.json({data:{id:rows[0]!.id,requestId:rid}})}const v=b.data;const rows=await queryAdminRows<{id:string}>("select public.admin_save_feature_flag($1,$2,$3,$4,$5,$6::jsonb,$7,$8,$9) id",[session.user.id,v.id??null,v.flagKey,v.enabled,v.rolloutPercentage,JSON.stringify(v.config),v.expectedVersion??null,v.reason,rid]);return NextResponse.json({data:{id:rows[0]!.id,requestId:rid}})}catch(e){return adminErrorResponse(e)}}
|
||||
export async function POST(request:Request){try{const b=schema.safeParse(await request.json().catch(()=>null));if(!b.success)return invalidQueryResponse(b.error.flatten());const session=await requireAdminMutation(request,"ops.flags.write");const rid=requestId(request);if(b.data.action==="publish"){const rows=await queryAdminRows<{id:string}>("select public.admin_publish_feature_flag($1,$2,$3,$4,$5) id",[session.user.id,b.data.id,b.data.expectedVersion,b.data.reason,rid]);return NextResponse.json({data:{id:rows[0]!.id,requestId:rid}})}const v=b.data;const rows=await queryAdminRows<{id:string}>("select public.admin_save_feature_flag($1,$2,$3,$4,$5,$6::jsonb,$7,$8,$9) id",[session.user.id,v.id??null,v.flagKey,v.enabled,v.rolloutPercentage,JSON.stringify(v.config),v.expectedVersion??null,v.reason,rid]);return NextResponse.json({data:{id:rows[0]!.id,requestId:rid}})}catch(e){return adminErrorResponse(e)}}
|
||||
|
||||
@@ -10,8 +10,6 @@ import { AdminAuthorizationError, requirePermission, type AdminSession } from "@
|
||||
import {
|
||||
ADMIN_MFA_PROOF_COOKIE,
|
||||
ADMIN_MFA_PROOF_TTL_MS,
|
||||
HIGH_RISK_ADMIN_CHALLENGE_COOKIE,
|
||||
HIGH_RISK_ADMIN_PROOF_COOKIE,
|
||||
issueAdminMfaProof,
|
||||
} from "@/lib/admin/auth-policy";
|
||||
import {
|
||||
@@ -114,7 +112,6 @@ function mfaProofResponse(
|
||||
required: session.requiresMfa,
|
||||
enrolled: true,
|
||||
verified: true,
|
||||
highRiskWritesEnabled: true,
|
||||
expiresIn: ADMIN_MFA_PROOF_TTL_MS / 1_000,
|
||||
},
|
||||
});
|
||||
@@ -282,23 +279,16 @@ export async function POST(request: Request) {
|
||||
required: session.requiresMfa,
|
||||
enrolled: false,
|
||||
verified: false,
|
||||
highRiskWritesEnabled: !session.requiresMfa,
|
||||
},
|
||||
});
|
||||
copyNativeCookies(nativeResponse, response);
|
||||
for (const name of [
|
||||
ADMIN_MFA_PROOF_COOKIE,
|
||||
HIGH_RISK_ADMIN_CHALLENGE_COOKIE,
|
||||
HIGH_RISK_ADMIN_PROOF_COOKIE,
|
||||
]) {
|
||||
response.cookies.set(name, "", {
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
secure: true,
|
||||
path: "/api/admin",
|
||||
maxAge: 0,
|
||||
});
|
||||
}
|
||||
response.cookies.set(ADMIN_MFA_PROOF_COOKIE, "", {
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
secure: true,
|
||||
path: "/api/admin",
|
||||
maxAge: 0,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
return nativeErrorResponse(error) ?? adminErrorResponse(error);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
invalidQueryResponse,
|
||||
parseListQuery,
|
||||
requestId,
|
||||
requireHighRiskAdminMutation,
|
||||
requireAdminMutation,
|
||||
} from "@/lib/admin/http";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
@@ -108,7 +108,7 @@ export async function GET(request: Request) {
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await requireHighRiskAdminMutation(
|
||||
const session = await requireAdminMutation(
|
||||
request,
|
||||
"billing.adjustments.write",
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from "zod";
|
||||
|
||||
import { requirePermission } from "@/lib/admin/auth";
|
||||
import { pageOffset, queryAdminRows } from "@/lib/admin/database";
|
||||
import { adminErrorResponse, invalidQueryResponse, parseListQuery, requestId, requireHighRiskAdminMutation } from "@/lib/admin/http";
|
||||
import { adminErrorResponse, invalidQueryResponse, parseListQuery, requestId, requireAdminMutation } from "@/lib/admin/http";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
@@ -88,7 +88,7 @@ export async function POST(request: Request) {
|
||||
const body = mutationSchema.safeParse(await request.json().catch(() => null));
|
||||
if (!body.success) return invalidQueryResponse(body.error.flatten());
|
||||
const permission = body.data.action === "publish" ? "billing.products.publish" : "billing.products.write";
|
||||
const session = await requireHighRiskAdminMutation(request, permission);
|
||||
const session = await requireAdminMutation(request, permission);
|
||||
const rid = requestId(request);
|
||||
if (body.data.action === "publish") {
|
||||
const rows = await queryAdminRows<{ id: string }>("select public.admin_publish_product($1,$2,$3,$4) id", [session.user.id, body.data.id, body.data.reason, rid]);
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import { APIError } from "better-auth";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getIdentityEmailOtpApi } from "@/modules/identity/auth";
|
||||
import { AdminAuthorizationError } from "@/lib/admin/auth";
|
||||
import {
|
||||
adminPermissions,
|
||||
HIGH_RISK_ADMIN_CHALLENGE_COOKIE,
|
||||
HIGH_RISK_ADMIN_CHALLENGE_TTL_MS,
|
||||
HIGH_RISK_ADMIN_PROOF_COOKIE,
|
||||
HIGH_RISK_ADMIN_PROOF_TTL_MS,
|
||||
issueHighRiskAdminChallenge,
|
||||
issueHighRiskAdminProof,
|
||||
verifyHighRiskAdminChallenge,
|
||||
} from "@/lib/admin/auth-policy";
|
||||
import {
|
||||
adminProofSigningSecret,
|
||||
adminErrorResponse,
|
||||
invalidQueryResponse,
|
||||
requestCookie,
|
||||
requireAdminMutation,
|
||||
} from "@/lib/admin/http";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const reauthSchema = z.discriminatedUnion("action", [
|
||||
z.object({
|
||||
action: z.literal("request"),
|
||||
permission: z.enum(adminPermissions),
|
||||
}),
|
||||
z.object({
|
||||
action: z.literal("verify"),
|
||||
permission: z.enum(adminPermissions),
|
||||
otp: z.string().regex(/^\d{6}$/),
|
||||
}),
|
||||
]);
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const parsed = reauthSchema.safeParse(await request.json().catch(() => null));
|
||||
if (!parsed.success) return invalidQueryResponse(parsed.error.flatten());
|
||||
|
||||
const session = await requireAdminMutation(request, parsed.data.permission);
|
||||
const auth = getIdentityEmailOtpApi();
|
||||
const proofContext = {
|
||||
userId: session.user.id,
|
||||
sessionId: session.identitySession.id,
|
||||
permission: parsed.data.permission,
|
||||
origin: new URL(request.url).origin,
|
||||
};
|
||||
const proofSecret = adminProofSigningSecret();
|
||||
if (parsed.data.action === "request") {
|
||||
await auth.sendVerificationOTP({
|
||||
body: { email: session.user.email, type: "email-verification" },
|
||||
});
|
||||
const response = NextResponse.json({ data: { sent: true, expiresIn: 300 } });
|
||||
response.cookies.set(
|
||||
HIGH_RISK_ADMIN_CHALLENGE_COOKIE,
|
||||
issueHighRiskAdminChallenge(
|
||||
proofContext,
|
||||
proofSecret,
|
||||
session.identitySession.token,
|
||||
),
|
||||
{
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
secure: true,
|
||||
path: "/api/admin",
|
||||
maxAge: HIGH_RISK_ADMIN_CHALLENGE_TTL_MS / 1_000,
|
||||
},
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
if (!verifyHighRiskAdminChallenge(
|
||||
requestCookie(request, HIGH_RISK_ADMIN_CHALLENGE_COOKIE),
|
||||
proofContext,
|
||||
proofSecret,
|
||||
session.identitySession.token,
|
||||
)) {
|
||||
throw new AdminAuthorizationError("请先请求当前权限的邮箱验证码", 403);
|
||||
}
|
||||
|
||||
try {
|
||||
await auth.verifyEmailOTP({
|
||||
body: { email: session.user.email, otp: parsed.data.otp },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof APIError && error.statusCode >= 400 && error.statusCode < 500) {
|
||||
return NextResponse.json({ error: "验证码错误或已过期" }, { status: 400 });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const response = NextResponse.json({ data: { verified: true } });
|
||||
response.cookies.set(
|
||||
HIGH_RISK_ADMIN_PROOF_COOKIE,
|
||||
issueHighRiskAdminProof(
|
||||
proofContext,
|
||||
proofSecret,
|
||||
session.identitySession.token,
|
||||
),
|
||||
{
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
secure: true,
|
||||
path: "/api/admin",
|
||||
maxAge: HIGH_RISK_ADMIN_PROOF_TTL_MS / 1_000,
|
||||
},
|
||||
);
|
||||
response.cookies.set(HIGH_RISK_ADMIN_CHALLENGE_COOKIE, "", {
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
secure: true,
|
||||
path: "/api/admin",
|
||||
maxAge: 0,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
return adminErrorResponse(error);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { requirePermission } from "@/lib/admin/auth";
|
||||
import { pageOffset, queryAdminRows } from "@/lib/admin/database";
|
||||
import { adminErrorResponse, invalidQueryResponse, parseListQuery, requestId, requireHighRiskAdminMutation } from "@/lib/admin/http";
|
||||
import { adminErrorResponse, invalidQueryResponse, parseListQuery, requestId, requireAdminMutation } from "@/lib/admin/http";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
const mutationSchema = z.object({
|
||||
@@ -28,7 +28,7 @@ export async function GET(request: Request) {
|
||||
export async function POST(request: Request){
|
||||
try{
|
||||
const body=mutationSchema.safeParse(await request.json().catch(()=>null)); if(!body.success)return invalidQueryResponse(body.error.flatten());
|
||||
const session=await requireHighRiskAdminMutation(request,"billing.adjustments.write"); const rid=requestId(request);
|
||||
const session=await requireAdminMutation(request,"billing.adjustments.write"); const rid=requestId(request);
|
||||
const rows=await queryAdminRows<{id:string}>("select public.admin_adjust_subscription($1,$2,$3,$4,$5,$6,$7) id",[session.user.id,body.data.id,body.data.action,body.data.days??null,body.data.expectedEndsAt,body.data.reason,rid]);
|
||||
return NextResponse.json({data:{id:rows[0]!.id,requestId:rid}});
|
||||
}catch(error){return adminErrorResponse(error);}
|
||||
|
||||
Reference in New Issue
Block a user