databasePath); $app = AppFactory::create(); $app->addBodyParsingMiddleware(); $app->addRoutingMiddleware(); $errorMiddleware = $app->addErrorMiddleware($config->displayErrors, true, true); $errorMiddleware->setDefaultErrorHandler( new JsonErrorHandler($app->getResponseFactory(), $config->displayErrors) ); // --- Wiring ----------------------------------------------------------------- $users = new UserRepository($database->pdo()); $projects = new ProjectRepository($database->pdo()); $cards = new CardRepository($database->pdo()); $cardStatuses = new CardStatusRepository($database->pdo()); $verificationTokens = new EmailVerificationRepository($database->pdo()); $passkeys = new PasskeyRepository($database->pdo()); $webauthnChallenges = new WebAuthnChallengeRepository($database->pdo()); $jwt = new JwtService($config->jwtSecret, $config->jwtTtl); $session = new SessionPayload($jwt, $verificationTokens, $passkeys); /** @var Mailer $mailer */ $mailer = $config->mail->transport === 'log' ? new LogMailer($config->mail->logPath) : new PhpMailerMailer($config->mail); $verifier = new EmailVerifier($verificationTokens, $users, $mailer, $config->appUrl); // 'none' attestation: verify the credential is a legitimate WebAuthn response // without checking authenticator provenance against a root CA -- the usual // choice for "log in with the same device you registered", not a fleet of // company-issued security keys. useBase64UrlEncoding=true so the challenge/ // ids in getCreateArgs()/getGetArgs() JSON straight to the frontend. $webAuthn = new WebAuthn($config->webauthnRpName, $config->webauthnRpId, ['none'], true); $authController = new AuthController($users, $session, $verifier, $config->allowRegistration); $emailController = new EmailVerificationController($users, $verificationTokens, $verifier, $session); $projectController = new ProjectController($projects, $cardStatuses); $cardController = new CardController($projects, $cards, $cardStatuses); $cardStatusController = new CardStatusController($projects, $cardStatuses, $cards); $passkeyController = new PasskeyController($webAuthn, $passkeys, $webauthnChallenges, $users, $session); $authMiddleware = new AuthMiddleware($jwt, $users); // --- Routes --------------------------------------------------------------- $app->group('/api', function (RouteCollectorProxy $group) use ( $authController, $emailController, $projectController, $cardController, $cardStatusController, $passkeyController, $authMiddleware, ) { $group->get('/health', function (Request $request, Response $response): Response { $response->getBody()->write((string) json_encode(['status' => 'ok'])); return $response->withHeader('Content-Type', 'application/json'); }); $group->post('/auth/magic-link', [$authController, 'requestLoginLink']); $group->post('/auth/verify-email', [$emailController, 'verify']); $group->post('/auth/passkey/options', [$passkeyController, 'loginOptions']); $group->post('/auth/passkey/verify', [$passkeyController, 'loginVerify']); $group->get('/me', [$authController, 'me'])->add($authMiddleware); $group->post('/email/change', [$emailController, 'requestChange'])->add($authMiddleware); $group->group('/passkeys', function (RouteCollectorProxy $passkeys) use ($passkeyController) { $passkeys->get('', [$passkeyController, 'index']); $passkeys->post('', [$passkeyController, 'store']); $passkeys->post('/options', [$passkeyController, 'registerOptions']); $passkeys->delete('/{passkeyId:[0-9]+}', [$passkeyController, 'destroy']); })->add($authMiddleware); $group->group('/projects', function (RouteCollectorProxy $projects) use ( $projectController, $cardController, $cardStatusController, ) { $projects->get('', [$projectController, 'index']); $projects->post('', [$projectController, 'store']); $projects->get('/{projectId:[0-9]+}', [$projectController, 'show']); $projects->patch('/{projectId:[0-9]+}', [$projectController, 'update']); $projects->delete('/{projectId:[0-9]+}', [$projectController, 'destroy']); $projects->get('/{projectId:[0-9]+}/statuses', [$cardStatusController, 'index']); $projects->post('/{projectId:[0-9]+}/statuses', [$cardStatusController, 'store']); $projects->put('/{projectId:[0-9]+}/statuses/order', [$cardStatusController, 'reorder']); $projects->delete('/{projectId:[0-9]+}/statuses/{statusId:[0-9]+}', [$cardStatusController, 'destroy']); $projects->get('/{projectId:[0-9]+}/cards', [$cardController, 'index']); $projects->post('/{projectId:[0-9]+}/cards', [$cardController, 'store']); })->add($authMiddleware); // The inbox has no project of its own -- a user's unfiled cards. $group->group('/inbox', function (RouteCollectorProxy $inbox) use ($cardController) { $inbox->get('/cards', [$cardController, 'inboxIndex']); $inbox->post('/cards', [$cardController, 'inboxStore']); })->add($authMiddleware); // Single-card and ordering routes are global: a card may not have a // project to nest them under. $group->group('/cards', function (RouteCollectorProxy $cards) use ($cardController) { $cards->put('/order', [$cardController, 'reorder']); $cards->get('/{cardId:[0-9]+}', [$cardController, 'show']); $cards->patch('/{cardId:[0-9]+}', [$cardController, 'update']); $cards->delete('/{cardId:[0-9]+}', [$cardController, 'destroy']); })->add($authMiddleware); }); return $app;