import { Router } from "express"; import { storage } from "../storage"; import { requireSuperAdmin, authenticateToken, requirePermission, type AuthenticatedRequest, type SuperAdminRequest } from "../middleware/auth.middleware"; import { authLimiter } from "./shared"; const router = Router(); // ===================== // Super Admin Endpoints // ===================== // GET /api/superadmin/exists — public, returns whether any super admin exists router.get('/api/superadmin/exists', async (_req, res) => { try { const count = await storage.getSuperAdminCount(); return res.json({ exists: count > 0 }); } catch (error) { console.error('SuperAdmin exists check error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // POST /api/superadmin/seed — create first super admin (only if none exist) router.post('/api/superadmin/seed', authLimiter, async (req, res) => { try { const { superAdminService } = await import('../services/superadmin.service'); const { email, password, name, seedToken } = req.body; if (!email || !password || !name) { return res.status(400).json({ success: false, error: 'Необходимы email, password и name' }); } // If there are already super admins — always reject const count = await storage.getSuperAdminCount(); if (count > 0) { return res.status(409).json({ success: false, error: 'Суперадмин уже существует' }); } // If SUPERADMIN_SEED_TOKEN is set — require it; otherwise allow without token (first-run UI flow) const expectedToken = process.env.SUPERADMIN_SEED_TOKEN; if (expectedToken && seedToken !== expectedToken) { return res.status(403).json({ success: false, error: 'Неверный seed token' }); } const result = await superAdminService.seedFirstSuperAdmin(email, password, name); if (result.success) { // Auto-login: return a token so the UI can redirect immediately const loginResult = await superAdminService.login(email, password); if (loginResult.success) { return res.status(201).json({ success: true, token: loginResult.token, superAdmin: loginResult.superAdmin }); } return res.status(201).json({ success: true }); } return res.status(400).json({ success: false, error: result.error }); } catch (error) { console.error('SuperAdmin seed error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // POST /api/superadmin/forgot-password — request password reset link router.post('/api/superadmin/forgot-password', authLimiter, async (req, res) => { try { const { superAdminService } = await import('../services/superadmin.service'); const { email } = req.body; if (!email) { return res.status(400).json({ success: false, error: 'Необходим email' }); } await superAdminService.requestPasswordReset(email); return res.json({ success: true }); } catch (error) { console.error('SuperAdmin forgot password error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // POST /api/superadmin/reset-password — confirm reset with token and new password router.post('/api/superadmin/reset-password', authLimiter, async (req, res) => { try { const { superAdminService } = await import('../services/superadmin.service'); const { token, password } = req.body; if (!token || !password) { return res.status(400).json({ success: false, error: 'Необходимы token и password' }); } if (password.length < 8) { return res.status(400).json({ success: false, error: 'Пароль должен быть не менее 8 символов' }); } const result = await superAdminService.confirmPasswordReset(token, password); if (result.success) { return res.json({ success: true }); } return res.status(400).json({ success: false, error: result.error }); } catch (error) { console.error('SuperAdmin reset password error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // POST /api/superadmin/login router.post('/api/superadmin/login', authLimiter, async (req, res) => { try { const { superAdminService } = await import('../services/superadmin.service'); const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ success: false, error: 'Необходимы email и password' }); } const result = await superAdminService.login(email, password); if (result.success) { return res.json({ success: true, token: result.token, superAdmin: result.superAdmin }); } return res.status(401).json({ success: false, error: result.error }); } catch (error) { console.error('SuperAdmin login error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // GET /api/superadmin/admins — list all super admins router.get('/api/superadmin/admins', requireSuperAdmin, async (_req, res) => { try { const admins = await storage.getAllSuperAdmins(); const safe = admins.map(a => ({ id: a.id, email: a.email, name: a.name, createdAt: a.createdAt, lastLogin: a.lastLogin })); return res.json({ success: true, admins: safe }); } catch (error) { console.error('SuperAdmin list error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // POST /api/superadmin/admins — create additional super admin router.post('/api/superadmin/admins', requireSuperAdmin, authLimiter, async (req, res) => { try { const { superAdminService } = await import('../services/superadmin.service'); const { email, password, name } = req.body; if (!email || !password || !name) { return res.status(400).json({ success: false, error: 'Необходимы email, password и name' }); } if (password.length < 8) { return res.status(400).json({ success: false, error: 'Пароль должен быть не менее 8 символов' }); } const result = await superAdminService.createAdditionalSuperAdmin(email, password, name); if (result.success) { return res.status(201).json({ success: true, superAdmin: result.superAdmin }); } return res.status(400).json({ success: false, error: result.error }); } catch (error) { console.error('SuperAdmin create error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // DELETE /api/superadmin/admins/:id — delete super admin router.delete('/api/superadmin/admins/:id', requireSuperAdmin, async (req: SuperAdminRequest, res) => { try { const { superAdminService } = await import('../services/superadmin.service'); const idToDelete = parseInt(req.params.id); if (isNaN(idToDelete)) return res.status(400).json({ success: false, error: 'Некорректный ID' }); const currentId = req.superAdmin?.id ?? 0; const result = await superAdminService.deleteSuperAdmin(idToDelete, currentId); if (result.success) { return res.json({ success: true }); } return res.status(400).json({ success: false, error: result.error }); } catch (error) { console.error('SuperAdmin delete error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // GET /api/superadmin/organizations — list all with stats // requireSuperAdmin already sets up the SA db context (app.is_superadmin='true') // via AsyncLocalStorage, so storage.getAllOrganizationsWithStats() automatically // bypasses tenant RLS policies without any additional wrapping here. router.get('/api/superadmin/organizations', requireSuperAdmin, async (_req, res) => { try { const orgs = await storage.getAllOrganizationsWithStats(); return res.json({ success: true, organizations: orgs }); } catch (error) { console.error('SuperAdmin orgs error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // GET /api/superadmin/organizations/:id/stats router.get('/api/superadmin/organizations/:id/stats', requireSuperAdmin, async (req, res) => { try { const orgId = parseInt(req.params.id); if (isNaN(orgId)) return res.status(400).json({ success: false, error: 'Некорректный ID' }); const [org, stats] = await Promise.all([ storage.getOrganization(orgId), storage.getOrganizationDetailedStats(orgId), ]); if (!org) return res.status(404).json({ success: false, error: 'Организация не найдена' }); return res.json({ success: true, organization: org, stats }); } catch (error) { console.error('SuperAdmin org stats error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // PATCH /api/superadmin/organizations/:id — update isActive etc router.patch('/api/superadmin/organizations/:id', requireSuperAdmin, async (req, res) => { try { const orgId = parseInt(req.params.id); if (isNaN(orgId)) return res.status(400).json({ success: false, error: 'Некорректный ID' }); const { isActive } = req.body; if (typeof isActive !== 'boolean') { return res.status(400).json({ success: false, error: 'Поле isActive должно быть булевым значением' }); } const updates: { isActive: boolean } = { isActive }; const org = await storage.updateOrganization(orgId, updates); if (!org) return res.status(404).json({ success: false, error: 'Организация не найдена' }); return res.json({ success: true, organization: org }); } catch (error) { console.error('SuperAdmin update org error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // ===================== // Billing — Super Admin // ===================== // GET /api/superadmin/organizations/:id/billing router.get('/api/superadmin/organizations/:id/billing', requireSuperAdmin, async (req, res) => { try { const orgId = parseInt(req.params.id); if (isNaN(orgId)) return res.status(400).json({ success: false, error: 'Некорректный ID' }); const org = await storage.getOrganization(orgId); if (!org) return res.status(404).json({ success: false, error: 'Организация не найдена' }); const billing = await storage.getOrCreateBillingSettings(orgId); return res.json({ success: true, billing, organization: org }); } catch (error) { console.error('SuperAdmin billing get error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // POST /api/superadmin/organizations/:id/billing/topup — пополнить баланс router.post('/api/superadmin/organizations/:id/billing/topup', requireSuperAdmin, async (req: SuperAdminRequest, res) => { try { const orgId = parseInt(req.params.id); if (isNaN(orgId)) return res.status(400).json({ success: false, error: 'Некорректный ID' }); const { amount, description } = req.body as { amount: unknown; description?: string }; const amountNum = parseFloat(String(amount)); if (!amount || isNaN(amountNum) || amountNum <= 0) { return res.status(400).json({ success: false, error: 'Сумма должна быть положительным числом' }); } const tx = await storage.addBillingCredit(orgId, amountNum, description || 'Пополнение баланса', req.superAdmin?.id); return res.json({ success: true, transaction: tx }); } catch (error) { console.error('SuperAdmin billing topup error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // PATCH /api/superadmin/organizations/:id/billing — изменить настройки (pricePerUser, currency) router.patch('/api/superadmin/organizations/:id/billing', requireSuperAdmin, async (req, res) => { try { const orgId = parseInt(req.params.id); if (isNaN(orgId)) return res.status(400).json({ success: false, error: 'Некорректный ID' }); const { pricePerUser, currency } = req.body as { pricePerUser?: unknown; currency?: unknown }; const updates: { pricePerUser?: string; currency?: string } = {}; if (pricePerUser !== undefined) { const p = parseFloat(String(pricePerUser)); if (isNaN(p) || p < 0) return res.status(400).json({ success: false, error: 'Некорректная цена' }); updates.pricePerUser = p.toFixed(2); } if (currency) updates.currency = String(currency).slice(0, 10); const billing = await storage.updateBillingSettings(orgId, updates); return res.json({ success: true, billing }); } catch (error) { console.error('SuperAdmin billing update error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // POST /api/superadmin/organizations/:id/billing/unblock — снять блокировку router.post('/api/superadmin/organizations/:id/billing/unblock', requireSuperAdmin, async (req, res) => { try { const orgId = parseInt(req.params.id); if (isNaN(orgId)) return res.status(400).json({ success: false, error: 'Некорректный ID' }); await storage.setBillingBlocked(orgId, false); return res.json({ success: true }); } catch (error) { console.error('SuperAdmin billing unblock error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // GET /api/superadmin/organizations/:id/billing/transactions router.get('/api/superadmin/organizations/:id/billing/transactions', requireSuperAdmin, async (req, res) => { try { const orgId = parseInt(req.params.id); if (isNaN(orgId)) return res.status(400).json({ success: false, error: 'Некорректный ID' }); const transactions = await storage.getBillingTransactions(orgId, 50); return res.json({ success: true, transactions }); } catch (error) { console.error('SuperAdmin billing transactions error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // ===================== // Billing — Org Admin // ===================== // GET /api/billing/summary — текущий баланс, активные пользователи и статус router.get('/api/billing/summary', authenticateToken, requirePermission('billing.manage'), async (req: AuthenticatedRequest, res) => { try { const summary = await storage.getBillingSummary(req.organizationId!); return res.json({ success: true, summary }); } catch (error) { console.error('Billing summary error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // GET /api/billing/transactions — история транзакций router.get('/api/billing/transactions', authenticateToken, requirePermission('billing.manage'), async (req: AuthenticatedRequest, res) => { try { const transactions = await storage.getBillingTransactions(req.organizationId!, 20); return res.json({ success: true, transactions }); } catch (error) { console.error('Billing transactions error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); export default router;