fix: BigInt serialization in admin routes
- Add jsonResponse helper for BigInt conversion - Cast all COUNT() and ID fields to text in SQL - Convert back to Number() in response - Fix partnerships response format
This commit is contained in:
@@ -5,6 +5,14 @@ import type { Context, Next } from "@oak/oak";
|
|||||||
|
|
||||||
export const adminRouter = new Router({ prefix: "/api/admin" });
|
export const adminRouter = new Router({ prefix: "/api/admin" });
|
||||||
|
|
||||||
|
// Helper to stringify with BigInt support - returns a string for ctx.response.body
|
||||||
|
function jsonResponse(obj: unknown): string {
|
||||||
|
return JSON.stringify(obj, (_key, value) => {
|
||||||
|
if (typeof value === 'bigint') return Number(value);
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Super Admin emails (hardcoded for security)
|
// Super Admin emails (hardcoded for security)
|
||||||
const SUPER_ADMINS = [
|
const SUPER_ADMINS = [
|
||||||
"admin@kronos-soulution.de",
|
"admin@kronos-soulution.de",
|
||||||
@@ -33,99 +41,126 @@ async function requireSuperAdmin(ctx: Context, next: Next): Promise<void> {
|
|||||||
|
|
||||||
// Dashboard stats
|
// Dashboard stats
|
||||||
adminRouter.get("/dashboard", requireSuperAdmin, async (ctx) => {
|
adminRouter.get("/dashboard", requireSuperAdmin, async (ctx) => {
|
||||||
const stats = await queryOne<{
|
try {
|
||||||
org_count: number;
|
// Use ::text to avoid BigInt issues
|
||||||
user_count: number;
|
const rawStats = await queryOne(
|
||||||
order_count: number;
|
`SELECT
|
||||||
timesheet_count: number;
|
(SELECT COUNT(*) FROM organizations)::text as org_count,
|
||||||
}>(
|
(SELECT COUNT(*) FROM users WHERE active = true)::text as user_count,
|
||||||
`SELECT
|
(SELECT COUNT(*) FROM orders)::text as order_count,
|
||||||
(SELECT COUNT(*) FROM organizations) as org_count,
|
(SELECT COUNT(*) FROM timesheets)::text as timesheet_count`
|
||||||
(SELECT COUNT(*) FROM users WHERE active = true) as user_count,
|
);
|
||||||
(SELECT COUNT(*) FROM orders) as order_count,
|
|
||||||
(SELECT COUNT(*) FROM timesheets) as timesheet_count`
|
console.log("rawStats:", rawStats);
|
||||||
);
|
|
||||||
|
const stats = {
|
||||||
// Recent orgs
|
org_count: Number(rawStats?.org_count || 0),
|
||||||
const recentOrgs = await query<{ id: string; name: string; slug: string; created_at: Date }>(
|
user_count: Number(rawStats?.user_count || 0),
|
||||||
`SELECT id, name, slug, created_at
|
order_count: Number(rawStats?.order_count || 0),
|
||||||
FROM organizations
|
timesheet_count: Number(rawStats?.timesheet_count || 0)
|
||||||
ORDER BY created_at DESC
|
};
|
||||||
LIMIT 10`
|
|
||||||
);
|
console.log("stats:", stats);
|
||||||
|
|
||||||
ctx.response.body = { stats, recentOrgs };
|
// Recent orgs - also cast to text to avoid BigInt
|
||||||
|
const rawOrgs = await query(
|
||||||
|
`SELECT id::text as id, name, slug, to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') as created_at
|
||||||
|
FROM organizations
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 10`
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("rawOrgs:", rawOrgs);
|
||||||
|
|
||||||
|
const recentOrgs = rawOrgs.map((o: Record<string, unknown>) => ({
|
||||||
|
id: Number(o.id),
|
||||||
|
name: String(o.name),
|
||||||
|
slug: String(o.slug),
|
||||||
|
created_at: String(o.created_at)
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log("recentOrgs:", recentOrgs);
|
||||||
|
|
||||||
|
const responseBody = { stats, recentOrgs };
|
||||||
|
|
||||||
|
// Stringify manually and set as text response to avoid Oak's JSON.stringify
|
||||||
|
ctx.response.type = "application/json";
|
||||||
|
ctx.response.body = JSON.stringify(responseBody);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Dashboard error:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// List all organizations
|
// List all organizations
|
||||||
adminRouter.get("/organizations", requireSuperAdmin, async (ctx) => {
|
adminRouter.get("/organizations", requireSuperAdmin, async (ctx) => {
|
||||||
const orgs = await query<{
|
const rawOrgs = await query(
|
||||||
id: string;
|
`SELECT o.id::text, o.name, o.slug, o.status, o.plan, o.max_users::text, o.created_at::text,
|
||||||
name: string;
|
(SELECT COUNT(*)::text FROM users WHERE org_id = o.id AND active = true) as user_count,
|
||||||
slug: string;
|
(SELECT COUNT(*)::text FROM orders WHERE org_id = o.id) as order_count
|
||||||
settings: Record<string, unknown>;
|
|
||||||
created_at: Date;
|
|
||||||
user_count: number;
|
|
||||||
order_count: number;
|
|
||||||
}>(
|
|
||||||
`SELECT o.*,
|
|
||||||
(SELECT COUNT(*) FROM users WHERE org_id = o.id AND active = true) as user_count,
|
|
||||||
(SELECT COUNT(*) FROM orders WHERE org_id = o.id) as order_count
|
|
||||||
FROM organizations o
|
FROM organizations o
|
||||||
ORDER BY o.created_at DESC`
|
ORDER BY o.created_at DESC`
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.response.body = { organizations: orgs };
|
const organizations = rawOrgs.map((o: Record<string, unknown>) => ({
|
||||||
|
...o,
|
||||||
|
id: Number(o.id),
|
||||||
|
max_users: Number(o.max_users || 10),
|
||||||
|
user_count: Number(o.user_count || 0),
|
||||||
|
order_count: Number(o.order_count || 0)
|
||||||
|
}));
|
||||||
|
|
||||||
|
ctx.response.body = { organizations };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get single organization details
|
// Get single organization details
|
||||||
adminRouter.get("/organizations/:id", requireSuperAdmin, async (ctx) => {
|
adminRouter.get("/organizations/:id", requireSuperAdmin, async (ctx) => {
|
||||||
const orgId = ctx.params.id;
|
const orgId = ctx.params.id;
|
||||||
|
|
||||||
const org = await queryOne<{
|
const rawOrg = await queryOne(
|
||||||
id: string;
|
`SELECT id::text, name, slug, status, plan, max_users::text, created_at::text
|
||||||
name: string;
|
FROM organizations WHERE id = $1`,
|
||||||
slug: string;
|
|
||||||
settings: Record<string, unknown>;
|
|
||||||
created_at: Date;
|
|
||||||
}>(
|
|
||||||
"SELECT * FROM organizations WHERE id = $1",
|
|
||||||
[orgId]
|
[orgId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!org) {
|
if (!rawOrg) {
|
||||||
throw new AppError("Organisation nicht gefunden", 404);
|
throw new AppError("Organisation nicht gefunden", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const org = {
|
||||||
|
...rawOrg,
|
||||||
|
id: Number(rawOrg.id),
|
||||||
|
max_users: Number(rawOrg.max_users || 10)
|
||||||
|
};
|
||||||
|
|
||||||
// Get users
|
// Get users
|
||||||
const users = await query<{
|
const rawUsers = await query(
|
||||||
id: string;
|
`SELECT id::text, email, role, first_name, last_name, active, last_login::text
|
||||||
email: string;
|
|
||||||
role: string;
|
|
||||||
first_name: string;
|
|
||||||
last_name: string;
|
|
||||||
active: boolean;
|
|
||||||
last_login: Date;
|
|
||||||
}>(
|
|
||||||
`SELECT id, email, role, first_name, last_name, active, last_login
|
|
||||||
FROM users WHERE org_id = $1
|
FROM users WHERE org_id = $1
|
||||||
ORDER BY role, last_name`,
|
ORDER BY role, last_name`,
|
||||||
[orgId]
|
[orgId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const users = rawUsers.map((u: Record<string, unknown>) => ({
|
||||||
|
...u,
|
||||||
|
id: Number(u.id)
|
||||||
|
}));
|
||||||
|
|
||||||
// Get stats
|
// Get stats
|
||||||
const stats = await queryOne<{
|
const rawStats = await queryOne(
|
||||||
order_count: number;
|
|
||||||
timesheet_count: number;
|
|
||||||
enabled_modules: number;
|
|
||||||
}>(
|
|
||||||
`SELECT
|
`SELECT
|
||||||
(SELECT COUNT(*) FROM orders WHERE org_id = $1) as order_count,
|
(SELECT COUNT(*)::text FROM orders WHERE org_id = $1) as order_count,
|
||||||
(SELECT COUNT(*) FROM timesheets t JOIN users u ON t.user_id = u.id WHERE u.org_id = $1) as timesheet_count,
|
(SELECT COUNT(*)::text FROM timesheets t JOIN users u ON t.user_id = u.id WHERE u.org_id = $1) as timesheet_count,
|
||||||
(SELECT COUNT(*) FROM organization_modules WHERE org_id = $1 AND enabled = true) as enabled_modules`,
|
(SELECT COUNT(*)::text FROM organization_modules WHERE org_id = $1 AND enabled = true) as enabled_modules`,
|
||||||
[orgId]
|
[orgId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
order_count: Number(rawStats?.order_count || 0),
|
||||||
|
timesheet_count: Number(rawStats?.timesheet_count || 0),
|
||||||
|
enabled_modules: Number(rawStats?.enabled_modules || 0)
|
||||||
|
};
|
||||||
|
|
||||||
ctx.response.body = { organization: org, users, stats };
|
ctx.response.body = { organization: org, users, stats };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user