gitdataai/admin/src/app/api/platform/users/route.ts
ZhenYi fb91f5a6c5 feat(admin): add admin panel with billing alerts and model sync
- Add libs/api/admin with admin API endpoints:
  sync models, workspace credit, billing alert check
- Add workspace_alert_config model and alert service
- Add Session::no_op() for background tasks without user context
- Add admin/ Next.js admin panel (AI models, billing, workspaces, audit)
- Start billing alert background task every 30 minutes
2026-04-19 20:48:59 +08:00

117 lines
4.0 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { query } from "@/lib/db";
import { createAuditLog } from "@/lib/log";
export const runtime = "nodejs";
export async function GET(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10));
const pageSize = Math.max(1, parseInt(searchParams.get("pageSize") || "20", 10));
const search = searchParams.get("search") || "";
const offset = (page - 1) * pageSize;
let whereClause = "";
const params: unknown[] = [];
if (search) {
whereClause = 'WHERE username ILIKE $1 OR display_name ILIKE $1';
params.push(`%${search}%`);
}
const countResult = await query<{ count: string }>(
`SELECT COUNT(*) FROM "user" ${whereClause}`,
params
);
const total = parseInt(countResult.rows[0].count, 10);
const result = await query(
`SELECT u.uid, u.username, u.display_name, u.avatar_url, u.organization,
u.last_sign_in_at::text as last_sign_in_at, u.created_at::text as created_at,
COALESCE(up.is_active, true) as is_active,
ue.email
FROM "user" u
LEFT JOIN user_password up ON up.user = u.uid
LEFT JOIN user_email ue ON ue.user = u.uid
${whereClause}
ORDER BY u.created_at DESC
LIMIT $1 OFFSET $2`,
params.length > 0
? [...params, pageSize, offset]
: [pageSize, offset]
);
const users = result.rows.map((r: Record<string, unknown>) => ({
uid: r.uid,
username: r.username,
display_name: r.display_name,
avatar_url: r.avatar_url,
organization: r.organization,
last_sign_in_at: r.last_sign_in_at,
created_at: r.created_at,
is_active: r.is_active,
email: r.email,
}));
return NextResponse.json({ users, total, page, pageSize });
} catch (e) {
console.error("Platform users error:", e);
return NextResponse.json({ error: "服务器错误" }, { status: 500 });
}
}
export async function PATCH(req: NextRequest) {
try {
const body = await req.json() as {
ids?: string[];
action?: "enable" | "disable";
};
const { ids = [], action } = body;
if (!ids.length) {
return NextResponse.json({ error: "至少选择一个用户" }, { status: 400 });
}
if (!action || !["enable", "disable"].includes(action)) {
return NextResponse.json({ error: "action 必须是 enable 或 disable" }, { status: 400 });
}
const isActive = action === "enable";
// Get user ids from uids
const uidPlaceholders = ids.map((_, i) => `$${i + 1}`).join(", ");
const uidResult = await query<{ id: number }>(
`SELECT id FROM "user" WHERE uid IN (${uidPlaceholders})`,
ids
);
const userIds = uidResult.rows.map((r) => r.id);
if (!userIds.length) {
return NextResponse.json({ error: "未找到匹配的用户" }, { status: 404 });
}
const idPlaceholders = userIds.map((_, i) => `$${i + 1}`).join(", ");
await query(
`UPDATE user_password SET is_active = $${userIds.length + 1}, updated_at = NOW() WHERE user_id IN (${idPlaceholders})`,
[...userIds, isActive]
);
const adminUserId = parseInt(req.headers.get("x-admin-user-id") || "0", 10);
const adminUsername = req.headers.get("x-admin-username") || "unknown";
await createAuditLog({
userId: adminUserId,
username: adminUsername,
action: "update",
resource: "user_batch_status",
resourceId: `batch(${userIds.length})`,
requestParams: { uidCount: ids.length, userIdCount: userIds.length, action },
ipAddress: req.headers.get("x-forwarded-for") || undefined,
userAgent: req.headers.get("user-agent") || undefined,
});
return NextResponse.json({ success: true, updated: userIds.length });
} catch (e) {
console.error("Batch update user status error:", e);
return NextResponse.json({ error: "服务器错误" }, { status: 500 });
}
}