import 'dart:convert'; import 'dart:io'; import 'package:shelf/shelf.dart'; import 'package:shelf_router/shelf_router.dart'; import '../cors_headers.dart'; import '../firebase_auth.dart'; import '../spelling/spelling_items_db.dart'; const String spellingWordsBasePath = '/v1/me/spelling-words'; Handler spellingWordsHandler({ required FirebaseAuthVerifier auth, required SpellingItemsDb spellingItemsDb, }) { final Router router = Router(); router.get(spellingWordsBasePath, (Request request) async { final String? firebaseUid = await _verify(auth, request); if (firebaseUid == null) { return _jsonResponse(401, {'error': 'Unauthorized'}); } try { final List words = await spellingItemsDb.listCanonicalWords(); return _jsonResponse(200, {'words': words}); } catch (e, st) { stderr.writeln('List spelling words error: $e\n$st'); return _jsonResponse(500, {'error': 'Internal error'}); } }); router.post('$spellingWordsBasePath/import', (Request request) async { final String? firebaseUid = await _verify(auth, request); if (firebaseUid == null) { return _jsonResponse(401, {'error': 'Unauthorized'}); } try { final String body = await request.readAsString(); final List lines = body.split(RegExp(r'\r?\n')); final ({int added, int skipped}) result = await spellingItemsDb.importCanonicalWords(lines); return _jsonResponse(200, { 'added': result.added, 'skipped': result.skipped, }); } catch (e, st) { stderr.writeln('Import spelling words error: $e\n$st'); return _jsonResponse(500, {'error': 'Internal error'}); } }); router.get('$spellingWordsBasePath/export', (Request request) async { final String? firebaseUid = await _verify(auth, request); if (firebaseUid == null) { return Response(401, body: 'Unauthorized', headers: _textCorsHeaders()); } try { final List words = await spellingItemsDb.listCanonicalWords(); return Response.ok( words.join('\n'), headers: { ..._textCorsHeaders(), 'Content-Type': 'text/plain; charset=utf-8', }, ); } catch (e, st) { stderr.writeln('Export spelling words error: $e\n$st'); return Response(500, body: 'Internal error', headers: _textCorsHeaders()); } }); router.post(spellingWordsBasePath, (Request request) async { final String? firebaseUid = await _verify(auth, request); if (firebaseUid == null) { return _jsonResponse(401, {'error': 'Unauthorized'}); } try { final String body = await request.readAsString(); final Map json = body.isEmpty ? {} : jsonDecode(body) as Map; final String? word = json['word'] as String?; if (word == null || word.trim().isEmpty) { return _jsonResponse(400, {'error': 'word is required'}); } final bool added = await spellingItemsDb.addCanonicalWord(word); if (!added) { return _jsonResponse(409, {'error': 'Word already exists'}); } final List words = await spellingItemsDb.listCanonicalWords(); return _jsonResponse(201, { 'word': word.trim().toLowerCase(), 'words': words, }); } catch (e, st) { stderr.writeln('Add spelling word error: $e\n$st'); return _jsonResponse(500, {'error': 'Internal error'}); } }); router.delete('$spellingWordsBasePath/', (Request request, String word) async { final String? firebaseUid = await _verify(auth, request); if (firebaseUid == null) { return _jsonResponse(401, {'error': 'Unauthorized'}); } try { final String decodedWord = Uri.decodeComponent(word); final bool deleted = await spellingItemsDb.deleteCanonicalWord(decodedWord); if (!deleted) { return _jsonResponse(404, {'error': 'Word not found'}); } final List words = await spellingItemsDb.listCanonicalWords(); return _jsonResponse(200, { 'word': decodedWord.trim().toLowerCase(), 'words': words, }); } catch (e, st) { stderr.writeln('Delete spelling word error: $e\n$st'); return _jsonResponse(500, {'error': 'Internal error'}); } }); return (Request request) async { if (request.method == 'OPTIONS' && request.requestedUri.path.startsWith(spellingWordsBasePath)) { return Response.ok('', headers: _jsonCorsHeaders()); } final Response response = await router.call(request); if (response.statusCode != 404) { return response; } return _jsonResponse(404, {'error': 'Not found'}); }; } Future _verify(FirebaseAuthVerifier auth, Request request) { return auth.verifyBearerToken( request.headers['Authorization'] ?? request.headers['authorization'], ); } Map _jsonCorsHeaders() => apiCorsHeaders(methods: 'GET, POST, DELETE, OPTIONS'); Map _textCorsHeaders() => apiCorsHeaders(methods: 'GET, OPTIONS'); Response _jsonResponse(int status, Map body) { return Response( status, body: jsonEncode(body), headers: { ..._jsonCorsHeaders(), 'Content-Type': 'application/json', }, ); }