import 'dart:convert'; import 'package:shelf/shelf.dart'; import 'package:shelf_router/shelf_router.dart'; import '../database/database.dart'; class AuthHandler { final AppDatabase db; AuthHandler(this.db); Router get router { final router = Router(); router.post('/login', _login); router.post('/logout', _logout); router.get('/me', _getCurrentUser); return router; } /// POST /api/auth/login Future _login(Request request) async { try { print('[Auth] Login attempt received'); final bodyStr = await request.readAsString(); final payload = jsonDecode(bodyStr) as Map; final username = payload['username'] as String?; final password = payload['password'] as String?; if (username == null || password == null) { print('[Auth] Missing username or password'); return Response(400, body: jsonEncode({ 'success': false, 'error': 'Username and password are required', }), headers: {'Content-Type': 'application/json'},); } print('[Auth] Verifying credentials for user: $username'); // Verify credentials final user = db.verifyCredentials(username, password); if (user == null) { print('[Auth] Invalid credentials for user: $username'); return Response(401, body: jsonEncode({ 'success': false, 'error': 'Invalid credentials', }), headers: {'Content-Type': 'application/json'},); } print('[Auth] User verified, creating session for userId: ${user.id}'); // Create session final session = db.createSession(user.id); // HttpOnly session cookie so same-origin media requests (hls.js inside // the player iframe cannot send Authorization headers) are authenticated. final maxAge = session.expiresAt.difference(DateTime.now()).inSeconds; return Response.ok(jsonEncode({ 'success': true, 'user': user.toJson(), 'token': session.token, }), headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'session=${session.token}; HttpOnly; SameSite=Lax; Path=/; Max-Age=$maxAge', },); } catch (e, stackTrace) { print('[Auth] ERROR during login: $e'); print('[Auth] Stack trace: $stackTrace'); return Response.internalServerError( body: jsonEncode({'success': false, 'error': e.toString()}), headers: {'Content-Type': 'application/json'}, ); } } /// POST /api/auth/logout Future _logout(Request request) async { try { final token = _extractToken(request); if (token == null) { return Response(401, body: jsonEncode({ 'success': false, 'error': 'Unauthorized', }), headers: {'Content-Type': 'application/json'},); } db.deleteSession(token); return Response.ok(jsonEncode({ 'success': true, }), headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'session=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0', },); } catch (e) { return Response.internalServerError( body: jsonEncode({'success': false, 'error': e.toString()}), headers: {'Content-Type': 'application/json'}, ); } } /// GET /api/auth/me Future _getCurrentUser(Request request) async { try { final token = _extractToken(request); if (token == null) { return Response(401, body: jsonEncode({ 'success': false, 'error': 'Unauthorized', }), headers: {'Content-Type': 'application/json'},); } final session = db.findSessionByToken(token); if (session == null) { return Response(401, body: jsonEncode({ 'success': false, 'error': 'Invalid or expired session', }), headers: {'Content-Type': 'application/json'},); } final user = db.findUserById(session.userId); if (user == null) { return Response(404, body: jsonEncode({ 'success': false, 'error': 'User not found', }), headers: {'Content-Type': 'application/json'},); } return Response.ok(jsonEncode({ 'user': user.toJson(), }), headers: {'Content-Type': 'application/json'},); } catch (e) { return Response.internalServerError( body: jsonEncode({'success': false, 'error': e.toString()}), headers: {'Content-Type': 'application/json'}, ); } } /// Extract token from Authorization header or cookie String? _extractToken(Request request) { // Try Authorization header first final authHeader = request.headers['authorization']; if (authHeader != null && authHeader.startsWith('Bearer ')) { return authHeader.substring(7); } // Try cookie final cookie = request.headers['cookie']; if (cookie != null) { final parts = cookie.split(';'); for (final part in parts) { final trimmed = part.trim(); if (trimmed.startsWith('session=')) { return trimmed.substring(8); } } } return null; } }