Skip to content

Commit 62f0f50

Browse files
authored
Merge pull request #40 from flutter-news-app-full-source-code/fix-data-route
Fix data route
2 parents 91217ba + 89db404 commit 62f0f50

File tree

6 files changed

+493
-325
lines changed

6 files changed

+493
-325
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import 'package:core/core.dart';
2+
import 'package:dart_frog/dart_frog.dart';
3+
import 'package:data_repository/data_repository.dart';
4+
import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/ownership_check_middleware.dart';
5+
import 'package:flutter_news_app_api_server_full_source_code/src/services/dashboard_summary_service.dart';
6+
import 'package:logging/logging.dart';
7+
8+
final _log = Logger('DataFetchMiddleware');
9+
10+
/// Middleware to fetch a data item by its ID and provide it to the context.
11+
///
12+
/// This middleware is responsible for:
13+
/// 1. Reading the `modelName` and item `id` from the context.
14+
/// 2. Calling the appropriate data repository to fetch the item.
15+
/// 3. If the item is found, providing it to the downstream context wrapped in a
16+
/// [FetchedItem] for type safety.
17+
/// 4. If the item is not found, throwing a [NotFoundException] to halt the
18+
/// request pipeline early.
19+
///
20+
/// This centralizes the item fetching logic for all item-specific routes,
21+
/// ensuring that subsequent middleware (like ownership checks) and the final
22+
/// route handler can safely assume the item exists in the context.
23+
Middleware dataFetchMiddleware() {
24+
return (handler) {
25+
return (context) async {
26+
final modelName = context.read<String>();
27+
final id = context.request.uri.pathSegments.last;
28+
29+
_log.info('Fetching item for model "$modelName", id "$id".');
30+
31+
final item = await _fetchItem(context, modelName, id);
32+
33+
if (item == null) {
34+
_log.warning(
35+
'Item not found for model "$modelName", id "$id".',
36+
);
37+
throw NotFoundException(
38+
'The requested item of type "$modelName" with id "$id" was not found.',
39+
);
40+
}
41+
42+
_log.finer('Item found. Providing to context.');
43+
final updatedContext = context.provide<FetchedItem<dynamic>>(
44+
() => FetchedItem(item),
45+
);
46+
47+
return handler(updatedContext);
48+
};
49+
};
50+
}
51+
52+
/// Helper function to fetch an item from the correct repository based on the
53+
/// model name.
54+
///
55+
/// This function contains the switch statement that maps a `modelName` string
56+
/// to a specific `DataRepository` call.
57+
///
58+
/// Throws [OperationFailedException] for unsupported model types.
59+
Future<dynamic> _fetchItem(
60+
RequestContext context,
61+
String modelName,
62+
String id,
63+
) async {
64+
// The `userId` is not needed here because this middleware's purpose is to
65+
// fetch the item regardless of ownership. Ownership is checked in a
66+
// subsequent middleware. We pass `null` for `userId` to ensure we are
67+
// performing a global lookup for the item.
68+
const String? userId = null;
69+
70+
try {
71+
switch (modelName) {
72+
case 'headline':
73+
return await context.read<DataRepository<Headline>>().read(
74+
id: id,
75+
userId: userId,
76+
);
77+
case 'topic':
78+
return await context
79+
.read<DataRepository<Topic>>()
80+
.read(id: id, userId: userId);
81+
case 'source':
82+
return await context.read<DataRepository<Source>>().read(
83+
id: id,
84+
userId: userId,
85+
);
86+
case 'country':
87+
return await context.read<DataRepository<Country>>().read(
88+
id: id,
89+
userId: userId,
90+
);
91+
case 'language':
92+
return await context.read<DataRepository<Language>>().read(
93+
id: id,
94+
userId: userId,
95+
);
96+
case 'user':
97+
return await context
98+
.read<DataRepository<User>>()
99+
.read(id: id, userId: userId);
100+
case 'user_app_settings':
101+
return await context.read<DataRepository<UserAppSettings>>().read(
102+
id: id,
103+
userId: userId,
104+
);
105+
case 'user_content_preferences':
106+
return await context
107+
.read<DataRepository<UserContentPreferences>>()
108+
.read(
109+
id: id,
110+
userId: userId,
111+
);
112+
case 'remote_config':
113+
return await context.read<DataRepository<RemoteConfig>>().read(
114+
id: id,
115+
userId: userId,
116+
);
117+
case 'dashboard_summary':
118+
// This is a special case that doesn't use a standard repository.
119+
return await context.read<DashboardSummaryService>().getSummary();
120+
default:
121+
_log.warning('Unsupported model type "$modelName" for fetch operation.');
122+
throw OperationFailedException(
123+
'Unsupported model type "$modelName" for fetch operation.',
124+
);
125+
}
126+
} on NotFoundException {
127+
// The repository will throw this if the item doesn't exist.
128+
// We return null to let the main middleware handler throw a more
129+
// detailed exception.
130+
return null;
131+
} catch (e, s) {
132+
_log.severe(
133+
'Unhandled exception in _fetchItem for model "$modelName", id "$id".',
134+
e,
135+
s,
136+
);
137+
// Re-throw as a standard exception type that the main error handler
138+
// can process into a 500 error, while preserving the original cause.
139+
throw OperationFailedException(
140+
'An internal error occurred while fetching the item: $e',
141+
);
142+
}
143+
}
Lines changed: 25 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import 'package:core/core.dart';
22
import 'package:dart_frog/dart_frog.dart';
3-
import 'package:data_repository/data_repository.dart';
43
import 'package:flutter_news_app_api_server_full_source_code/src/rbac/permission_service.dart';
54
import 'package:flutter_news_app_api_server_full_source_code/src/registry/model_registry.dart';
65

@@ -19,28 +18,27 @@ class FetchedItem<T> {
1918
/// Middleware to check if the authenticated user is the owner of the requested
2019
/// item.
2120
///
22-
/// This middleware is designed to run on item-specific routes (e.g., `/[id]`).
23-
/// It performs the following steps:
21+
/// This middleware runs *after* the `dataFetchMiddleware`, which means it can
22+
/// safely assume that the requested item has already been fetched and is
23+
/// available in the context.
2424
///
25-
/// 1. Determines if an ownership check is required for the current action
26-
/// (GET, PUT, DELETE) based on the `ModelConfig`.
27-
/// 2. If a check is required and the user is not an admin, it fetches the
28-
/// item from the database.
29-
/// 3. It then compares the item's owner ID with the authenticated user's ID.
30-
/// 4. If the check fails, it throws a [ForbiddenException].
31-
/// 5. If the check passes, it provides the fetched item into the request
32-
/// context via `context.provide<FetchedItem<dynamic>>`. This prevents the
33-
/// downstream route handler from needing to fetch the item again.
25+
/// It performs the following steps:
26+
/// 1. Determines if an ownership check is required for the current action
27+
/// based on the `ModelConfig`.
28+
/// 2. If a check is required and the user is not an admin, it reads the
29+
/// pre-fetched item from the context.
30+
/// 3. It then compares the item's owner ID with the authenticated user's ID.
31+
/// 4. If the IDs do not match, it throws a [ForbiddenException].
32+
/// 5. If the check is not required or passes, it calls the next handler.
3433
Middleware ownershipCheckMiddleware() {
3534
return (handler) {
3635
return (context) async {
37-
final modelName = context.read<String>();
3836
final modelConfig = context.read<ModelConfig<dynamic>>();
3937
final user = context.read<User>();
4038
final permissionService = context.read<PermissionService>();
4139
final method = context.request.method;
42-
final id = context.request.uri.pathSegments.last;
4340

41+
// Determine the required permission configuration for the current method.
4442
ModelActionPermission permission;
4543
switch (method) {
4644
case HttpMethod.get:
@@ -50,54 +48,41 @@ Middleware ownershipCheckMiddleware() {
5048
case HttpMethod.delete:
5149
permission = modelConfig.deletePermission;
5250
default:
53-
// For other methods, no ownership check is performed here.
51+
// For any other methods, no ownership check is performed.
5452
return handler(context);
5553
}
5654

57-
// If no ownership check is required or if the user is an admin,
58-
// proceed to the next handler without fetching the item.
55+
// If no ownership check is required for this action, or if the user is
56+
// an admin (who bypasses ownership checks), proceed immediately.
5957
if (!permission.requiresOwnershipCheck ||
6058
permissionService.isAdmin(user)) {
6159
return handler(context);
6260
}
6361

62+
// At this point, an ownership check is required for a non-admin user.
63+
64+
// Ensure the model is configured to support ownership checks.
6465
if (modelConfig.getOwnerId == null) {
6566
throw const OperationFailedException(
6667
'Internal Server Error: Model configuration error for ownership check.',
6768
);
6869
}
6970

70-
final userIdForRepoCall = user.id;
71-
dynamic item;
72-
73-
switch (modelName) {
74-
case 'user':
75-
final repo = context.read<DataRepository<User>>();
76-
item = await repo.read(id: id, userId: userIdForRepoCall);
77-
case 'user_app_settings':
78-
final repo = context.read<DataRepository<UserAppSettings>>();
79-
item = await repo.read(id: id, userId: userIdForRepoCall);
80-
case 'user_content_preferences':
81-
final repo = context.read<DataRepository<UserContentPreferences>>();
82-
item = await repo.read(id: id, userId: userIdForRepoCall);
83-
default:
84-
throw OperationFailedException(
85-
'Ownership check not implemented for model "$modelName".',
86-
);
87-
}
71+
// Read the item that was pre-fetched by the dataFetchMiddleware.
72+
// This is guaranteed to exist because dataFetchMiddleware would have
73+
// thrown a NotFoundException if the item did not exist.
74+
final item = context.read<FetchedItem<dynamic>>().data;
8875

76+
// Compare the item's owner ID with the authenticated user's ID.
8977
final itemOwnerId = modelConfig.getOwnerId!(item);
9078
if (itemOwnerId != user.id) {
9179
throw const ForbiddenException(
9280
'You do not have permission to access this item.',
9381
);
9482
}
9583

96-
final updatedContext = context.provide<FetchedItem<dynamic>>(
97-
() => FetchedItem(item),
98-
);
99-
100-
return handler(updatedContext);
84+
// If the ownership check passes, proceed to the final route handler.
85+
return handler(context);
10186
};
10287
};
10388
}

routes/_middleware.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,20 @@ Handler middleware(Handler handler) {
3434
if (!_loggerConfigured) {
3535
Logger.root.level = Level.ALL;
3636
Logger.root.onRecord.listen((record) {
37+
// A more detailed logger that includes the error and stack trace.
3738
// ignore: avoid_print
3839
print(
3940
'${record.level.name}: ${record.time}: ${record.loggerName}: '
4041
'${record.message}',
4142
);
43+
if (record.error != null) {
44+
// ignore: avoid_print
45+
print(' ERROR: ${record.error}');
46+
}
47+
if (record.stackTrace != null) {
48+
// ignore: avoid_print
49+
print(' STACK TRACE: ${record.stackTrace}');
50+
}
4251
});
4352
_loggerConfigured = true;
4453
}
Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
import 'package:dart_frog/dart_frog.dart';
2+
import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/data_fetch_middleware.dart';
23
import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/ownership_check_middleware.dart';
34

45
/// Middleware specific to the item-level `/api/v1/data/[id]` route path.
56
///
6-
/// This middleware applies the [ownershipCheckMiddleware] to perform an
7-
/// ownership check on the requested item *after* the parent middleware
8-
/// (`/api/v1/data/_middleware.dart`) has already performed authentication and
9-
/// authorization checks.
7+
/// This middleware chain is responsible for fetching the requested data item
8+
/// and then performing an ownership check on it.
109
///
11-
/// This ensures that only authorized users can proceed, and then this
12-
/// middleware adds the final layer of security by verifying item ownership
13-
/// for non-admin users when required by the model's configuration.
10+
/// The execution order is as follows:
11+
/// 1. `dataFetchMiddleware`: This runs first. It fetches the item by its ID
12+
/// from the database and provides it to the context. If the item is not
13+
/// found, it throws a `NotFoundException`, aborting the request.
14+
/// 2. `ownershipCheckMiddleware`: This runs second. It reads the fetched item
15+
/// from the context and verifies that the authenticated user is the owner,
16+
/// if the model's configuration requires such a check.
17+
///
18+
/// This ensures that the final route handler only executes for valid,
19+
/// authorized requests and can safely assume the requested item exists.
1420
Handler middleware(Handler handler) {
15-
// The `ownershipCheckMiddleware` will run after the middleware from
16-
// `/api/v1/data/_middleware.dart` (authn, authz, model validation).
17-
return handler.use(ownershipCheckMiddleware());
21+
// The middleware is applied in reverse order of execution.
22+
// `ownershipCheckMiddleware` is the inner middleware, running after
23+
// `dataFetchMiddleware`.
24+
return handler
25+
.use(ownershipCheckMiddleware()) // Runs second
26+
.use(dataFetchMiddleware()); // Runs first
1827
}

0 commit comments

Comments
 (0)