Some checks failed
Admin portal tests / test (push) Has been cancelled
- Replaced single Postgres Connection with Pool across the server to prevent runTx locking - Added _filterForEnabledSources to client's _fetchUnansweredForPanel to strictly enforce local configuration Co-authored-by: Cursor <cursoragent@cursor.com>
282 lines
10 KiB
Dart
282 lines
10 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 '../pipeline/question_pipeline.dart';
|
|
import '../question_dispatcher.dart';
|
|
import '../question_service.dart';
|
|
import '../questions_db.dart';
|
|
|
|
const String questionsBasePath = '/v1/me/questions';
|
|
|
|
Handler questionsHandler({
|
|
required FirebaseAuthVerifier auth,
|
|
required QuestionsDb questionsDb,
|
|
required QuestionService questionService,
|
|
required QuestionDispatcher questionDispatcher,
|
|
QuestionPipeline? questionPipeline,
|
|
}) {
|
|
final Router router = Router();
|
|
|
|
router.post('$questionsBasePath/bootstrap', (Request request) async {
|
|
final String? firebaseUid = await _verify(auth, request);
|
|
if (firebaseUid == null) {
|
|
return _jsonResponse(401, <String, dynamic>{'error': 'Unauthorized'});
|
|
}
|
|
try {
|
|
await questionService.bootstrapOnLogin(firebaseUid);
|
|
final Map<String, dynamic>? question =
|
|
await questionDispatcher.dispatchNextQuestion(firebaseUid);
|
|
final int unansweredCount =
|
|
await questionDispatcher.countEnabledUnansweredQuestions(firebaseUid);
|
|
final Map<String, dynamic> score =
|
|
await questionsDb.getGuessScoreSummary(firebaseUid);
|
|
return _jsonResponse(200, <String, dynamic>{
|
|
'question': question,
|
|
'unansweredCount': unansweredCount,
|
|
'score': score,
|
|
});
|
|
} catch (e, st) {
|
|
stderr.writeln('Bootstrap questions error: $e\n$st');
|
|
return _jsonResponse(500, <String, dynamic>{'error': 'Internal error'});
|
|
}
|
|
});
|
|
|
|
router.get(questionsBasePath, (Request request) async {
|
|
final String? firebaseUid = await _verify(auth, request);
|
|
if (firebaseUid == null) {
|
|
return _jsonResponse(401, <String, dynamic>{'error': 'Unauthorized'});
|
|
}
|
|
try {
|
|
final List<Map<String, dynamic>> questions =
|
|
await questionDispatcher.listPanelQuestions(firebaseUid);
|
|
return _jsonResponse(200, <String, dynamic>{
|
|
'questions': questions,
|
|
'unansweredCount': questions.length,
|
|
});
|
|
} catch (e, st) {
|
|
stderr.writeln('List questions error: $e\n$st');
|
|
return _jsonResponse(500, <String, dynamic>{'error': 'Internal error'});
|
|
}
|
|
});
|
|
|
|
router.get('$questionsBasePath/score', (Request request) async {
|
|
final String? firebaseUid = await _verify(auth, request);
|
|
if (firebaseUid == null) {
|
|
return _jsonResponse(401, <String, dynamic>{'error': 'Unauthorized'});
|
|
}
|
|
try {
|
|
final Map<String, dynamic> score =
|
|
await questionsDb.getGuessScoreSummary(firebaseUid);
|
|
return _jsonResponse(200, <String, dynamic>{
|
|
'score': score,
|
|
});
|
|
} catch (e, st) {
|
|
stderr.writeln('Get questions score error: $e\n$st');
|
|
return _jsonResponse(500, <String, dynamic>{'error': 'Internal error'});
|
|
}
|
|
});
|
|
|
|
router.post('$questionsBasePath/score/reset', (Request request) async {
|
|
final String? firebaseUid = await _verify(auth, request);
|
|
if (firebaseUid == null) {
|
|
return _jsonResponse(401, <String, dynamic>{'error': 'Unauthorized'});
|
|
}
|
|
try {
|
|
final Map<String, dynamic> score =
|
|
await questionsDb.resetGuessScoreSummary(firebaseUid);
|
|
final Map<String, dynamic>? question =
|
|
await questionDispatcher.dispatchNextQuestion(firebaseUid);
|
|
final int unansweredCount =
|
|
await questionDispatcher.countEnabledUnansweredQuestions(firebaseUid);
|
|
return _jsonResponse(200, <String, dynamic>{
|
|
'score': score,
|
|
'unansweredCount': unansweredCount,
|
|
if (question != null) 'question': question,
|
|
});
|
|
} catch (e, st) {
|
|
stderr.writeln('Reset questions score error: $e\n$st');
|
|
return _jsonResponse(500, <String, dynamic>{'error': 'Internal error'});
|
|
}
|
|
});
|
|
|
|
router.post(
|
|
'$questionsBasePath/<id>/answer',
|
|
(Request request, String id) 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? answerText = json['answerText'] as String?;
|
|
final num? answer = json['answer'] as num?;
|
|
|
|
final Map<String, dynamic>? updated = await questionsDb.submitAnswer(
|
|
questionId: id,
|
|
assignedUserId: firebaseUid,
|
|
userResponse: answer,
|
|
userResponseText: answerText,
|
|
);
|
|
if (updated == null) {
|
|
return _jsonResponse(404, <String, dynamic>{'error': 'Not found'});
|
|
}
|
|
if (questionPipeline != null) {
|
|
await questionPipeline.onAnswerSubmitted(
|
|
firebaseUid: firebaseUid,
|
|
answeredQuestion: updated,
|
|
userResponse: updated['userResponse'] as num? ?? answer ?? 0,
|
|
);
|
|
}
|
|
var unansweredCount =
|
|
await questionDispatcher.countEnabledUnansweredQuestions(firebaseUid);
|
|
final Map<String, dynamic>? nextQuestion =
|
|
await questionDispatcher.nextQuestionAfterAnswer(
|
|
firebaseUid: firebaseUid,
|
|
answeredQuestion: updated,
|
|
);
|
|
if (nextQuestion != null) {
|
|
unansweredCount =
|
|
(nextQuestion['unansweredCount'] as num?)?.toInt() ??
|
|
unansweredCount;
|
|
}
|
|
final bool isMarketAnswer =
|
|
QuestionsDb.resolveSourceId(updated) == 'market_history';
|
|
final Map<String, dynamic>? score = isMarketAnswer
|
|
? await questionsDb.getGuessScoreSummary(firebaseUid)
|
|
: null;
|
|
return _jsonResponse(200, <String, dynamic>{
|
|
'question': updated,
|
|
'unansweredCount': unansweredCount,
|
|
if (score != null) 'score': score,
|
|
if (nextQuestion != null) 'nextQuestion': nextQuestion,
|
|
});
|
|
} catch (e, st) {
|
|
stderr.writeln('Answer question error: $e\n$st');
|
|
return _jsonResponse(500, <String, dynamic>{'error': 'Internal error'});
|
|
}
|
|
},
|
|
);
|
|
|
|
router.post('$questionsBasePath/spelling/reset', (Request request) async {
|
|
final String? firebaseUid = await _verify(auth, request);
|
|
if (firebaseUid == null) {
|
|
return _jsonResponse(401, <String, dynamic>{'error': 'Unauthorized'});
|
|
}
|
|
if (questionPipeline == null) {
|
|
return _jsonResponse(503, <String, dynamic>{'error': 'Unavailable'});
|
|
}
|
|
try {
|
|
final Map<String, dynamic> result =
|
|
await questionPipeline.resetSpellingProgressForUser(firebaseUid);
|
|
return _jsonResponse(200, result);
|
|
} catch (e, st) {
|
|
stderr.writeln('Reset spelling progress error: $e\n$st');
|
|
return _jsonResponse(500, <String, dynamic>{'error': 'Internal error'});
|
|
}
|
|
});
|
|
|
|
router.post(
|
|
'$questionsBasePath/<id>/spelling/prepare',
|
|
(Request request, String id) async {
|
|
final String? firebaseUid = await _verify(auth, request);
|
|
if (firebaseUid == null) {
|
|
return _jsonResponse(401, <String, dynamic>{'error': 'Unauthorized'});
|
|
}
|
|
if (questionPipeline == null) {
|
|
return _jsonResponse(503, <String, dynamic>{'error': 'Unavailable'});
|
|
}
|
|
try {
|
|
final Map<String, dynamic>? question =
|
|
await questionPipeline.prepareSpellingQuestion(
|
|
firebaseUid: firebaseUid,
|
|
questionId: id,
|
|
);
|
|
if (question == null) {
|
|
return _jsonResponse(404, <String, dynamic>{'error': 'Not found'});
|
|
}
|
|
return _jsonResponse(200, <String, dynamic>{'question': question});
|
|
} catch (e, st) {
|
|
stderr.writeln('Prepare spelling question error: $e\n$st');
|
|
return _jsonResponse(500, <String, dynamic>{'error': 'Internal error'});
|
|
}
|
|
},
|
|
);
|
|
|
|
router.post(
|
|
'$questionsBasePath/<id>/defer',
|
|
(Request request, String id) async {
|
|
final String? firebaseUid = await _verify(auth, request);
|
|
if (firebaseUid == null) {
|
|
return _jsonResponse(401, <String, dynamic>{'error': 'Unauthorized'});
|
|
}
|
|
try {
|
|
final Map<String, dynamic>? updated = await questionsDb.deferQuestion(
|
|
questionId: id,
|
|
assignedUserId: firebaseUid,
|
|
);
|
|
if (updated == null) {
|
|
return _jsonResponse(404, <String, dynamic>{'error': 'Not found'});
|
|
}
|
|
final int unansweredCount =
|
|
await questionDispatcher.countEnabledUnansweredQuestions(firebaseUid);
|
|
final Map<String, dynamic>? nextQuestion =
|
|
await questionDispatcher.nextQuestionAfterDefer(
|
|
firebaseUid: firebaseUid,
|
|
deferredQuestionId: id,
|
|
);
|
|
return _jsonResponse(200, <String, dynamic>{
|
|
'question': updated,
|
|
'unansweredCount': unansweredCount,
|
|
if (nextQuestion != null) 'nextQuestion': nextQuestion,
|
|
});
|
|
} catch (e, st) {
|
|
stderr.writeln('Defer question error: $e\n$st');
|
|
return _jsonResponse(500, <String, dynamic>{'error': 'Internal error'});
|
|
}
|
|
},
|
|
);
|
|
|
|
return (Request request) async {
|
|
if (request.method == 'OPTIONS' &&
|
|
request.requestedUri.path.startsWith(questionsBasePath)) {
|
|
return Response.ok('', headers: apiCorsHeaders());
|
|
}
|
|
|
|
final String? firebaseUid = await _verify(auth, request);
|
|
if (firebaseUid == null) {
|
|
return _jsonResponse(401, <String, dynamic>{'error': 'Unauthorized'});
|
|
}
|
|
|
|
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'],
|
|
);
|
|
}
|
|
|
|
Response _jsonResponse(int status, Map<String, dynamic> body) {
|
|
return Response(
|
|
status,
|
|
body: jsonEncode(body),
|
|
headers: <String, String>{
|
|
...apiCorsHeaders(),
|
|
'Content-Type': 'application/json',
|
|
},
|
|
);
|
|
}
|