Skip to content

Commit 16007a9

Browse files
committed
fix: updated lib/src/services/jwt_auth_token_service.dart to correctly serialize the UserRole enum in the JWT payload. This change addresses the JWTException seen in the server logs for the /api/v1/auth/anonymous and /api/v1/auth/verify-code routes
1 parent e8ea54c commit 16007a9

File tree

10 files changed

+958
-492
lines changed

10 files changed

+958
-492
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import 'package:dart_frog/dart_frog.dart';
2+
import 'package:ht_api/src/rbac/permission_service.dart';
3+
import 'package:ht_api/src/registry/model_registry.dart';
4+
import 'package:ht_shared/ht_shared.dart'; // For User, ForbiddenException
5+
6+
/// {@template authorization_middleware}
7+
/// Middleware to enforce role-based permissions and model-specific access rules.
8+
///
9+
/// This middleware reads the authenticated [User], the requested `modelName`,
10+
/// the `HttpMethod`, and the `ModelConfig` from the request context. It then
11+
/// determines the required permission based on the `ModelConfig` metadata for
12+
/// the specific HTTP method and checks if the authenticated user has that
13+
/// permission using the [PermissionService].
14+
///
15+
/// If the user does not have the required permission, it throws a
16+
/// [ForbiddenException], which should be caught by the [errorHandler] middleware.
17+
///
18+
/// This middleware runs *after* authentication and model validation.
19+
/// It does NOT perform instance-level ownership checks; those are handled
20+
/// by the route handlers (`index.dart`, `[id].dart`) if required by the
21+
/// `ModelActionPermission.requiresOwnershipCheck` flag.
22+
/// {@endtemplate}
23+
Middleware authorizationMiddleware() {
24+
return (handler) {
25+
return (context) async {
26+
// Read dependencies from the context.
27+
// User is guaranteed non-null by requireAuthentication() middleware.
28+
final user = context.read<User>();
29+
final permissionService = context.read<PermissionService>();
30+
final modelName = context.read<String>(); // Provided by data/_middleware
31+
final modelConfig = context.read<ModelConfig<dynamic>>(); // Provided by data/_middleware
32+
final method = context.request.method;
33+
34+
// Determine the required permission configuration based on the HTTP method
35+
ModelActionPermission requiredPermissionConfig;
36+
switch (method) {
37+
case HttpMethod.get:
38+
requiredPermissionConfig = modelConfig.getPermission;
39+
case HttpMethod.post:
40+
requiredPermissionConfig = modelConfig.postPermission;
41+
case HttpMethod.put:
42+
requiredPermissionConfig = modelConfig.putPermission;
43+
case HttpMethod.delete:
44+
requiredPermissionConfig = modelConfig.deletePermission;
45+
default:
46+
// Should ideally be caught earlier by Dart Frog's routing,
47+
// but as a safeguard, deny unsupported methods.
48+
throw const ForbiddenException('Method not supported for this resource.');
49+
}
50+
51+
// Perform the permission check based on the configuration type
52+
switch (requiredPermissionConfig.type) {
53+
case RequiredPermissionType.none:
54+
// No specific permission required (beyond authentication if applicable)
55+
// This case is primarily for documentation/completeness if a route
56+
// group didn't require authentication, but the /data route does.
57+
// For the /data route, 'none' effectively means 'authenticated users allowed'.
58+
break;
59+
case RequiredPermissionType.adminOnly:
60+
// Requires the user to be an admin
61+
if (!permissionService.isAdmin(user)) {
62+
throw const ForbiddenException(
63+
'Only administrators can perform this action.',
64+
);
65+
}
66+
case RequiredPermissionType.specificPermission:
67+
// Requires a specific permission string
68+
final permission = requiredPermissionConfig.permission;
69+
if (permission == null) {
70+
// This indicates a configuration error in ModelRegistry
71+
print(
72+
'[AuthorizationMiddleware] Configuration Error: specificPermission '
73+
'type requires a permission string for model "$modelName", method "$method".',
74+
);
75+
throw const OperationFailedException(
76+
'Internal Server Error: Authorization configuration error.',
77+
);
78+
}
79+
if (!permissionService.hasPermission(user, permission)) {
80+
throw const ForbiddenException(
81+
'You do not have permission to perform this action.',
82+
);
83+
}
84+
}
85+
86+
// If all checks pass, proceed to the next handler in the chain.
87+
// Instance-level ownership checks (if requiredPermissionConfig.requiresOwnershipCheck is true)
88+
// are handled by the route handlers themselves.
89+
return handler(context);
90+
};
91+
};
92+
}

lib/src/rbac/permission_service.dart

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import 'package:ht_api/src/rbac/role_permissions.dart';
2+
import 'package:ht_shared/ht_shared.dart';
3+
4+
/// {@template permission_service}
5+
/// Service responsible for checking if a user has a specific permission.
6+
///
7+
/// This service uses the predefined [rolePermissions] map to determine
8+
/// a user's access rights based on their [UserRole]. It also includes
9+
/// an explicit check for the [UserRole.admin], granting them all permissions.
10+
/// {@endtemplate}
11+
class PermissionService {
12+
/// {@macro permission_service}
13+
const PermissionService();
14+
15+
/// Checks if the given [user] has the specified [permission].
16+
///
17+
/// Returns `true` if the user's role grants the permission, or if the user
18+
/// is an administrator. Returns `false` otherwise.
19+
///
20+
/// - [user]: The authenticated user.
21+
/// - [permission]: The permission string to check (e.g., `headline.read`).
22+
bool hasPermission(User user, String permission) {
23+
// Administrators have all permissions
24+
if (user.role == UserRole.admin) {
25+
return true;
26+
}
27+
28+
// Check if the user's role is in the map and has the permission
29+
return rolePermissions[user.role]?.contains(permission) ?? false;
30+
}
31+
32+
/// Checks if the given [user] has the [UserRole.admin] role.
33+
///
34+
/// This is a convenience method for checks that are strictly limited
35+
/// to administrators, bypassing the permission map.
36+
///
37+
/// - [user]: The authenticated user.
38+
bool isAdmin(User user) {
39+
return user.role == UserRole.admin;
40+
}
41+
}

lib/src/rbac/permissions.dart

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// ignore_for_file: public_member_api_docs
2+
3+
/// Defines the available permissions in the system.
4+
///
5+
/// Permissions follow the format `resource.action`.
6+
abstract class Permissions {
7+
// Headline Permissions
8+
static const String headlineCreate = 'headline.create';
9+
static const String headlineRead = 'headline.read';
10+
static const String headlineUpdate = 'headline.update';
11+
static const String headlineDelete = 'headline.delete';
12+
13+
// Category Permissions
14+
static const String categoryCreate = 'category.create';
15+
static const String categoryRead = 'category.read';
16+
static const String categoryUpdate = 'category.update';
17+
static const String categoryDelete = 'category.delete';
18+
19+
// Source Permissions
20+
static const String sourceCreate = 'source.create';
21+
static const String sourceRead = 'source.read';
22+
static const String sourceUpdate = 'source.update';
23+
static const String sourceDelete = 'source.delete';
24+
25+
// Country Permissions
26+
static const String countryCreate = 'country.create';
27+
static const String countryRead = 'country.read';
28+
static const String countryUpdate = 'country.update';
29+
static const String countryDelete = 'country.delete';
30+
31+
// User Permissions
32+
// Allows reading any user profile (e.g., for admin or public profiles)
33+
static const String userRead = 'user.read';
34+
// Allows reading the authenticated user's own profile
35+
static const String userReadOwned = 'user.read_owned';
36+
// Allows updating the authenticated user's own profile
37+
static const String userUpdateOwned = 'user.update_owned';
38+
// Allows deleting the authenticated user's own account
39+
static const String userDeleteOwned = 'user.delete_owned';
40+
41+
// App Settings Permissions (User-owned)
42+
static const String appSettingsReadOwned = 'app_settings.read_owned';
43+
static const String appSettingsUpdateOwned = 'app_settings.update_owned';
44+
45+
// User Preferences Permissions (User-owned)
46+
static const String userPreferencesReadOwned = 'user_preferences.read_owned';
47+
static const String userPreferencesUpdateOwned = 'user_preferences.update_owned';
48+
49+
// Remote Config Permissions (Admin-owned/managed)
50+
static const String remoteConfigReadAdmin = 'remote_config.read_admin';
51+
static const String remoteConfigUpdateAdmin = 'remote_config.update_admin';
52+
53+
// Add other permissions as needed for future models/features
54+
}

lib/src/rbac/role_permissions.dart

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import 'package:ht_api/src/rbac/permission_service.dart' show PermissionService;
2+
import 'package:ht_api/src/rbac/permissions.dart';
3+
import 'package:ht_shared/ht_shared.dart'; // Assuming UserRole is defined here
4+
5+
/// Defines the mapping between user roles and the permissions they possess.
6+
///
7+
/// This map is the core of the Role-Based Access Control (RBAC) system.
8+
/// Each key is a [UserRole], and the associated value is a [Set] of
9+
/// [Permissions] strings that users with that role are granted.
10+
///
11+
/// Note: Administrators typically have implicit access to all resources
12+
/// regardless of this map, but including their permissions here can aid
13+
/// documentation and clarity. The [PermissionService] should handle the
14+
/// explicit admin bypass if desired.
15+
final Map<UserRole, Set<String>> rolePermissions = {
16+
UserRole.admin: {
17+
// Admins typically have all permissions. Listing them explicitly
18+
// or handling the admin bypass in PermissionService are options.
19+
// For clarity, listing some key admin permissions here:
20+
Permissions.headlineCreate,
21+
Permissions.headlineRead,
22+
Permissions.headlineUpdate,
23+
Permissions.headlineDelete,
24+
Permissions.categoryCreate,
25+
Permissions.categoryRead,
26+
Permissions.categoryUpdate,
27+
Permissions.categoryDelete,
28+
Permissions.sourceCreate,
29+
Permissions.sourceRead,
30+
Permissions.sourceUpdate,
31+
Permissions.sourceDelete,
32+
Permissions.countryCreate,
33+
Permissions.countryRead,
34+
Permissions.countryUpdate,
35+
Permissions.countryDelete,
36+
Permissions.userRead, // Admins can read any user profile
37+
Permissions.userReadOwned,
38+
Permissions.userUpdateOwned,
39+
Permissions.userDeleteOwned,
40+
Permissions.appSettingsReadOwned,
41+
Permissions.appSettingsUpdateOwned,
42+
Permissions.userPreferencesReadOwned,
43+
Permissions.userPreferencesUpdateOwned,
44+
Permissions.remoteConfigReadAdmin,
45+
Permissions.remoteConfigUpdateAdmin,
46+
// Add all other permissions here for completeness if not using admin bypass
47+
},
48+
UserRole.standardUser: {
49+
// Standard users can read public/shared data
50+
Permissions.headlineRead,
51+
Permissions.categoryRead,
52+
Permissions.sourceRead,
53+
Permissions.countryRead,
54+
// Standard users can manage their own user-owned data
55+
Permissions.userReadOwned,
56+
Permissions.userUpdateOwned,
57+
Permissions.userDeleteOwned,
58+
Permissions.appSettingsReadOwned,
59+
Permissions.appSettingsUpdateOwned,
60+
Permissions.userPreferencesReadOwned,
61+
Permissions.userPreferencesUpdateOwned,
62+
// Add other permissions for standard users as needed
63+
},
64+
UserRole.guestUser: {
65+
// Guest users have very limited permissions, primarily reading public data
66+
Permissions.headlineRead,
67+
Permissions.categoryRead,
68+
Permissions.sourceRead,
69+
Permissions.countryRead,
70+
// Add other permissions for guest users as needed
71+
},
72+
};

0 commit comments

Comments
 (0)