config = $config; } /** * GET /api/admin/tenants */ public function listTenants(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $page = (int)($request->getQueryParams()['page'] ?? 1); $limit = min((int)($request->getQueryParams()['limit'] ?? 20), 100); $offset = ($page - 1) * $limit; $status = $request->getQueryParams()['status'] ?? null; $where = ''; $params = []; if ($status) { $where = 'WHERE status = $1'; $params[] = $status; } $total = Database::fetch("SELECT COUNT(*) as count FROM tenants {$where}", $params)['count']; $tenants = Database::fetchAll( "SELECT id, name, slug, email, status, trial_ends_at, is_self_hosted, created_at FROM tenants {$where} ORDER BY created_at DESC LIMIT {$limit} OFFSET {$offset}", $params ); // Get module counts foreach ($tenants as &$tenant) { $modules = Database::fetchAll( 'SELECT m.key FROM modules m JOIN tenant_modules tm ON tm.module_id = m.id WHERE tm.tenant_id = $1 AND tm.status = $2', [$tenant['id'], 'enabled'] ); $tenant['modules'] = array_column($modules, 'key'); $tenant['module_count'] = count($modules); } return Response::success($response, [ 'tenants' => $tenants, 'pagination' => [ 'page' => $page, 'limit' => $limit, 'total' => (int)$total, 'pages' => ceil($total / $limit), ], ]); } /** * POST /api/admin/tenants */ public function createTenant(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $body = $request->getParsedBody(); $name = trim($body['name'] ?? ''); $email = trim($body['email'] ?? ''); $phone = trim($body['phone'] ?? ''); $address = trim($body['address'] ?? ''); $errors = []; if (empty($name)) $errors['name'] = 'Name is required'; if (empty($email)) $errors['email'] = 'Email is required'; if (!empty($errors)) { return Response::validationError($response, $errors); } $slug = strtolower(preg_replace('/[^a-z0-9]+/', '-', $name)); $originalSlug = $slug; $counter = 1; while (Database::fetch('SELECT id FROM tenants WHERE slug = $1', [$slug])) { $slug = $originalSlug . '-' . $counter++; } $tenantId = Database::insert('tenants', [ 'name' => $name, 'slug' => $slug, 'email' => $email, 'phone' => $phone, 'address' => $address, 'status' => 'active', ]); $tenant = Database::fetch('SELECT * FROM tenants WHERE id = $1', [$tenantId]); return Response::success($response, $tenant, 'Tenant created', 201); } /** * GET /api/admin/tenants/:id */ public function getTenant(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { $id = $args['id']; $tenant = Database::fetch('SELECT * FROM tenants WHERE id = $1', [$id]); if (!$tenant) { return Response::notFound($response, 'Tenant not found'); } // Get branches $tenant['branches'] = Database::fetchAll( 'SELECT * FROM branches WHERE tenant_id = $1 ORDER BY is_headquarters DESC, name ASC', [$id] ); // Get users count $tenant['user_count'] = Database::fetch( 'SELECT COUNT(*) as count FROM users WHERE tenant_id = $1', [$id] )['count']; // Get modules $modules = Database::fetchAll( 'SELECT m.*, tm.status, tm.enabled_at FROM modules m JOIN tenant_modules tm ON tm.module_id = m.id WHERE tm.tenant_id = $1', [$id] ); $tenant['modules'] = $modules; return Response::success($response, $tenant); } /** * PUT /api/admin/tenants/:id */ public function updateTenant(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { $id = $args['id']; $body = $request->getParsedBody(); $tenant = Database::fetch('SELECT * FROM tenants WHERE id = $1', [$id]); if (!$tenant) { return Response::notFound($response, 'Tenant not found'); } $allowed = ['name', 'email', 'phone', 'address', 'status', 'primary_color', 'secondary_color', 'logo_url', 'custom_css']; $data = array_intersect_key($body, array_flip($allowed)); if (!empty($data)) { Database::update('tenants', $data, 'id = $1', [$id]); } $updated = Database::fetch('SELECT * FROM tenants WHERE id = $1', [$id]); return Response::success($response, $updated, 'Tenant updated'); } /** * DELETE /api/admin/tenants/:id */ public function deleteTenant(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { $id = $args['id']; $tenant = Database::fetch('SELECT * FROM tenants WHERE id = $1', [$id]); if (!$tenant) { return Response::notFound($response, 'Tenant not found'); } // Check if tenant has users $userCount = Database::fetch('SELECT COUNT(*) as count FROM users WHERE tenant_id = $1', [$id])['count']; if ($userCount > 0) { return Response::error($response, 'Cannot delete tenant with users. Delete users first.', 400); } Database::delete('tenants', 'id = $1', [$id]); return Response::success($response, null, 'Tenant deleted'); } /** * POST /api/admin/tenants/:id/approve */ public function approveTenant(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { $id = $args['id']; $tenant = Database::fetch('SELECT * FROM tenants WHERE id = $1', [$id]); if (!$tenant) { return Response::notFound($response, 'Tenant not found'); } if ($tenant['status'] !== 'pending_approval') { return Response::error($response, 'Tenant is not pending approval', 400); } // Set to trial for 14 days $trialEnds = date('Y-m-d H:i:s', strtotime('+14 days')); Database::update('tenants', [ 'status' => 'trial', 'trial_ends_at' => $trialEnds, ], 'id = $1', [$id]); $updated = Database::fetch('SELECT * FROM tenants WHERE id = $1', [$id]); return Response::success($response, $updated, 'Tenant approved, trial started'); } /** * POST /api/admin/tenants/:id/suspend */ public function suspendTenant(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { $id = $args['id']; $tenant = Database::fetch('SELECT * FROM tenants WHERE id = $1', [$id]); if (!$tenant) { return Response::notFound($response, 'Tenant not found'); } Database::update('tenants', ['status' => 'suspended'], 'id = $1', [$id]); return Response::success($response, null, 'Tenant suspended'); } /** * GET /api/admin/tenants/:id/modules */ public function getTenantModules(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { $id = $args['id']; $modules = Database::fetchAll( 'SELECT m.*, tm.status, tm.enabled_at, tm.expires_at, tm.trial_ends_at FROM modules m LEFT JOIN tenant_modules tm ON tm.module_id = m.id AND tm.tenant_id = $1 ORDER BY m.name', [$id] ); return Response::success($response, $modules); } /** * POST /api/admin/tenants/:id/modules */ public function enableTenantModule(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { $id = $args['id']; $body = $request->getParsedBody(); $moduleId = $body['module_id'] ?? ''; $expiresAt = $body['expires_at'] ?? null; $trialEndsAt = $body['trial_ends_at'] ?? null; if (empty($moduleId)) { return Response::validationError($response, ['module_id' => 'Module ID is required']); } // Check if module exists $module = Database::fetch('SELECT * FROM modules WHERE id = $1', [$moduleId]); if (!$module) { return Response::notFound($response, 'Module not found'); } // Check if already enabled $existing = Database::fetch( 'SELECT * FROM tenant_modules WHERE tenant_id = $1 AND module_id = $2', [$id, $moduleId] ); if ($existing) { return Response::error($response, 'Module already enabled for this tenant', 409); } Database::insert('tenant_modules', [ 'tenant_id' => $id, 'module_id' => $moduleId, 'status' => 'enabled', 'expires_at' => $expiresAt, 'trial_ends_at' => $trialEndsAt, ]); return Response::success($response, null, 'Module enabled', 201); } /** * DELETE /api/admin/tenants/:id/modules/:moduleId */ public function disableTenantModule(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { $tenantId = $args['id']; $moduleId = $args['moduleId']; Database::delete('tenant_modules', 'tenant_id = $1 AND module_id = $2', [$tenantId, $moduleId]); return Response::success($response, null, 'Module disabled'); } /** * GET /api/admin/platform-modules */ public function listPlatformModules(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $modules = Database::fetchAll('SELECT * FROM modules ORDER BY name'); return Response::success($response, $modules); } /** * GET /api/admin/audit-logs */ public function listAuditLogs(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $page = (int)($request->getQueryParams()['page'] ?? 1); $limit = min((int)($request->getQueryParams()['limit'] ?? 50), 100); $offset = ($page - 1) * $limit; $tenantId = $request->getQueryParams()['tenant_id'] ?? null; $action = $request->getQueryParams()['action'] ?? null; $entityType = $request->getQueryParams()['entity_type'] ?? null; $where = []; $params = []; $paramIndex = 1; if ($tenantId) { $where[] = "tenant_id = \${$paramIndex}"; $params[] = $tenantId; $paramIndex++; } if ($action) { $where[] = "action = \${$paramIndex}"; $params[] = $action; $paramIndex++; } if ($entityType) { $where[] = "entity_type = \${$paramIndex}"; $params[] = $entityType; $paramIndex++; } $whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; $total = Database::fetch("SELECT COUNT(*) as count FROM audit_logs {$whereClause}", $params)['count']; $logs = Database::fetchAll( "SELECT al.*, pa.name as admin_name, pa.email as admin_email, u.first_name, u.last_name, u.email as user_email, t.name as tenant_name FROM audit_logs al LEFT JOIN platform_admins pa ON al.user_id = al.tenant_id IS NULL AND pa.id = al.user_id LEFT JOIN users u ON al.tenant_id IS NOT NULL AND u.id = al.user_id LEFT JOIN tenants t ON t.id = al.tenant_id {$whereClause} ORDER BY al.created_at DESC LIMIT {$limit} OFFSET {$offset}", $params ); return Response::success($response, [ 'logs' => $logs, 'pagination' => [ 'page' => $page, 'limit' => $limit, 'total' => (int)$total, 'pages' => ceil($total / $limit), ], ]); } /** * GET /api/admin/invoices */ public function listInvoices(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $page = (int)($request->getQueryParams()['page'] ?? 1); $limit = min((int)($request->getQueryParams()['limit'] ?? 20), 100); $offset = ($page - 1) * $limit; $status = $request->getQueryParams()['status'] ?? null; $tenantId = $request->getQueryParams()['tenant_id'] ?? null; $where = []; $params = []; $paramIndex = 1; if ($status) { $where[] = "i.status = \${$paramIndex}"; $params[] = $status; $paramIndex++; } if ($tenantId) { $where[] = "i.tenant_id = \${$paramIndex}"; $params[] = $tenantId; $paramIndex++; } $whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; $total = Database::fetch("SELECT COUNT(*) as count FROM invoices i {$whereClause}", $params)['count']; $invoices = Database::fetchAll( "SELECT i.*, t.name as tenant_name, t.slug as tenant_slug FROM invoices i JOIN tenants t ON t.id = i.tenant_id {$whereClause} ORDER BY i.created_at DESC LIMIT {$limit} OFFSET {$offset}", $params ); return Response::success($response, [ 'invoices' => $invoices, 'pagination' => [ 'page' => $page, 'limit' => $limit, 'total' => (int)$total, 'pages' => ceil($total / $limit), ], ]); } /** * PUT /api/admin/invoices/:id/mark-paid */ public function markInvoicePaid(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { $id = $args['id']; $invoice = Database::fetch('SELECT * FROM invoices WHERE id = $1', [$id]); if (!$invoice) { return Response::notFound($response, 'Invoice not found'); } Database::update('invoices', [ 'status' => 'paid', 'paid_at' => date('Y-m-d H:i:s'), ], 'id = $1', [$id]); $updated = Database::fetch('SELECT * FROM invoices WHERE id = $1', [$id]); return Response::success($response, $updated, 'Invoice marked as paid'); } /** * GET /api/admin/platform-admins */ public function listPlatformAdmins(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $admins = Database::fetchAll( 'SELECT id, email, name, role, created_at, last_login_at FROM platform_admins ORDER BY created_at DESC' ); return Response::success($response, $admins); } /** * POST /api/admin/platform-admins */ public function createPlatformAdmin(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $body = $request->getParsedBody(); $email = trim($body['email'] ?? ''); $name = trim($body['name'] ?? ''); $password = $body['password'] ?? ''; $role = $body['role'] ?? 'support'; $errors = []; if (empty($email)) $errors['email'] = 'Email is required'; if (empty($name)) $errors['name'] = 'Name is required'; if (empty($password)) $errors['password'] = 'Password is required'; elseif (strlen($password) < 8) $errors['password'] = 'Password must be at least 8 characters'; if (!empty($errors)) { return Response::validationError($response, $errors); } // Check if email exists if (Database::fetch('SELECT id FROM platform_admins WHERE email = $1', [$email])) { return Response::error($response, 'Email already exists', 409); } $id = Database::insert('platform_admins', [ 'email' => $email, 'name' => $name, 'password_hash' => password_hash($password, PASSWORD_DEFAULT), 'role' => $role, ]); $admin = Database::fetch( 'SELECT id, email, name, role, created_at FROM platform_admins WHERE id = $1', [$id] ); return Response::success($response, $admin, 'Platform admin created', 201); } /** * PUT /api/admin/platform-admins/:id */ public function updatePlatformAdmin(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { $id = $args['id']; $body = $request->getParsedBody(); $allowed = ['name', 'role']; if (!empty($body['password'])) { $allowed[] = 'password_hash'; $body['password_hash'] = password_hash($body['password'], PASSWORD_DEFAULT); } $data = array_intersect_key($body, array_flip($allowed)); if (!empty($data)) { Database::update('platform_admins', $data, 'id = $1', [$id]); } $admin = Database::fetch( 'SELECT id, email, name, role, created_at, last_login_at FROM platform_admins WHERE id = $1', [$id] ); return Response::success($response, $admin, 'Platform admin updated'); } /** * DELETE /api/admin/platform-admins/:id */ public function deletePlatformAdmin(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { $id = $args['id']; $auth = $request->getAttribute('auth'); if ($auth['id'] === $id) { return Response::error($response, 'Cannot delete yourself', 400); } Database::delete('platform_admins', 'id = $1', [$id]); return Response::success($response, null, 'Platform admin deleted'); } }