config = $config; } /** * POST /api/auth/login */ public function login(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $body = $request->getParsedBody(); $email = trim($body['email'] ?? ''); $password = $body['password'] ?? ''; if (empty($email) || empty($password)) { return Response::validationError($response, [ 'email' => 'Email is required', 'password' => 'Password is required', ]); } // Check if it's a platform admin login $admin = Database::fetch( 'SELECT * FROM platform_admins WHERE email = $1', [$email] ); if ($admin && password_verify($password, $admin['password_hash'])) { return $this->createTokenResponse($response, $admin, 'platform_admin'); } // Check if it's a tenant user login $user = Database::fetch( 'SELECT u.*, t.status as tenant_status FROM users u JOIN tenants t ON t.id = u.tenant_id WHERE u.email = $1', [$email] ); if ($user && password_verify($password, $user['password_hash'])) { if ($user['tenant_status'] !== 'active') { return Response::error($response, 'Account is not active', 403); } if ($user['status'] !== 'active') { return Response::error($response, 'User is inactive', 403); } // Update last login Database::update('users', ['last_login_at' => 'NOW()'], 'id = $1', [$user['id']]); return $this->createTokenResponse($response, $user, 'user'); } return Response::error($response, 'Invalid credentials', 401); } /** * POST /api/auth/register */ public function register(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $body = $request->getParsedBody(); $name = trim($body['name'] ?? ''); $email = trim($body['email'] ?? ''); $password = $body['password'] ?? ''; $phone = trim($body['phone'] ?? ''); $address = trim($body['address'] ?? ''); // Validation $errors = []; if (empty($name)) $errors['name'] = 'Name is required'; if (empty($email)) $errors['email'] = 'Email is required'; elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors['email'] = 'Invalid email format'; 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 already exists $exists = Database::fetch( 'SELECT id FROM tenants WHERE email = $1', [$email] ); if ($exists) { return Response::error($response, 'Email already registered', 409); } // Generate slug from name $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++; } // Create tenant $tenantId = Database::insert('tenants', [ 'name' => $name, 'slug' => $slug, 'email' => $email, 'phone' => $phone, 'address' => $address, 'status' => 'pending_approval', ]); // Create first user (admin of the tenant) $userId = Database::insert('users', [ 'tenant_id' => $tenantId, 'email' => $email, 'password_hash' => password_hash($password, PASSWORD_DEFAULT), 'first_name' => $name, 'last_name' => '', 'user_type' => 'admin', 'status' => 'pending', // Needs email verification first ]); // Enable default modules (office, calendar, messenger) $defaultModules = Database::fetchAll('SELECT id FROM modules WHERE key IN (\'office\', \'calendar\', \'messenger\')'); foreach ($defaultModules as $module) { Database::insert('tenant_modules', [ 'tenant_id' => $tenantId, 'module_id' => $module['id'], 'status' => 'enabled', ]); } return Response::success($response, [ 'tenant_id' => $tenantId, 'user_id' => $userId, 'message' => 'Registration successful. Pending admin approval.', ], 'Registration successful', 201); } /** * POST /api/auth/verify-email */ public function verifyEmail(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $body = $request->getParsedBody(); $token = $body['token'] ?? ''; if (empty($token)) { return Response::validationError($response, ['token' => 'Token is required']); } // TODO: Verify token and activate user return Response::success($response, null, 'Email verified successfully'); } /** * GET /api/auth/me */ public function me(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $auth = $request->getAttribute('auth'); if ($auth['type'] === 'platform_admin') { $data = Database::fetch( 'SELECT id, email, name, role, created_at, last_login_at FROM platform_admins WHERE id = $1', [$auth['id']] ); $data['type'] = 'platform_admin'; } else { $data = Database::fetch( 'SELECT u.id, u.email, u.first_name, u.last_name, u.user_type, u.status, u.avatar_url, u.last_login_at, t.id as tenant_id, t.name as tenant_name, t.slug as tenant_slug, t.status as tenant_status FROM users u JOIN tenants t ON t.id = u.tenant_id WHERE u.id = $1', [$auth['id']] ); $data['type'] = 'user'; } return Response::success($response, $data); } /** * POST /api/auth/refresh */ public function refresh(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $body = $request->getParsedBody(); $refreshToken = $body['refresh_token'] ?? ''; if (empty($refreshToken)) { return Response::validationError($response, ['refresh_token' => 'Refresh token is required']); } try { $decoded = JWT::decode($refreshToken, new Key($this->config['jwt']['secret'], 'HS256')); if ($decoded->type !== 'refresh') { return Response::error($response, 'Invalid token type', 401); } // Get user/admin if ($decoded->user_type === 'platform_admin') { $user = Database::fetch('SELECT * FROM platform_admins WHERE id = $1', [$decoded->sub]); } else { $user = Database::fetch('SELECT * FROM users WHERE id = $1', [$decoded->sub]); } if (!$user) { return Response::error($response, 'User not found', 401); } return $this->createTokenResponse($response, $user, $decoded->user_type); } catch (\Exception $e) { return Response::error($response, 'Invalid refresh token', 401); } } /** * POST /api/auth/logout */ public function logout(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { // JWT is stateless, so logout is handled client-side // Here we could blacklist the token if needed return Response::success($response, null, 'Logged out successfully'); } /** * POST /api/auth/forgot-password */ public function forgotPassword(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $body = $request->getParsedBody(); $email = trim($body['email'] ?? ''); if (empty($email)) { return Response::validationError($response, ['email' => 'Email is required']); } // Check if email exists $admin = Database::fetch('SELECT id FROM platform_admins WHERE email = $1', [$email]); $user = Database::fetch('SELECT id FROM users WHERE email = $1', [$email]); // Always return success to prevent email enumeration if (!$admin && !$user) { return Response::success($response, null, 'If the email exists, a reset link has been sent'); } // TODO: Generate reset token, send email return Response::success($response, null, 'If the email exists, a reset link has been sent'); } /** * Create JWT token response */ private function createTokenResponse(ResponseInterface $response, array $user, string $type): ResponseInterface { $now = time(); $expiry = $this->config['jwt']['expiry']; $refreshExpiry = $this->config['jwt']['refresh_expiry']; $payload = [ 'iss' => 'fahrschuldesk', 'sub' => $user['id'], 'iat' => $now, 'exp' => $now + $expiry, 'type' => 'access', 'user_type' => $type, ]; $refreshPayload = [ 'iss' => 'fahrschuldesk', 'sub' => $user['id'], 'iat' => $now, 'exp' => $now + $refreshExpiry, 'type' => 'refresh', 'user_type' => $type, ]; $accessToken = JWT::encode($payload, $this->config['jwt']['secret'], 'HS256'); $refreshToken = JWT::encode($refreshPayload, $this->config['jwt']['secret'], 'HS256'); return Response::json($response, [ 'success' => true, 'data' => [ 'access_token' => $accessToken, 'refresh_token' => $refreshToken, 'token_type' => 'Bearer', 'expires_in' => $expiry, ], ]); } }