edtech / apps /api /src /routes /super-admin.ts
CognxSafeTrack
fix(audit): resolve all remaining technical debt issues
a888244
import { FastifyInstance } from 'fastify';
import { Prisma } from '@repo/database';
import { prisma } from '../services/prisma';
import { logger } from '../logger';
import { z } from 'zod';
import { redis } from '../lib/redis';
import { whatsappQueue } from '../services/queue';
export async function superAdminRoutes(fastify: FastifyInstance) {
fastify.addHook('preHandler', async (request: any, reply) => {
const role = request.user?.role;
if (role !== 'SUPER_ADMIN') {
return reply.code(403).send({ error: 'Super admin access required' });
}
});
// ── Platform Stats ────────────────────────────────────────────────────────
fastify.get('/platform/stats', async (_req, reply) => {
try {
const since24h = new Date(Date.now() - 24 * 60 * 60 * 1000);
const since30d = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const [orgsCount, usersCount, messagesLast24h, revenueAgg, queueCounts] = await Promise.all([
prisma.organization.count(),
prisma.user.count({ where: { deletedAt: null } }),
prisma.message.count({ where: { createdAt: { gte: since24h } } }),
prisma.walletTransaction.aggregate({
where: { type: 'TOP_UP_MANUAL', createdAt: { gte: since30d } },
_sum: { amount: true }
}),
Promise.all([
whatsappQueue.getWaitingCount(),
whatsappQueue.getFailedCount(),
whatsappQueue.getActiveCount(),
]).catch(() => [0, 0, 0]),
]);
const activeOrgs = await prisma.organization.count({
where: { isHardStopped: false }
});
return {
orgsCount,
activeOrgs,
usersCount,
messagesLast24h,
queueDepth: Array.isArray(queueCounts) ? (queueCounts[0] + queueCounts[2]) : 0,
queueFailed: Array.isArray(queueCounts) ? queueCounts[1] : 0,
revenueThisMonth: revenueAgg._sum.amount ?? 0,
};
} catch (err) {
logger.error({ err }, '[SUPER_ADMIN] platform/stats failed');
return reply.code(500).send({ error: 'Failed to fetch platform stats' });
}
});
// ── Organizations ─────────────────────────────────────────────────────────
fastify.get('/organizations', async (req, reply) => {
const QuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(20),
search: z.string().optional(),
plan: z.string().optional(),
});
const parsed = QuerySchema.safeParse(req.query);
if (!parsed.success) return reply.code(400).send({ error: 'Invalid query params' });
const { page, limit, search, plan } = parsed.data;
const where: any = {};
if (search) where.name = { contains: search, mode: 'insensitive' };
if (plan) where.subscriptionPlan = plan;
try {
const [orgs, total] = await Promise.all([
prisma.organization.findMany({
where,
select: {
id: true, name: true, wabaId: true, walletBalance: true,
subscriptionPlan: true, subscriptionStatus: true, isHardStopped: true,
createdAt: true, mode: true, isCrmActive: true, isEdTechActive: true,
_count: { select: { users: true } }
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit,
}),
prisma.organization.count({ where }),
]);
return { data: orgs.map(o => ({ ...o, userCount: o._count.users })), total, page, limit };
} catch (err) {
logger.error({ err }, '[SUPER_ADMIN] organizations list failed');
return reply.code(500).send({ error: 'Failed to list organizations' });
}
});
fastify.post('/organizations', async (req, reply) => {
const Schema = z.object({
name: z.string().min(1),
subscriptionPlan: z.enum(['STARTER', 'GROWTH', 'SCALE', 'ENTERPRISE']).default('STARTER'),
aiCreditsLimit: z.number().int().positive().default(500),
isCrmActive: z.boolean().default(false),
isEdTechActive: z.boolean().default(true),
});
const body = Schema.safeParse(req.body);
if (!body.success) return reply.code(400).send({ error: body.error.flatten() });
try {
const org = await prisma.organization.create({ data: body.data });
return reply.code(201).send(org);
} catch (err: any) {
logger.error({ err }, '[SUPER_ADMIN] org create failed');
return reply.code(500).send({ error: 'Failed to create organization' });
}
});
fastify.patch<{ Params: { id: string } }>('/organizations/:id', async (req, reply) => {
const Schema = z.object({
name: z.string().min(1).optional(),
subscriptionPlan: z.enum(['STARTER', 'GROWTH', 'SCALE', 'ENTERPRISE']).optional(),
subscriptionStatus: z.string().optional(),
aiCreditsLimit: z.number().int().positive().optional(),
isHardStopped: z.boolean().optional(),
isCrmActive: z.boolean().optional(),
isEdTechActive: z.boolean().optional(),
walletBalance: z.number().int().optional(),
});
const body = Schema.safeParse(req.body);
if (!body.success) return reply.code(400).send({ error: body.error.flatten() });
try {
const org = await prisma.organization.update({
where: { id: req.params.id },
data: body.data,
});
return org;
} catch (err: any) {
logger.error({ err, orgId: req.params.id }, '[SUPER_ADMIN] org update failed');
if (err.code === 'P2025') return reply.code(404).send({ error: 'Organization not found' });
return reply.code(500).send({ error: 'Failed to update organization' });
}
});
// Suspend / activate shortcut
fastify.post<{ Params: { id: string } }>('/organizations/:id/suspend', async (req, reply) => {
const Schema = z.object({ suspend: z.boolean() });
const body = Schema.safeParse(req.body);
if (!body.success) return reply.code(400).send({ error: body.error.flatten() });
try {
await prisma.organization.update({
where: { id: req.params.id },
data: { isHardStopped: body.data.suspend, subscriptionStatus: body.data.suspend ? 'SUSPENDED' : 'ACTIVE' }
});
return { ok: true };
} catch (err: any) {
if (err.code === 'P2025') return reply.code(404).send({ error: 'Organization not found' });
return reply.code(500).send({ error: 'Failed to suspend organization' });
}
});
// Soft-delete — sets subscriptionStatus to DELETED and hard-stops
fastify.delete<{ Params: { id: string } }>('/organizations/:id', async (req, reply) => {
try {
const org = await prisma.organization.findUnique({ where: { id: req.params.id }, select: { id: true, name: true } });
if (!org) return reply.code(404).send({ error: 'Organization not found' });
await prisma.organization.update({
where: { id: req.params.id },
data: { subscriptionStatus: 'DELETED', isHardStopped: true },
});
logger.info({ orgId: req.params.id, actor: (req as any).user?.id }, '[SUPER_ADMIN] Organization soft-deleted');
return { ok: true };
} catch (err) {
return reply.code(500).send({ error: 'Delete failed' });
}
});
// Impersonation — returns a short-lived JWT scoped to the target org
fastify.post<{ Params: { id: string } }>('/organizations/:id/impersonate', async (req, reply) => {
try {
const org = await prisma.organization.findUnique({ where: { id: req.params.id }, select: { id: true, name: true } });
if (!org) return reply.code(404).send({ error: 'Organization not found' });
const impersonateToken = (fastify as any).jwt.sign(
{ id: (req as any).user.id, role: 'ORG_ADMIN', organizationId: org.id, impersonating: true },
{ expiresIn: '1h' }
);
return { token: impersonateToken, orgName: org.name };
} catch (err) {
logger.error({ err }, '[SUPER_ADMIN] impersonate failed');
return reply.code(500).send({ error: 'Impersonation failed' });
}
});
// ── Users (cross-org) ─────────────────────────────────────────────────────
fastify.get('/users', async (req, reply) => {
const QuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(20),
orgId: z.string().optional(),
role: z.string().optional(),
search: z.string().optional(),
});
const parsed = QuerySchema.safeParse(req.query);
if (!parsed.success) return reply.code(400).send({ error: 'Invalid query params' });
const { page, limit, orgId, role, search } = parsed.data;
const where: any = { deletedAt: null };
if (orgId) where.organizationId = orgId;
if (role) where.role = role;
if (search) where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } },
];
try {
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
select: { id: true, name: true, email: true, phone: true, role: true, organizationId: true, createdAt: true, lastActivityAt: true,
organization: { select: { name: true } }
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit,
}),
prisma.user.count({ where }),
]);
return { data: users, total, page, limit };
} catch (err) {
logger.error({ err }, '[SUPER_ADMIN] users list failed');
return reply.code(500).send({ error: 'Failed to list users' });
}
});
fastify.patch<{ Params: { userId: string } }>('/users/:userId/role', async (req, reply) => {
const Schema = z.object({ role: z.enum(['STUDENT', 'ORG_MEMBER', 'ORG_ADMIN', 'ADMIN', 'SUPER_ADMIN']) });
const body = Schema.safeParse(req.body);
if (!body.success) return reply.code(400).send({ error: body.error.flatten() });
try {
const user = await prisma.user.update({ where: { id: req.params.userId }, data: { role: body.data.role } });
return { ok: true, role: user.role };
} catch (err: any) {
if (err.code === 'P2025') return reply.code(404).send({ error: 'User not found' });
return reply.code(500).send({ error: 'Failed to update role' });
}
});
// Reset password — generates a 1h JWT reset token and emails a link
fastify.post<{ Params: { userId: string } }>('/users/:userId/reset-password', async (req, reply) => {
try {
const user = await prisma.user.findUnique({ where: { id: req.params.userId }, select: { id: true, email: true, name: true } });
if (!user) return reply.code(404).send({ error: 'User not found' });
if (!user.email) return reply.code(400).send({ error: 'User has no email address' });
const resetToken = (fastify as any).jwt.sign(
{ id: user.id, purpose: 'reset' },
{ expiresIn: '1h' }
) as string;
const adminUrl = process.env.ADMIN_URL || 'https://edtechadmin.netlify.app';
const resetLink = `${adminUrl}/reset-password?token=${resetToken}`;
const { scheduleEmail } = await import('../services/queue');
await scheduleEmail({
to: user.email,
subject: 'Réinitialisation de votre mot de passe — XAMLÉ (admin)',
htmlContent: `<p>Bonjour ${user.name ?? ''},</p>
<p>Un administrateur a déclenché une réinitialisation de votre mot de passe. Cliquez ci-dessous pour définir un nouveau mot de passe. Ce lien expire dans <strong>1 heure</strong>.</p>
<p><a href="${resetLink}" style="background:#4f46e5;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:bold;">Réinitialiser mon mot de passe</a></p>
<p>Si vous n'attendiez pas cet email, contactez votre administrateur.</p>`,
});
logger.info({ userId: user.id, actor: (req as any).user?.id }, '[SUPER_ADMIN] Password reset email sent');
return { ok: true, email: user.email };
} catch (err) {
logger.error({ err }, '[SUPER_ADMIN] reset-password failed');
return reply.code(500).send({ error: 'Failed to send reset email' });
}
});
// ── WhatsApp Numbers (cross-org) ──────────────────────────────────────────
fastify.get('/whatsapp/numbers', async (_req, reply) => {
try {
const numbers = await prisma.whatsAppPhoneNumber.findMany({
include: { organization: { select: { id: true, name: true } } },
orderBy: { createdAt: 'desc' },
});
return { data: numbers };
} catch (err) {
logger.error({ err }, '[SUPER_ADMIN] whatsapp numbers failed');
return reply.code(500).send({ error: 'Failed to list phone numbers' });
}
});
// ── WhatsApp Number Registration (OTP flow) ───────────────────────────────
// Step 1 — Trigger Meta to send OTP to the phone number
fastify.post('/whatsapp/numbers/register', async (req, reply) => {
const Schema = z.object({
orgId: z.string(),
phoneNumberId: z.string(),
pin: z.string().length(6).regex(/^\d{6}$/).default('000000'),
});
const body = Schema.safeParse(req.body);
if (!body.success) return reply.code(400).send({ error: body.error.flatten() });
const { orgId, phoneNumberId, pin } = body.data;
try {
const { decryptSecrets } = await import('../services/organization');
const { whatsappService } = await import('../services/whatsapp');
const org = await prisma.organization.findUnique({
where: { id: orgId },
select: { id: true, name: true, systemUserToken: true, wabaId: true, systemUserTokenIssuedAt: true, webhookSecret: true },
});
if (!org) return reply.code(404).send({ error: 'Organization not found' });
if (!org.systemUserToken) return reply.code(400).send({ error: 'Organization has no systemUserToken configured' });
const decrypted = decryptSecrets(org as any);
const result = await whatsappService.registerPhoneNumber({ accessToken: decrypted.systemUserToken! }, phoneNumberId, pin);
logger.info({ orgId, phoneNumberId, actor: (req as any).user?.id }, '[SUPER_ADMIN] Phone number registration initiated');
return { ok: true, metaResponse: result };
} catch (err: any) {
const detail = err.response?.data?.error?.message ?? err.message;
return reply.code(502).send({ error: 'Meta API error', detail });
}
});
// Step 2 — Verify OTP sent by Meta
fastify.post('/whatsapp/numbers/verify', async (req, reply) => {
const Schema = z.object({
orgId: z.string(),
phoneNumberId: z.string(),
code: z.string().min(4).max(8),
});
const body = Schema.safeParse(req.body);
if (!body.success) return reply.code(400).send({ error: body.error.flatten() });
const { orgId, phoneNumberId, code } = body.data;
try {
const { decryptSecrets } = await import('../services/organization');
const { whatsappService } = await import('../services/whatsapp');
const org = await prisma.organization.findUnique({
where: { id: orgId },
select: { id: true, name: true, systemUserToken: true, webhookSecret: true, systemUserTokenIssuedAt: true },
});
if (!org) return reply.code(404).send({ error: 'Organization not found' });
if (!org.systemUserToken) return reply.code(400).send({ error: 'Organization has no systemUserToken configured' });
const decrypted = decryptSecrets(org as any);
const result = await whatsappService.verifyPhoneNumber({ accessToken: decrypted.systemUserToken! }, phoneNumberId, code);
logger.info({ orgId, phoneNumberId, actor: (req as any).user?.id }, '[SUPER_ADMIN] Phone number verified');
return { ok: true, metaResponse: result };
} catch (err: any) {
const detail = err.response?.data?.error?.message ?? err.message;
return reply.code(502).send({ error: 'Meta verification failed', detail });
}
});
// ── WhatsApp Template Creation (cross-org) ────────────────────────────────
fastify.post('/whatsapp/templates', async (req, reply) => {
const Schema = z.object({
orgId: z.string(),
name: z.string().min(1).regex(/^[a-z0-9_]+$/, 'Template name must be lowercase snake_case'),
category: z.enum(['MARKETING', 'UTILITY', 'AUTHENTICATION']),
language: z.string().min(2),
body: z.string().min(1).max(1024),
header: z.string().max(60).optional(),
footer: z.string().max(60).optional(),
});
const body = Schema.safeParse(req.body);
if (!body.success) return reply.code(400).send({ error: body.error.flatten() });
const { orgId, name, category, language, body: bodyText, header, footer } = body.data;
try {
const { decryptSecrets } = await import('../services/organization');
const { whatsappService } = await import('../services/whatsapp');
const org = await prisma.organization.findUnique({
where: { id: orgId },
select: { id: true, name: true, wabaId: true, systemUserToken: true, webhookSecret: true, systemUserTokenIssuedAt: true },
});
if (!org) return reply.code(404).send({ error: 'Organization not found' });
if (!org.wabaId || !org.systemUserToken) return reply.code(400).send({ error: 'Organization WhatsApp not configured' });
const decrypted = decryptSecrets(org as any);
const components: any[] = [];
if (header) components.push({ type: 'HEADER', format: 'TEXT', text: header });
components.push({ type: 'BODY', text: bodyText });
if (footer) components.push({ type: 'FOOTER', text: footer });
const result = await whatsappService.createMetaTemplate(
{ accessToken: decrypted.systemUserToken!, wabaId: org.wabaId },
{ name, category, language, components }
);
await prisma.auditLog.create({
data: {
action: 'SUPER_ADMIN_CREATE_TEMPLATE',
actorId: (req as any).user?.id,
resourceId: orgId,
details: { templateName: name, category, language },
},
});
logger.info({ orgId, templateName: name, actor: (req as any).user?.id }, '[SUPER_ADMIN] Template created');
return { ok: true, template: result };
} catch (err: any) {
const detail = err.response?.data?.error?.message ?? err.message;
return reply.code(502).send({ error: 'Meta API error', detail });
}
});
// ── Billing ───────────────────────────────────────────────────────────────
fastify.get('/billing/transactions', async (req, reply) => {
const QuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(20),
orgId: z.string().optional(),
});
const parsed = QuerySchema.safeParse(req.query);
if (!parsed.success) return reply.code(400).send({ error: 'Invalid query params' });
const { page, limit, orgId } = parsed.data;
const where: any = {};
if (orgId) where.organizationId = orgId;
try {
const [txns, total] = await Promise.all([
prisma.walletTransaction.findMany({
where,
include: { organization: { select: { id: true, name: true } } },
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit,
}),
prisma.walletTransaction.count({ where }),
]);
return { data: txns, total, page, limit };
} catch (err) {
logger.error({ err }, '[SUPER_ADMIN] billing transactions failed');
return reply.code(500).send({ error: 'Failed to list transactions' });
}
});
fastify.post('/billing/credits', async (req, reply) => {
const Schema = z.object({
orgId: z.string(),
amount: z.number().int().positive(),
description: z.string().optional(),
});
const body = Schema.safeParse(req.body);
if (!body.success) return reply.code(400).send({ error: body.error.flatten() });
try {
const orgExists = await prisma.organization.findUnique({ where: { id: body.data.orgId }, select: { id: true } });
if (!orgExists) return reply.code(404).send({ error: 'Organization not found' });
const { orgId, amount, description } = body.data;
const updated = await prisma.$transaction(async (tx) => {
const org = await tx.organization.update({
where: { id: orgId },
data: { walletBalance: { increment: amount } },
select: { walletBalance: true, name: true },
});
const txn = await tx.walletTransaction.create({
data: {
organizationId: orgId,
amount,
balanceAfter: org.walletBalance,
type: 'TOP_UP_MANUAL',
description: description || 'Manual credit by super-admin',
actorId: (req as any).user?.id,
},
});
return { org, txn };
});
return { ok: true, newBalance: updated.org.walletBalance, transactionId: updated.txn.id };
} catch (err) {
logger.error({ err }, '[SUPER_ADMIN] add credits failed');
return reply.code(500).send({ error: 'Failed to add credits' });
}
});
// ── Monitoring ────────────────────────────────────────────────────────────
fastify.get('/monitoring/health', async (_req, reply) => {
try {
const [dbPing, redisPing, queueWaiting, queueFailed, queueActive] = await Promise.all([
prisma.$queryRaw`SELECT 1`.then(() => true).catch(() => false),
redis.ping().then(() => true).catch(() => false),
whatsappQueue.getWaitingCount().catch(() => -1),
whatsappQueue.getFailedCount().catch(() => -1),
whatsappQueue.getActiveCount().catch(() => -1),
]);
// Token expiry check — find orgs with systemUserTokenIssuedAt older than 50 days
const tokenExpiryThreshold = new Date(Date.now() - 50 * 24 * 60 * 60 * 1000);
const expiringTokenOrgs = await prisma.organization.findMany({
where: {
systemUserTokenIssuedAt: { lte: tokenExpiryThreshold },
systemUserToken: { not: null }
},
select: { id: true, name: true, systemUserTokenIssuedAt: true }
});
// Low wallet balance
const lowBalanceOrgs = await prisma.organization.findMany({
where: { walletBalance: { lt: 100 }, isHardStopped: false },
select: { id: true, name: true, walletBalance: true }
});
return {
db: { ok: dbPing },
redis: { ok: redisPing },
queue: { waiting: queueWaiting, failed: queueFailed, active: queueActive },
tokenExpiries: expiringTokenOrgs.map(o => ({
orgId: o.id,
orgName: o.name,
issuedAt: o.systemUserTokenIssuedAt,
daysOld: o.systemUserTokenIssuedAt
? Math.floor((Date.now() - o.systemUserTokenIssuedAt.getTime()) / (24 * 60 * 60 * 1000))
: null
})),
lowBalanceOrgs,
};
} catch (err) {
logger.error({ err }, '[SUPER_ADMIN] monitoring failed');
return reply.code(500).send({ error: 'Failed to fetch monitoring data' });
}
});
// ── AI Agentic Command ────────────────────────────────────────────────────
fastify.post('/ai/command', async (req, reply) => {
const Schema = z.object({
command: z.string().min(1),
confirm: z.boolean().default(false),
pendingAction: z.object({
action: z.string(),
params: z.record(z.unknown()),
}).optional(),
});
const body = Schema.safeParse(req.body);
if (!body.success) return reply.code(400).send({ error: body.error.flatten() });
const { command, confirm, pendingAction } = body.data;
// If confirming a pending action
if (confirm && pendingAction) {
return executeSuperAdminAction(pendingAction.action, pendingAction.params, req, reply);
}
// Parse command with AI
const { aiService } = await import('../services/ai');
const { z: zod } = await import('zod');
const CommandSchema = zod.object({
action: zod.enum(['QUERY_STATS', 'LIST_ORGS', 'SUSPEND_ORG', 'ACTIVATE_ORG', 'ADD_CREDITS', 'LIST_LOW_BALANCE', 'LIST_ALERTS', 'LIST_USERS']),
params: zod.record(zod.unknown()).default({}),
confirmation_required: zod.boolean(),
human_summary: zod.string(),
});
try {
const systemPrompt = `You are a super-admin AI assistant for the XAMLÉ platform. Parse the admin's command and return a structured action.
Available actions:
- QUERY_STATS: Get platform statistics (no params needed)
- LIST_ORGS: List organizations (params: search?, plan?)
- SUSPEND_ORG: Suspend an organization (params: orgName or orgId) — confirmation required
- ACTIVATE_ORG: Reactivate a suspended org (params: orgName or orgId) — confirmation required
- ADD_CREDITS: Add wallet credits to an org (params: orgName or orgId, amount) — confirmation required
- LIST_LOW_BALANCE: List orgs with low wallet balance
- LIST_ALERTS: List monitoring alerts
- LIST_USERS: List users (params: orgName?, role?)
Always extract organization names and amounts from the command. Set confirmation_required=true for destructive or financial actions.`;
const { data: parsed } = await aiService.generateStructuredData(
`${systemPrompt}\n\nAdmin command: "${command}"`,
CommandSchema,
0.1
);
if (parsed.confirmation_required) {
return {
status: 'pending_confirmation',
summary: parsed.human_summary,
action: parsed.action,
params: parsed.params,
};
}
return executeSuperAdminAction(parsed.action, parsed.params ?? {}, req, reply);
} catch (err) {
logger.error({ err }, '[SUPER_ADMIN] AI command failed');
return reply.code(503).send({ error: 'AI service unavailable' });
}
});
// ── Audit Logs ────────────────────────────────────────────────────────────
fastify.get('/audit-logs', async (req, _reply) => {
const q = req.query as { page?: string; limit?: string; orgId?: string; actorId?: string; action?: string; from?: string; to?: string };
const page = Math.max(1, parseInt(q.page ?? '1'));
const limit = Math.min(100, parseInt(q.limit ?? '50'));
const where: any = {};
if (q.orgId) where.resourceId = q.orgId;
if (q.actorId) where.actorId = q.actorId;
if (q.action) where.action = { contains: q.action, mode: 'insensitive' };
if (q.from || q.to) {
where.createdAt = {};
if (q.from) where.createdAt.gte = new Date(q.from);
if (q.to) where.createdAt.lte = new Date(q.to);
}
const [logs, total] = await Promise.all([
prisma.auditLog.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit,
}),
prisma.auditLog.count({ where }),
]);
return { logs, total, page, limit };
});
// ── WhatsApp Profiles ─────────────────────────────────────────────────────
fastify.get('/whatsapp/profiles', async (_req, _reply) => {
const orgs = await prisma.organization.findMany({
orderBy: { createdAt: 'desc' },
select: {
id: true, name: true, wabaId: true,
brandingData: true, subscriptionPlan: true, subscriptionStatus: true,
systemUserTokenIssuedAt: true, createdAt: true,
},
});
return { profiles: orgs };
});
fastify.patch<{ Params: { id: string } }>('/whatsapp/profiles/:id', async (req, reply) => {
const Schema = z.object({
name: z.string().min(1).optional(),
brandingData: z.record(z.unknown()).optional(),
});
const body = Schema.safeParse(req.body);
if (!body.success) return reply.code(400).send({ error: body.error.flatten() });
const org = await prisma.organization.findUnique({ where: { id: req.params.id } });
if (!org) return reply.code(404).send({ error: 'Organization not found' });
const updated = await prisma.organization.update({
where: { id: req.params.id },
data: {
...(body.data.name && { name: body.data.name }),
...(body.data.brandingData && { brandingData: body.data.brandingData as Prisma.InputJsonValue }),
},
select: { id: true, name: true, brandingData: true },
});
return updated;
});
// ── WhatsApp Templates (cross-org) ────────────────────────────────────────
fastify.get('/whatsapp/templates', async (_req, _reply) => {
const orgs = await prisma.organization.findMany({
where: { wabaId: { not: null }, subscriptionStatus: 'ACTIVE' },
select: { id: true, name: true, wabaId: true, subscriptionPlan: true },
orderBy: { name: 'asc' },
});
return { orgs };
});
}
async function executeSuperAdminAction(action: string, params: Record<string, unknown>, req: any, reply: any) {
try {
switch (action) {
case 'QUERY_STATS': {
const since24h = new Date(Date.now() - 24 * 60 * 60 * 1000);
const [orgsCount, usersCount, messagesLast24h] = await Promise.all([
prisma.organization.count(),
prisma.user.count({ where: { deletedAt: null } }),
prisma.message.count({ where: { createdAt: { gte: since24h } } }),
]);
return { status: 'success', result: { orgsCount, usersCount, messagesLast24h } };
}
case 'LIST_ORGS': {
const orgs = await prisma.organization.findMany({
where: params.search ? { name: { contains: params.search as string, mode: 'insensitive' } } : undefined,
select: { id: true, name: true, subscriptionPlan: true, walletBalance: true, isHardStopped: true },
take: 20,
orderBy: { createdAt: 'desc' },
});
return { status: 'success', result: orgs };
}
case 'LIST_LOW_BALANCE': {
const orgs = await prisma.organization.findMany({
where: { walletBalance: { lt: 100 } },
select: { id: true, name: true, walletBalance: true },
orderBy: { walletBalance: 'asc' },
});
return { status: 'success', result: orgs };
}
case 'LIST_ALERTS': {
const tokenThreshold = new Date(Date.now() - 50 * 24 * 60 * 60 * 1000);
const [expiringTokens, lowBalance] = await Promise.all([
prisma.organization.findMany({
where: { systemUserTokenIssuedAt: { lte: tokenThreshold }, systemUserToken: { not: null } },
select: { id: true, name: true, systemUserTokenIssuedAt: true },
}),
prisma.organization.findMany({
where: { walletBalance: { lt: 100 } },
select: { id: true, name: true, walletBalance: true },
}),
]);
return { status: 'success', result: { expiringTokens, lowBalance } };
}
case 'SUSPEND_ORG':
case 'ACTIVATE_ORG': {
const suspend = action === 'SUSPEND_ORG';
const orgSearch = (params.orgName || params.orgId) as string;
const org = await prisma.organization.findFirst({
where: params.orgId
? { id: params.orgId as string }
: { name: { contains: orgSearch, mode: 'insensitive' } }
});
if (!org) return reply.code(404).send({ error: `Organization not found: ${orgSearch}` });
// When matched by name (not by unambiguous ID), require exact match or surface a confirmation
if (!params.orgId) {
const exactMatch = org.name.toLowerCase() === (orgSearch as string).toLowerCase();
if (!exactMatch) {
return {
status: 'pending_confirmation',
summary: `Found org '${org.name}' — is this the right one? Confirm to proceed.`,
action,
params: { ...params, orgId: org.id },
};
}
}
await prisma.organization.update({
where: { id: org.id },
data: { isHardStopped: suspend, subscriptionStatus: suspend ? 'SUSPENDED' : 'ACTIVE' }
});
return { status: 'success', result: { orgId: org.id, orgName: org.name, action: suspend ? 'suspended' : 'activated' } };
}
case 'ADD_CREDITS': {
const orgSearch = (params.orgName || params.orgId) as string;
const amount = typeof params.amount === 'number' ? params.amount : parseInt(params.amount as string, 10);
if (!amount || amount <= 0) return reply.code(400).send({ error: 'Invalid amount' });
const org = await prisma.organization.findFirst({
where: params.orgId
? { id: params.orgId as string }
: { name: { contains: orgSearch, mode: 'insensitive' } }
});
if (!org) return reply.code(404).send({ error: `Organization not found: ${orgSearch}` });
// When matched by name (not by unambiguous ID), require exact match or surface a confirmation
if (!params.orgId) {
const exactMatch = org.name.toLowerCase() === (orgSearch as string).toLowerCase();
if (!exactMatch) {
return {
status: 'pending_confirmation',
summary: `Found org '${org.name}' — is this the right one? Confirm to proceed.`,
action,
params: { ...params, orgId: org.id },
};
}
}
const newBalance = org.walletBalance + amount;
await prisma.$transaction([
prisma.organization.update({ where: { id: org.id }, data: { walletBalance: newBalance } }),
prisma.walletTransaction.create({
data: {
organizationId: org.id,
amount,
balanceAfter: newBalance,
type: 'TOP_UP_MANUAL',
description: `Super-admin AI command: add ${amount} credits`,
actorId: req.user?.id,
}
})
]);
return { status: 'success', result: { orgId: org.id, orgName: org.name, amount, newBalance } };
}
case 'LIST_USERS': {
const where: any = { deletedAt: null };
if (params.role) where.role = params.role;
if (params.orgName) {
const org = await prisma.organization.findFirst({ where: { name: { contains: params.orgName as string, mode: 'insensitive' } } });
if (org) where.organizationId = org.id;
}
const users = await prisma.user.findMany({ where, select: { id: true, name: true, email: true, role: true, organizationId: true }, take: 20 });
return { status: 'success', result: users };
}
default:
return reply.code(400).send({ error: `Unknown action: ${action}` });
}
} catch (err) {
logger.error({ err, action }, '[SUPER_ADMIN] action execution failed');
return reply.code(500).send({ error: 'Action execution failed' });
}
}