Dart Shelf API with modules: auth (JWT + PBKDF2), tasks (CRUD + dopamine scorer), streaks (forgiveness + freeze), rewards (variable reward engine), time perception, sync (offline-first push/pull), rooms (body doubling placeholder). Includes DB migration (001_initial_schema.sql) and Docker Compose. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
99 lines
3.2 KiB
Dart
99 lines
3.2 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:shelf/shelf.dart';
|
|
import 'package:shelf_router/shelf_router.dart';
|
|
|
|
import '../../middleware/error_handler.dart';
|
|
import '../../shared/api_response.dart';
|
|
import 'streak_service.dart';
|
|
|
|
/// Streak module route definitions.
|
|
class StreakRoutes {
|
|
final StreakService _service;
|
|
|
|
StreakRoutes(this._service);
|
|
|
|
Router get router {
|
|
final router = Router();
|
|
|
|
router.get('/', _listStreaks);
|
|
router.post('/', _createStreak);
|
|
router.get('/<id>', _getStreak);
|
|
router.post('/<id>/complete', _completeToday);
|
|
router.post('/<id>/forgive', _forgive);
|
|
router.post('/<id>/freeze', _freeze);
|
|
router.get('/<id>/history', _getHistory);
|
|
|
|
return router;
|
|
}
|
|
|
|
// ── Handlers ────────────────────────────────────────────────────────
|
|
|
|
Future<Response> _listStreaks(Request request) async {
|
|
final userId = request.context['userId'] as String;
|
|
final streaks = await _service.listStreaks(userId);
|
|
return ApiResponse.success(streaks);
|
|
}
|
|
|
|
Future<Response> _createStreak(Request request) async {
|
|
final userId = request.context['userId'] as String;
|
|
final body =
|
|
jsonDecode(await request.readAsString()) as Map<String, dynamic>;
|
|
|
|
if (body['name'] == null || (body['name'] as String).isEmpty) {
|
|
throw ApiException.badRequest('Name is required');
|
|
}
|
|
|
|
final streak = await _service.createStreak(userId, body);
|
|
return ApiResponse.created(streak);
|
|
}
|
|
|
|
Future<Response> _getStreak(Request request, String id) async {
|
|
final userId = request.context['userId'] as String;
|
|
final streak = await _service.getStreak(id, userId);
|
|
return ApiResponse.success(streak);
|
|
}
|
|
|
|
Future<Response> _completeToday(Request request, String id) async {
|
|
final userId = request.context['userId'] as String;
|
|
final result = await _service.completeToday(id, userId);
|
|
return ApiResponse.success(result, message: result['message'] as String);
|
|
}
|
|
|
|
Future<Response> _forgive(Request request, String id) async {
|
|
final userId = request.context['userId'] as String;
|
|
|
|
String? note;
|
|
try {
|
|
final body =
|
|
jsonDecode(await request.readAsString()) as Map<String, dynamic>;
|
|
note = body['note'] as String?;
|
|
} catch (_) {
|
|
// body optional
|
|
}
|
|
|
|
final result = await _service.forgive(id, userId, note: note);
|
|
return ApiResponse.success(result, message: result['message'] as String);
|
|
}
|
|
|
|
Future<Response> _freeze(Request request, String id) async {
|
|
final userId = request.context['userId'] as String;
|
|
final body =
|
|
jsonDecode(await request.readAsString()) as Map<String, dynamic>;
|
|
|
|
final days = body['days'] as int?;
|
|
if (days == null) {
|
|
throw ApiException.badRequest('Field "days" is required');
|
|
}
|
|
|
|
final result = await _service.freeze(id, userId, days: days);
|
|
return ApiResponse.success(result, message: result['message'] as String);
|
|
}
|
|
|
|
Future<Response> _getHistory(Request request, String id) async {
|
|
final userId = request.context['userId'] as String;
|
|
final history = await _service.getHistory(id, userId);
|
|
return ApiResponse.success(history);
|
|
}
|
|
}
|