Skip to content

Commit caf7135

Browse files
committed
feat(settings): implement user settings API endpoints
- Add middleware for settings repository using in-memory client - Create API endpoints for user settings: - GET/PUT /api/v1/users/me/settings/display - DELETE /api/v1/users/me/settings - GET/PUT /api/v1/users/me/settings/language - Implement error handling and request logging - Use HT API response formatting for consistency
1 parent 446278e commit caf7135

File tree

4 files changed

+306
-0
lines changed

4 files changed

+306
-0
lines changed

routes/_middleware.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import 'dart:io';
88
import 'package:dart_frog/dart_frog.dart';
99
import 'package:ht_api/src/middlewares/error_handler.dart';
1010
import 'package:ht_api/src/registry/model_registry.dart';
11+
import 'package:ht_app_settings_inmemory/ht_app_settings_inmemory.dart';
12+
import 'package:ht_app_settings_repository/ht_app_settings_repository.dart';
1113
import 'package:ht_data_inmemory/ht_data_inmemory.dart';
1214
import 'package:ht_data_repository/ht_data_repository.dart';
1315
import 'package:ht_shared/ht_shared.dart';
@@ -157,6 +159,9 @@ Handler middleware(Handler handler) {
157159
final categoryRepository = _createCategoryRepository();
158160
final sourceRepository = _createSourceRepository();
159161
final countryRepository = _createCountryRepository();
162+
// Instantiate settings client and repository
163+
final settingsClient = HtAppSettingsInMemory(); // Using in-memory for now
164+
final settingsRepository = HtAppSettingsRepository(client: settingsClient);
160165

161166
// Create a UUID generator instance
162167
const uuid = Uuid();
@@ -184,6 +189,8 @@ Handler middleware(Handler handler) {
184189
.use(provider<HtDataRepository<Category>>((_) => categoryRepository))
185190
.use(provider<HtDataRepository<Source>>((_) => sourceRepository))
186191
.use(provider<HtDataRepository<Country>>((_) => countryRepository))
192+
// Provide the settings repository
193+
.use(provider<HtAppSettingsRepository>((_) => settingsRepository))
187194

188195
// Add other essential middleware like error handling
189196
.use(requestLogger()) // Basic request logging
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//
2+
// ignore_for_file: lines_longer_than_80_chars, avoid_catches_without_on_clauses
3+
4+
import 'dart:io';
5+
6+
import 'package:dart_frog/dart_frog.dart';
7+
import 'package:ht_app_settings_client/ht_app_settings_client.dart';
8+
import 'package:ht_app_settings_repository/ht_app_settings_repository.dart';
9+
import 'package:ht_shared/ht_shared.dart';
10+
11+
// Import RequestId from the root middleware file
12+
import '../../../../../_middleware.dart';
13+
14+
/// Handles requests for the /api/v1/users/me/settings/display endpoint.
15+
Future<Response> onRequest(RequestContext context) async {
16+
// Read dependencies provided by middleware
17+
final settingsRepo = context.read<HtAppSettingsRepository>();
18+
final requestId = context.read<RequestId>().id;
19+
20+
try {
21+
switch (context.request.method) {
22+
case HttpMethod.get:
23+
return await _handleGet(context, settingsRepo, requestId);
24+
case HttpMethod.put:
25+
return await _handlePut(context, settingsRepo, requestId);
26+
default:
27+
return Response(statusCode: HttpStatus.methodNotAllowed);
28+
}
29+
} on HtHttpException catch (_) {
30+
// Let the errorHandler middleware handle HtHttpExceptions
31+
rethrow;
32+
} on FormatException catch (_) {
33+
// Let the errorHandler middleware handle FormatExceptions (from PUT body)
34+
rethrow;
35+
} catch (e, stackTrace) {
36+
// Handle any other unexpected errors locally
37+
print(
38+
'[ReqID: $requestId] Unexpected error in /settings/display.dart handler: $e\n$stackTrace',
39+
);
40+
return Response(
41+
statusCode: HttpStatus.internalServerError,
42+
body: 'Internal Server Error.',
43+
);
44+
}
45+
}
46+
47+
// --- GET Handler ---
48+
Future<Response> _handleGet(
49+
RequestContext context,
50+
HtAppSettingsRepository settingsRepo,
51+
String requestId,
52+
) async {
53+
// Exceptions from repository (e.g., client failure) will propagate up.
54+
final displaySettings = await settingsRepo.getDisplaySettings();
55+
56+
final metadata = ResponseMetadata(
57+
requestId: requestId,
58+
timestamp: DateTime.now().toUtc(),
59+
);
60+
61+
final successResponse = SuccessApiResponse<DisplaySettings>(
62+
data: displaySettings,
63+
metadata: metadata,
64+
);
65+
66+
// Use the generated toJson method for DisplaySettings
67+
final responseJson = successResponse.toJson((settings) => settings.toJson());
68+
69+
return Response.json(body: responseJson);
70+
}
71+
72+
// --- PUT Handler ---
73+
Future<Response> _handlePut(
74+
RequestContext context,
75+
HtAppSettingsRepository settingsRepo,
76+
String requestId,
77+
) async {
78+
final requestBody = await context.request.json() as Map<String, dynamic>?;
79+
if (requestBody == null) {
80+
return Response(
81+
statusCode: HttpStatus.badRequest,
82+
body: 'Missing or invalid request body.',
83+
);
84+
}
85+
86+
// Deserialize request body into DisplaySettings.
87+
// FormatException or TypeError during parsing will propagate up.
88+
final newSettings = DisplaySettings.fromJson(requestBody);
89+
90+
// Save the settings. Repository exceptions will propagate up.
91+
await settingsRepo.setDisplaySettings(newSettings);
92+
93+
// Optionally, return the updated settings.
94+
// Fetching again ensures we return the exact state after saving.
95+
final updatedSettings = await settingsRepo.getDisplaySettings();
96+
97+
final metadata = ResponseMetadata(
98+
requestId: requestId,
99+
timestamp: DateTime.now().toUtc(),
100+
);
101+
102+
final successResponse = SuccessApiResponse<DisplaySettings>(
103+
data: updatedSettings,
104+
metadata: metadata,
105+
);
106+
107+
final responseJson = successResponse.toJson((settings) => settings.toJson());
108+
109+
// Return 200 OK with the updated settings
110+
return Response.json(body: responseJson);
111+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
// ignore_for_file: lines_longer_than_80_chars, avoid_catches_without_on_clauses
3+
4+
import 'dart:io';
5+
6+
import 'package:dart_frog/dart_frog.dart';
7+
import 'package:ht_app_settings_repository/ht_app_settings_repository.dart';
8+
import 'package:ht_shared/ht_shared.dart';
9+
10+
// Import RequestId from the root middleware file
11+
import '../../../../../_middleware.dart';
12+
13+
/// Handles requests for the base /api/v1/users/me/settings endpoint.
14+
/// Currently only supports DELETE to clear all settings.
15+
Future<Response> onRequest(RequestContext context) async {
16+
// Read dependencies provided by middleware
17+
final settingsRepo = context.read<HtAppSettingsRepository>();
18+
final requestId = context.read<RequestId>().id;
19+
20+
try {
21+
// This endpoint currently only supports DELETE
22+
if (context.request.method != HttpMethod.delete) {
23+
return Response(statusCode: HttpStatus.methodNotAllowed);
24+
}
25+
26+
// Handle DELETE request
27+
return await _handleDelete(context, settingsRepo, requestId);
28+
} on HtHttpException catch (_) {
29+
// Let the errorHandler middleware handle HtHttpExceptions
30+
rethrow;
31+
} catch (e, stackTrace) {
32+
// Handle any other unexpected errors locally
33+
print(
34+
'[ReqID: $requestId] Unexpected error in /settings/index.dart handler: $e\n$stackTrace',
35+
);
36+
return Response(
37+
statusCode: HttpStatus.internalServerError,
38+
body: 'Internal Server Error.',
39+
);
40+
}
41+
}
42+
43+
// --- DELETE Handler ---
44+
Future<Response> _handleDelete(
45+
RequestContext context,
46+
HtAppSettingsRepository settingsRepo,
47+
String requestId,
48+
) async {
49+
// Call the repository method to clear settings.
50+
// Exceptions from the repository/client will propagate up.
51+
await settingsRepo.clearSettings();
52+
53+
// Return 204 No Content on successful deletion
54+
return Response(statusCode: HttpStatus.noContent);
55+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
//
2+
// ignore_for_file: lines_longer_than_80_chars, avoid_catches_without_on_clauses
3+
4+
import 'dart:io';
5+
6+
import 'package:dart_frog/dart_frog.dart';
7+
import 'package:ht_app_settings_repository/ht_app_settings_repository.dart';
8+
import 'package:ht_shared/ht_shared.dart';
9+
10+
// Import RequestId from the root middleware file
11+
import '../../../../../_middleware.dart';
12+
13+
/// Handles requests for the /api/v1/users/me/settings/language endpoint.
14+
Future<Response> onRequest(RequestContext context) async {
15+
// Read dependencies provided by middleware
16+
final settingsRepo = context.read<HtAppSettingsRepository>();
17+
final requestId = context.read<RequestId>().id;
18+
19+
try {
20+
switch (context.request.method) {
21+
case HttpMethod.get:
22+
return await _handleGet(context, settingsRepo, requestId);
23+
case HttpMethod.put:
24+
return await _handlePut(context, settingsRepo, requestId);
25+
default:
26+
return Response(statusCode: HttpStatus.methodNotAllowed);
27+
}
28+
} on HtHttpException catch (_) {
29+
// Let the errorHandler middleware handle HtHttpExceptions
30+
rethrow;
31+
} on FormatException catch (_) {
32+
// Let the errorHandler middleware handle FormatExceptions (from PUT body)
33+
rethrow;
34+
} catch (e, stackTrace) {
35+
// Handle any other unexpected errors locally
36+
print(
37+
'[ReqID: $requestId] Unexpected error in /settings/language.dart handler: $e\n$stackTrace',
38+
);
39+
return Response(
40+
statusCode: HttpStatus.internalServerError,
41+
body: 'Internal Server Error.',
42+
);
43+
}
44+
}
45+
46+
// --- GET Handler ---
47+
Future<Response> _handleGet(
48+
RequestContext context,
49+
HtAppSettingsRepository settingsRepo,
50+
String requestId,
51+
) async {
52+
// Exceptions from repository will propagate up.
53+
final language = await settingsRepo.getLanguage();
54+
55+
final metadata = ResponseMetadata(
56+
requestId: requestId,
57+
timestamp: DateTime.now().toUtc(),
58+
);
59+
60+
// Wrap the language string in a simple map for consistency
61+
final responseData = {'language': language};
62+
63+
final successResponse = SuccessApiResponse<Map<String, String>>(
64+
data: responseData,
65+
metadata: metadata,
66+
);
67+
68+
// Need toJsonT for Map<String, String> (identity function works)
69+
final responseJson = successResponse.toJson((map) => map);
70+
71+
return Response.json(body: responseJson);
72+
}
73+
74+
// --- PUT Handler ---
75+
Future<Response> _handlePut(
76+
RequestContext context,
77+
HtAppSettingsRepository settingsRepo,
78+
String requestId,
79+
) async {
80+
final requestBody = await context.request.json() as Map<String, dynamic>?;
81+
if (requestBody == null ||
82+
!requestBody.containsKey('language') ||
83+
requestBody['language'] is! String) {
84+
return Response.json(
85+
statusCode: HttpStatus.badRequest,
86+
body: {
87+
'error': {
88+
'code': 'INVALID_REQUEST_BODY',
89+
'message':
90+
"Missing or invalid 'language' field in request body. Expected a string.",
91+
},
92+
},
93+
);
94+
}
95+
96+
final newLanguage = requestBody['language'] as String;
97+
98+
// Basic validation (e.g., length check, could add regex for ISO codes)
99+
if (newLanguage.isEmpty || newLanguage.length > 10) {
100+
return Response.json(
101+
statusCode: HttpStatus.badRequest,
102+
body: {
103+
'error': {
104+
'code': 'INVALID_LANGUAGE_CODE',
105+
'message': 'Invalid language code format provided.',
106+
},
107+
},
108+
);
109+
}
110+
111+
// Save the language. Repository exceptions will propagate up.
112+
await settingsRepo.setLanguage(newLanguage);
113+
114+
// Optionally, return the updated language.
115+
final updatedLanguage = await settingsRepo.getLanguage();
116+
117+
final metadata = ResponseMetadata(
118+
requestId: requestId,
119+
timestamp: DateTime.now().toUtc(),
120+
);
121+
122+
final responseData = {'language': updatedLanguage};
123+
124+
final successResponse = SuccessApiResponse<Map<String, String>>(
125+
data: responseData,
126+
metadata: metadata,
127+
);
128+
129+
final responseJson = successResponse.toJson((map) => map);
130+
131+
// Return 200 OK with the updated language
132+
return Response.json(body: responseJson);
133+
}

0 commit comments

Comments
 (0)