import 'dart:convert'; import 'package:shelf/shelf.dart'; import '../application/application.dart'; import '../domain/domain.dart'; const authenticatedRequestContextKey = 'gametime.authenticatedRequest'; const bearerTokenContextKey = 'gametime.bearerToken'; typedef AuthenticateToken = Future Function({required String token}); final class AuthApi { const AuthApi({ required this.registerUser, required this.login, required this.logout, required this.authenticateRequest, }); final RegisterUserUseCase registerUser; final LoginUseCase login; final LogoutUseCase logout; final AuthenticateRequestUseCase authenticateRequest; Future register(Request request) async { try { final body = await _readJsonObject(request); final user = await registerUser.execute( email: _requiredString(body, 'email'), password: _requiredString(body, 'password'), displayName: _optionalString(body, 'displayName'), ); return _jsonResponse(201, {'userId': user.id, 'email': user.email}); } on EmailAlreadyTakenException catch (error) { return _errorResponse(409, error.message); } on ValidationException catch (error) { return _errorResponse(400, error.message); } on FormatException catch (error) { return _errorResponse(400, error.message); } } Future loginUser(Request request) async { try { final body = await _readJsonObject(request); final result = await login.execute( email: _requiredString(body, 'email'), password: _requiredString(body, 'password'), userAgent: request.headers['user-agent'], deviceLabel: _optionalString(body, 'deviceLabel'), ); return _jsonResponse(200, { 'token': result.token, 'expiresAt': result.expiresAt.toIso8601String(), }); } on InvalidCredentialsException catch (error) { return _errorResponse(401, error.message); } on ValidationException catch (error) { return _errorResponse(400, error.message); } on FormatException catch (error) { return _errorResponse(400, error.message); } } Future logoutUser(Request request) async { final token = request.context[bearerTokenContextKey] as String?; if (token == null) { return _errorResponse(401, 'Unauthorized.'); } try { await logout.execute(token: token); return Response(204); } on UnauthorizedException catch (error) { return _errorResponse(401, error.message); } } } Middleware authenticationMiddleware(AuthenticateToken authenticate) { return (innerHandler) { return (request) async { final token = _bearerToken(request.headers['authorization']); if (token == null) { return _errorResponse(401, 'Missing bearer token.'); } try { final authenticated = await authenticate(token: token); return innerHandler( request.change( context: { authenticatedRequestContextKey: authenticated, bearerTokenContextKey: token, }, ), ); } on UnauthorizedException catch (error) { return _errorResponse(401, error.message); } }; }; } AuthenticatedRequest? authenticatedRequestFrom(Request request) { return request.context[authenticatedRequestContextKey] as AuthenticatedRequest?; } String? _bearerToken(String? authorization) { if (authorization == null) { return null; } final parts = authorization.trim().split(RegExp(r'\s+')); if (parts.length != 2 || parts.first.toLowerCase() != 'bearer') { return null; } return parts[1].isEmpty ? null : parts[1]; } Future> _readJsonObject(Request request) async { final raw = await request.readAsString(); final decoded = jsonDecode(raw); if (decoded is! Map) { throw const FormatException('Request body must be a JSON object.'); } return decoded; } String _requiredString(Map body, String key) { final value = body[key]; if (value is! String || value.trim().isEmpty) { throw FormatException('$key must be a non-empty string.'); } return value; } String? _optionalString(Map body, String key) { final value = body[key]; if (value == null) { return null; } if (value is! String) { throw FormatException('$key must be a string.'); } final trimmed = value.trim(); return trimmed.isEmpty ? null : trimmed; } Response _jsonResponse(int statusCode, Map body) { return Response( statusCode, body: jsonEncode(body), headers: {'content-type': 'application/json'}, ); } Response _errorResponse(int statusCode, String message) { return _jsonResponse(statusCode, {'error': message}); }