cyberhybridhub/server/lib/handlers/questions_handler.dart
2026-06-03 04:21:42 -05:00

246 lines
8.4 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_service.dart';
import '../questions_db.dart';
const String questionsBasePath = '/v1/me/questions';
Handler questionsHandler({
required FirebaseAuthVerifier auth,
required QuestionsDb questionsDb,
required QuestionService questionService,
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 {
final Map<String, dynamic>? question =
await questionService.bootstrapOnLogin(firebaseUid);
final int unansweredCount =
await questionsDb.countUnansweredQuestions(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>> rows =
await questionsDb.listUnansweredQuestions(firebaseUid);
final List<Map<String, dynamic>> questions = rows
.map(
(Map<String, dynamic> row) => questionsDb.toClientPayload(
row,
unansweredCount: rows.length,
),
)
.toList();
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);
return _jsonResponse(200, <String, dynamic>{
'score': score,
});
} 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 num answer = (json['answer'] as num?) ?? 0;
final Map<String, dynamic>? updated = await questionsDb.submitAnswer(
questionId: id,
assignedUserId: firebaseUid,
userResponse: answer,
);
if (updated == null) {
return _jsonResponse(404, <String, dynamic>{'error': 'Not found'});
}
if (questionPipeline != null) {
await questionPipeline.onAnswerSubmitted(
firebaseUid: firebaseUid,
answeredQuestion: updated,
userResponse: answer,
);
}
var unansweredCount =
await questionsDb.countUnansweredQuestions(firebaseUid);
Map<String, dynamic>? nextQuestion;
if (unansweredCount == 0) {
nextQuestion =
await questionService.ensureProspectiveQuestionQueued(firebaseUid);
if (nextQuestion != null) {
unansweredCount =
(nextQuestion['unansweredCount'] as num?)?.toInt() ??
await questionsDb.countUnansweredQuestions(firebaseUid);
}
} else {
final Map<String, dynamic>? nextRow =
await questionsDb.findUnansweredQuestion(firebaseUid);
if (nextRow != null) {
nextQuestion = questionsDb.toClientPayload(
nextRow,
unansweredCount: unansweredCount,
);
}
}
final Map<String, dynamic> score =
await questionsDb.getGuessScoreSummary(firebaseUid);
return _jsonResponse(200, <String, dynamic>{
'question': updated,
'unansweredCount': unansweredCount,
'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/<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 questionsDb.countUnansweredQuestions(firebaseUid);
final Map<String, dynamic>? nextRow =
await questionsDb.findNextUnansweredAfterDefer(
assignedUserId: firebaseUid,
deferredQuestionId: id,
);
final Map<String, dynamic>? nextQuestion = nextRow == null
? null
: questionsDb.toClientPayload(
nextRow,
unansweredCount: unansweredCount,
);
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',
},
);
}