import { Router } from 'express'; import { query, one } from '../db.js'; import { insertRow, updateById } from '../lib/sql.js'; import { h } from '../lib/respond.js'; import { requireAuth, requireAdmin, type AuthedRequest } from '../middleware/auth.js'; const r = Router(); // checkAdminStatus is available to any authenticated user r.get('/check-status', requireAuth, h(async (req: AuthedRequest) => { const role = await one<{ role: string }>(`SELECT role FROM public.user_roles WHERE user_id = $1 LIMIT 1`, [req.userId]); return { isAdmin: role?.role === 'admin', role: role?.role || 'user' }; })); // everything below requires admin r.use(requireAdmin); r.get('/stats', h(async () => { const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0); const today = todayStart.toISOString(); const monthStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString(); const num = async (sql: string, p: any[] = []) => parseInt((await one<{ c: string }>(sql, p))?.c || '0', 10); const [totalUsers, totalBuilds, totalProjects, activeSubscriptions, activeBuilds, newUsersToday, buildsToday] = await Promise.all([ num(`SELECT count(*)::text c FROM public.profiles`), num(`SELECT count(*)::text c FROM public.app_builds`), num(`SELECT count(*)::text c FROM public.app_projects`), num(`SELECT count(*)::text c FROM public.user_subscriptions WHERE status = 'active'`), num(`SELECT count(*)::text c FROM public.app_builds WHERE status IN ('pending','building')`), num(`SELECT count(*)::text c FROM public.profiles WHERE created_at >= $1`, [today]), num(`SELECT count(*)::text c FROM public.app_builds WHERE created_at >= $1`, [today]), ]); const mrrRows = await query( `SELECT s.billing_cycle, p.price_monthly, p.price_yearly FROM public.user_subscriptions s JOIN public.subscription_plans p ON p.id = s.plan_id WHERE s.status = 'active'` ); const mrr = mrrRows.reduce((sum, s) => sum + (s.billing_cycle === 'yearly' ? Number(s.price_yearly) / 12 : Number(s.price_monthly)), 0); const totalRevenue = Number((await one<{ s: string }>( `SELECT COALESCE(sum(amount),0)::text s FROM public.payment_transactions WHERE status = 'completed'`))?.s || 0); const monthlyRevenue = Number((await one<{ s: string }>( `SELECT COALESCE(sum(amount),0)::text s FROM public.payment_transactions WHERE status = 'completed' AND created_at >= $1`, [monthStart]))?.s || 0); return { totalUsers, totalBuilds, totalProjects, totalRevenue, monthlyRevenue: monthlyRevenue || mrr, activeBuilds, activeSubscriptions, mrr, revenue: totalRevenue, newUsersToday, buildsToday, }; })); r.get('/users', h(async () => { const profiles = await query(`SELECT * FROM public.profiles ORDER BY created_at DESC LIMIT 100`); if (!profiles.length) return []; const ids = profiles.map((p) => p.id); const roles = await query(`SELECT user_id, role FROM public.user_roles WHERE user_id = ANY($1::text[])`, [ids]); const credits = await query(`SELECT user_id, monthly_credits, bonus_credits FROM public.user_credits WHERE user_id = ANY($1::text[])`, [ids]); const roleMap = new Map(); roles.forEach((r2) => { const a = roleMap.get(r2.user_id) || []; a.push({ role: r2.role }); roleMap.set(r2.user_id, a); }); const credMap = new Map(credits.map((c) => [c.user_id, c])); return profiles.map((p) => ({ ...p, user_roles: roleMap.get(p.id) || [], user_credits: credMap.get(p.id) ? [credMap.get(p.id)] : [], })); })); r.patch('/users/:userId/role', h(async (req) => { const { userId } = req.params; const role = req.body?.role; if (role === null) { await query(`DELETE FROM public.user_roles WHERE user_id = $1`, [userId]); return { message: 'Role removed' }; } const existing = await one(`SELECT id FROM public.user_roles WHERE user_id = $1 LIMIT 1`, [userId]); if (existing) { await query(`UPDATE public.user_roles SET role = $2 WHERE user_id = $1`, [userId, role]); return { message: 'Role updated' }; } await query(`INSERT INTO public.user_roles (user_id, role) VALUES ($1, $2)`, [userId, role]); return { message: 'Role created' }; })); r.get('/transactions', h(async () => query(`SELECT * FROM public.payment_transactions ORDER BY created_at DESC`))); r.get('/builds', h(async () => query(`SELECT * FROM public.app_builds ORDER BY created_at DESC LIMIT 100`))); // plans r.get('/plans', h(async () => query(`SELECT * FROM public.subscription_plans ORDER BY price_monthly ASC`))); r.post('/plans', h(async (req) => insertRow('subscription_plans', req.body))); r.patch('/plans/:id', h(async (req) => updateById('subscription_plans', req.params.id, req.body))); // credit packs r.get('/credit-packs', h(async () => query(`SELECT * FROM public.credit_packs ORDER BY price ASC`))); r.post('/credit-packs', h(async (req) => insertRow('credit_packs', req.body))); r.patch('/credit-packs/:id', h(async (req) => updateById('credit_packs', req.params.id, req.body))); r.delete('/credit-packs/:id', h(async (req) => { await query(`DELETE FROM public.credit_packs WHERE id = $1`, [req.params.id]); return { message: 'Credit pack deleted' }; })); // system settings r.get('/settings', h(async () => query(`SELECT * FROM public.system_settings ORDER BY key`))); r.patch('/settings/:id', h(async (req) => updateById('system_settings', req.params.id, { value: req.body?.value }))); r.put('/settings', h(async (req) => { const { key, value, category = 'general', description = null } = req.body || {}; return one( `INSERT INTO public.system_settings (key, value, category, description) VALUES ($1, $2::jsonb, $3, $4) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, category = EXCLUDED.category, description = EXCLUDED.description, updated_at = now() RETURNING *`, [key, JSON.stringify(value ?? null), category, description] ); })); // email templates r.get('/email-templates', h(async () => query(`SELECT * FROM public.email_templates ORDER BY name`))); r.patch('/email-templates/:id', h(async (req) => updateById('email_templates', req.params.id, req.body))); // plugins r.get('/plugins', h(async () => query(`SELECT * FROM public.plugins ORDER BY name`))); r.patch('/plugins/:id', h(async (req) => updateById('plugins', req.params.id, req.body))); // api configs r.get('/api-configs', h(async () => query(`SELECT * FROM public.api_configurations ORDER BY name`))); r.post('/api-configs', h(async (req) => insertRow('api_configurations', req.body))); r.patch('/api-configs/:id', h(async (req) => updateById('api_configurations', req.params.id, req.body))); r.delete('/api-configs/:id', h(async (req) => { await query(`DELETE FROM public.api_configurations WHERE id = $1`, [req.params.id]); return { message: 'API config deleted' }; })); // invoices r.get('/invoices', h(async () => query(`SELECT * FROM public.invoices ORDER BY created_at DESC`))); r.post('/invoices', h(async (req) => insertRow('invoices', req.body))); r.patch('/invoices/:id', h(async (req) => updateById('invoices', req.params.id, req.body))); // audit log r.get('/settings-audit-log', h(async () => query(`SELECT * FROM public.settings_audit_log ORDER BY created_at DESC LIMIT 100`))); // payment gateway configs (used by admin payment config screens) r.get('/payment-gateways', h(async () => query(`SELECT * FROM public.payment_gateway_configs ORDER BY gateway`))); r.put('/payment-gateways/:gateway', h(async (req) => { const { gateway } = req.params; const { is_enabled, is_test_mode, sandbox_config, live_config } = req.body || {}; return one( `INSERT INTO public.payment_gateway_configs (gateway, is_enabled, is_test_mode, sandbox_config, live_config) VALUES ($1,$2,$3,$4::jsonb,$5::jsonb) ON CONFLICT (gateway) DO UPDATE SET is_enabled = COALESCE(EXCLUDED.is_enabled, payment_gateway_configs.is_enabled), is_test_mode = COALESCE(EXCLUDED.is_test_mode, payment_gateway_configs.is_test_mode), sandbox_config = EXCLUDED.sandbox_config, live_config = EXCLUDED.live_config, updated_at = now() RETURNING *`, [gateway, is_enabled ?? null, is_test_mode ?? null, JSON.stringify(sandbox_config ?? {}), JSON.stringify(live_config ?? {})] ); })); // webhook event logs r.get('/webhook-logs', h(async () => query(`SELECT * FROM public.webhook_event_logs ORDER BY created_at DESC LIMIT 200`))); export default r;