mirror of
https://github.com/Such-Software/wowcheckout.git
synced 2026-10-17 05:51:45 +02:00
- Use secrets.compare_digest for timing-safe admin key comparison - Strip whitespace from ADMIN_API_KEY env var - Add PostgreSQL advisory locks per-invoice in reconciler to prevent duplicate processing during deployments - Add wallet-RPC retry with backoff (3 attempts: 0.5s, 1s, 2s) - Raise MAX_SUBADDRESS_INDEX from 100 to 300 - Tune DB connection pool (pool_size=20, max_overflow=10, recycle=3600) - Add 1-hour admin session timeout - Regenerate UI lockfile
173 lines
4.5 KiB
TypeScript
173 lines
4.5 KiB
TypeScript
"use server";
|
|
|
|
import { cookies } from "next/headers";
|
|
import { redirect } from "next/navigation";
|
|
import { revalidatePath } from "next/cache";
|
|
|
|
const apiBaseUrl = process.env.API_BASE_URL ?? "http://localhost:8000";
|
|
|
|
const ADMIN_COOKIE = "wowcheckout_admin_key";
|
|
|
|
export type AdminLoginState = {
|
|
error: string | null;
|
|
};
|
|
|
|
export type AdminCreateUserState = {
|
|
error: string | null;
|
|
success: string | null;
|
|
apiKey: string | null;
|
|
webhookSecret: string | null;
|
|
storeId: string | null;
|
|
};
|
|
|
|
export type AdminDeleteUserState = {
|
|
error: string | null;
|
|
success: string | null;
|
|
deletedId: string | null;
|
|
};
|
|
|
|
async function getAdminKey(): Promise<string | null> {
|
|
return (await cookies()).get(ADMIN_COOKIE)?.value ?? null;
|
|
}
|
|
|
|
export async function adminLoginAction(
|
|
_prevState: AdminLoginState,
|
|
formData: FormData
|
|
): Promise<AdminLoginState> {
|
|
const key = String(formData.get("admin_key") ?? "").trim();
|
|
if (!key) {
|
|
return { error: "Admin key is required." };
|
|
}
|
|
|
|
const response = await fetch(`${apiBaseUrl}/api/admin/auth/verify`, {
|
|
method: "POST",
|
|
headers: { "X-Admin-Key": key },
|
|
cache: "no-store",
|
|
});
|
|
|
|
if (!response.ok) {
|
|
return { error: "Invalid admin key." };
|
|
}
|
|
|
|
const cookieStore = await cookies();
|
|
cookieStore.set(ADMIN_COOKIE, key, {
|
|
httpOnly: true,
|
|
sameSite: "lax",
|
|
secure: process.env.NODE_ENV === "production",
|
|
path: "/",
|
|
maxAge: 3600,
|
|
});
|
|
|
|
redirect("/admin");
|
|
}
|
|
|
|
export async function adminLogoutAction() {
|
|
const cookieStore = await cookies();
|
|
cookieStore.set(ADMIN_COOKIE, "", {
|
|
httpOnly: true,
|
|
sameSite: "lax",
|
|
secure: process.env.NODE_ENV === "production",
|
|
path: "/",
|
|
maxAge: 0,
|
|
});
|
|
redirect("/admin");
|
|
}
|
|
|
|
export async function adminCreateUserAction(
|
|
_prevState: AdminCreateUserState,
|
|
formData: FormData
|
|
): Promise<AdminCreateUserState> {
|
|
const adminKey = await getAdminKey();
|
|
if (!adminKey) {
|
|
return { error: "Not authenticated.", success: null, apiKey: null, webhookSecret: null, storeId: null };
|
|
}
|
|
|
|
const paymentAddress = String(formData.get("payment_address") ?? "").trim();
|
|
const viewKey = String(formData.get("view_key") ?? "").trim();
|
|
|
|
if (!paymentAddress || !viewKey) {
|
|
return { error: "Address and view key are required.", success: null, apiKey: null, webhookSecret: null, storeId: null };
|
|
}
|
|
|
|
const response = await fetch(`${apiBaseUrl}/api/admin/users`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-Admin-Key": adminKey,
|
|
},
|
|
body: JSON.stringify({ payment_address: paymentAddress, view_key: viewKey }),
|
|
cache: "no-store",
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const detail = await response.json().catch(() => null);
|
|
const message = detail?.detail ?? "Failed to create user.";
|
|
return { error: message, success: null, apiKey: null, webhookSecret: null, storeId: null };
|
|
}
|
|
|
|
const data = (await response.json()) as {
|
|
api_key: string;
|
|
webhook_secret: string;
|
|
store_id: string;
|
|
};
|
|
|
|
revalidatePath("/admin");
|
|
return {
|
|
error: null,
|
|
success: `Merchant created.`,
|
|
apiKey: data.api_key,
|
|
webhookSecret: data.webhook_secret,
|
|
storeId: data.store_id,
|
|
};
|
|
}
|
|
|
|
export async function adminDeleteUserAction(
|
|
_prevState: AdminDeleteUserState,
|
|
formData: FormData
|
|
): Promise<AdminDeleteUserState> {
|
|
const adminKey = await getAdminKey();
|
|
if (!adminKey) {
|
|
return { error: "Not authenticated.", success: null, deletedId: null };
|
|
}
|
|
|
|
const userId = String(formData.get("user_id") ?? "").trim();
|
|
if (!userId) {
|
|
return { error: "User ID missing.", success: null, deletedId: null };
|
|
}
|
|
|
|
const response = await fetch(`${apiBaseUrl}/api/admin/users/${encodeURIComponent(userId)}`, {
|
|
method: "DELETE",
|
|
headers: { "X-Admin-Key": adminKey },
|
|
cache: "no-store",
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const detail = await response.json().catch(() => null);
|
|
const message = detail?.detail ?? "Failed to delete user.";
|
|
return { error: message, success: null, deletedId: null };
|
|
}
|
|
|
|
revalidatePath("/admin");
|
|
return { error: null, success: "Merchant deleted.", deletedId: userId };
|
|
}
|
|
|
|
export type AdminUser = {
|
|
id: string;
|
|
payment_address: string;
|
|
invoice_count: number;
|
|
created_at: string | null;
|
|
};
|
|
|
|
export async function fetchAdminUsers(): Promise<AdminUser[] | null> {
|
|
const adminKey = await getAdminKey();
|
|
if (!adminKey) return null;
|
|
|
|
const response = await fetch(`${apiBaseUrl}/api/admin/users`, {
|
|
headers: { "X-Admin-Key": adminKey },
|
|
cache: "no-store",
|
|
});
|
|
|
|
if (!response.ok) return null;
|
|
return response.json();
|
|
}
|