160 lines
5.5 KiB
Dart
160 lines
5.5 KiB
Dart
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, <String, dynamic>{'error': 'Unauthorized'});
|
|
}
|
|
try {
|
|
final List<String> words = await spellingItemsDb.listCanonicalWords();
|
|
return _jsonResponse(200, <String, dynamic>{'words': words});
|
|
} catch (e, st) {
|
|
stderr.writeln('List spelling words error: $e\n$st');
|
|
return _jsonResponse(500, <String, dynamic>{'error': 'Internal error'});
|
|
}
|
|
});
|
|
|
|
router.post('$spellingWordsBasePath/import', (Request request) async {
|
|
final String? firebaseUid = await _verify(auth, request);
|
|
if (firebaseUid == null) {
|
|
return _jsonResponse(401, <String, dynamic>{'error': 'Unauthorized'});
|
|
}
|
|
try {
|
|
final String body = await request.readAsString();
|
|
final List<String> lines = body.split(RegExp(r'\r?\n'));
|
|
final ({int added, int skipped}) result =
|
|
await spellingItemsDb.importCanonicalWords(lines);
|
|
return _jsonResponse(200, <String, dynamic>{
|
|
'added': result.added,
|
|
'skipped': result.skipped,
|
|
});
|
|
} catch (e, st) {
|
|
stderr.writeln('Import spelling words error: $e\n$st');
|
|
return _jsonResponse(500, <String, dynamic>{'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<String> words = await spellingItemsDb.listCanonicalWords();
|
|
return Response.ok(
|
|
words.join('\n'),
|
|
headers: <String, String>{
|
|
..._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, <String, dynamic>{'error': 'Unauthorized'});
|
|
}
|
|
try {
|
|
final String body = await request.readAsString();
|
|
final Map<String, dynamic> json = body.isEmpty
|
|
? <String, dynamic>{}
|
|
: jsonDecode(body) as Map<String, dynamic>;
|
|
final String? word = json['word'] as String?;
|
|
if (word == null || word.trim().isEmpty) {
|
|
return _jsonResponse(400, <String, dynamic>{'error': 'word is required'});
|
|
}
|
|
final bool added = await spellingItemsDb.addCanonicalWord(word);
|
|
if (!added) {
|
|
return _jsonResponse(409, <String, dynamic>{'error': 'Word already exists'});
|
|
}
|
|
final List<String> words = await spellingItemsDb.listCanonicalWords();
|
|
return _jsonResponse(201, <String, dynamic>{
|
|
'word': word.trim().toLowerCase(),
|
|
'words': words,
|
|
});
|
|
} catch (e, st) {
|
|
stderr.writeln('Add spelling word error: $e\n$st');
|
|
return _jsonResponse(500, <String, dynamic>{'error': 'Internal error'});
|
|
}
|
|
});
|
|
|
|
router.delete('$spellingWordsBasePath/<word>', (Request request, String word) async {
|
|
final String? firebaseUid = await _verify(auth, request);
|
|
if (firebaseUid == null) {
|
|
return _jsonResponse(401, <String, dynamic>{'error': 'Unauthorized'});
|
|
}
|
|
try {
|
|
final String decodedWord = Uri.decodeComponent(word);
|
|
final bool deleted = await spellingItemsDb.deleteCanonicalWord(decodedWord);
|
|
if (!deleted) {
|
|
return _jsonResponse(404, <String, dynamic>{'error': 'Word not found'});
|
|
}
|
|
final List<String> words = await spellingItemsDb.listCanonicalWords();
|
|
return _jsonResponse(200, <String, dynamic>{
|
|
'word': decodedWord.trim().toLowerCase(),
|
|
'words': words,
|
|
});
|
|
} catch (e, st) {
|
|
stderr.writeln('Delete spelling word error: $e\n$st');
|
|
return _jsonResponse(500, <String, dynamic>{'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, <String, dynamic>{'error': 'Not found'});
|
|
};
|
|
}
|
|
|
|
Future<String?> _verify(FirebaseAuthVerifier auth, Request request) {
|
|
return auth.verifyBearerToken(
|
|
request.headers['Authorization'] ?? request.headers['authorization'],
|
|
);
|
|
}
|
|
|
|
Map<String, String> _jsonCorsHeaders() =>
|
|
apiCorsHeaders(methods: 'GET, POST, DELETE, OPTIONS');
|
|
|
|
Map<String, String> _textCorsHeaders() =>
|
|
apiCorsHeaders(methods: 'GET, OPTIONS');
|
|
|
|
Response _jsonResponse(int status, Map<String, dynamic> body) {
|
|
return Response(
|
|
status,
|
|
body: jsonEncode(body),
|
|
headers: <String, String>{
|
|
..._jsonCorsHeaders(),
|
|
'Content-Type': 'application/json',
|
|
},
|
|
);
|
|
}
|