Skip to content

Commit f665430

Browse files
authored
Merge pull request #1 from headlines-toolkit/role_based_access_control_implementaion
Role based access control implementaion
2 parents 50174d2 + f680062 commit f665430

16 files changed

+1025
-706
lines changed

README.md

Lines changed: 67 additions & 208 deletions
Original file line numberDiff line numberDiff line change
@@ -1,208 +1,62 @@
11
# ht_api
22

3-
![coverage: percentage](https://img.shields.io/badge/coverage-22-green)
3+
![coverage: percentage](https://img.shields.io/badge/coverage-XX-green)
44
[![style: very good analysis](https://img.shields.io/badge/style-very_good_analysis-B22C89.svg)](https://pub.dev/packages/very_good_analysis)
55
[![License: PolyForm Free Trial](https://img.shields.io/badge/License-PolyForm%20Free%20Trial-blue)](https://polyformproject.org/licenses/free-trial/1.0.0)
66

7-
## Overview
8-
9-
`ht_api` is the central backend API service for the Headlines Toolkit (HT) project. Built with Dart using the Dart Frog framework, it provides essential APIs to support HT client applications (like the mobile app and web dashboard). It aims for simplicity, maintainability, and scalability, currently offering APIs for data access and user settings management.
10-
11-
## API Endpoints:
12-
13-
### Authentication (`/api/v1/auth`)
14-
15-
These endpoints handle user authentication flows.
16-
17-
**Standard Response Structure:** Uses the same `SuccessApiResponse` and error structure as the Data API. Authentication success responses typically use `SuccessApiResponse<AuthSuccessResponse>` (containing User and token) or `SuccessApiResponse<User>`.
18-
19-
**Authentication Operations:**
20-
21-
1. **Request Sign-In Code**
22-
* **Method:** `POST`
23-
* **Path:** `/api/v1/auth/request-code`
24-
* **Request Body:** JSON object `{"email": "[email protected]"}`.
25-
* **Success Response:** `202 Accepted` (Indicates request accepted, email sending initiated).
26-
* **Example:** `POST /api/v1/auth/request-code` with body `{"email": "[email protected]"}`
27-
28-
2. **Verify Sign-In Code**
29-
* **Method:** `POST`
30-
* **Path:** `/api/v1/auth/verify-code`
31-
* **Request Body:** JSON object `{"email": "[email protected]", "code": "123456"}`.
32-
* **Success Response:** `200 OK` with `SuccessApiResponse<AuthSuccessResponse>` containing the `User` object and the authentication `token`.
33-
* **Error Response:** `400 Bad Request` (e.g., invalid code/email format), `400 Bad Request` via `InvalidInputException` (e.g., code incorrect/expired).
34-
* **Example:** `POST /api/v1/auth/verify-code` with body `{"email": "[email protected]", "code": "654321"}`
35-
36-
3. **Sign In Anonymously**
37-
* **Method:** `POST`
38-
* **Path:** `/api/v1/auth/anonymous`
39-
* **Request Body:** None.
40-
* **Success Response:** `200 OK` with `SuccessApiResponse<AuthSuccessResponse>` containing the anonymous `User` object and the authentication `token`.
41-
* **Example:** `POST /api/v1/auth/anonymous`
42-
43-
4. **Initiate Account Linking (Anonymous User)**
44-
* **Method:** `POST`
45-
* **Path:** `/api/v1/auth/link-email`
46-
* **Authentication:** Required (Bearer Token of an *anonymous* user).
47-
* **Request Body:** JSON object `{"email": "[email protected]"}`.
48-
* **Success Response:** `202 Accepted` (Indicates request accepted, email sending initiated).
49-
* **Error Response:** `401 Unauthorized` (if not authenticated), `400 Bad Request` (if not anonymous or invalid email), `409 Conflict` (if email is already in use or linking is pending).
50-
* **Example:** `POST /api/v1/auth/link-email` with body `{"email": "[email protected]"}` and `Authorization: Bearer <anonymous_token>` header.
51-
52-
5. **Complete Account Linking (Anonymous User)**
53-
* **Method:** `POST`
54-
* **Path:** `/api/v1/auth/verify-link-email`
55-
* **Authentication:** Required (Bearer Token of the *anonymous* user who initiated the link).
56-
* **Request Body:** JSON object `{"code": "123456"}`.
57-
* **Success Response:** `200 OK` with `SuccessApiResponse<AuthSuccessResponse>` containing the updated (now permanent) `User` object and a **new** authentication `token`.
58-
* **Error Response:** `401 Unauthorized` (if not authenticated), `400 Bad Request` (if not anonymous or invalid code), `400 Bad Request` via `InvalidInputException` (if code is incorrect/expired).
59-
* **Example:** `POST /api/v1/auth/verify-link-email` with body `{"code": "654321"}` and `Authorization: Bearer <anonymous_token>` header.
60-
61-
6. **Get Current User Details**
62-
* **Method:** `GET`
63-
* **Path:** `/api/v1/auth/me`
64-
* **Authentication:** Required (Bearer Token).
65-
* **Success Response:** `200 OK` with `SuccessApiResponse<User>` containing the details of the authenticated user.
66-
* **Error Response:** `401 Unauthorized`.
67-
* **Example:** `GET /api/v1/auth/me` with `Authorization: Bearer <token>` header.
68-
69-
7. **Sign Out**
70-
* **Method:** `POST`
71-
* **Path:** `/api/v1/auth/sign-out`
72-
* **Authentication:** Required (Bearer Token).
73-
* **Request Body:** None.
74-
* **Success Response:** `204 No Content` (Indicates successful server-side action, if any). Client is responsible for clearing local token.
75-
* **Error Response:** `401 Unauthorized`.
76-
* **Example:** `POST /api/v1/auth/sign-out` with `Authorization: Bearer <token>` header.
77-
78-
8. **Delete Account**
79-
* **Method:** `DELETE`
80-
* **Path:** `/api/v1/auth/delete-account`
81-
* **Authentication:** Required (Bearer Token).
82-
* **Request Body:** None.
83-
* **Success Response:** `204 No Content` (Indicates successful deletion).
84-
* **Error Response:** `401 Unauthorized` (if not authenticated), `404 Not Found` (if the user was already deleted), or other standard errors via the error handler middleware.
85-
* **Example:** `DELETE /api/v1/auth/delete-account` with `Authorization: Bearer <token>` header.
86-
87-
### Data (`/api/v1/data`)
88-
89-
**Authentication required for all operations.**
90-
91-
This endpoint serves as the single entry point for accessing different data models. The specific model is determined by the `model` query parameter.
92-
93-
**Supported `model` values (currently global):** `headline`, `category`, `source`, `country`
94-
95-
**Standard Response Structure:** (Applies to both Data and Settings APIs)
96-
97-
* **Success:**
98-
```json
99-
{
100-
"data": <item_or_paginated_list_or_settings_object>,
101-
"metadata": {
102-
"request_id": "unique-uuid-v4-per-request",
103-
"timestamp": "iso-8601-utc-timestamp"
104-
}
105-
}
106-
```
107-
* **Error:**
108-
```json
109-
{
110-
"error": {
111-
"code": "ERROR_CODE_STRING",
112-
"message": "Descriptive error message"
113-
}
114-
}
115-
```
7+
🚀 Accelerate the development of your news application backend with **ht_api**, the
8+
dedicated API service for the Headlines Toolkit. Built on the high-performance
9+
Dart Frog framework, `ht_api` provides the essential server-side infrastructure
10+
specifically designed to power robust and feature-rich news applications.
11+
12+
`ht_api` is a core component of the **Headlines Toolkit**, a comprehensive,
13+
source-available ecosystem designed for building feature-rich news
14+
applications, which also includes a Flutter mobile app and a web-based content
15+
management dashboard.
16+
17+
## ✨ Key Capabilities
18+
19+
* 🔒 **Effortless User Authentication:** Provide secure and seamless user access
20+
with flexible flows including passwordless sign-in, anonymous access, and
21+
the ability to easily link anonymous accounts to permanent ones. Focus on
22+
user experience while `ht_api` handles the security complexities.
23+
24+
* ⚙️ **Synchronized App Settings:** Ensure a consistent and personalized user
25+
experience across devices by effortlessly syncing application preferences
26+
like theme, language, font styles, and more.
27+
28+
* 👤 **Personalized User Preferences:** Enable richer user interactions by
29+
managing and syncing user-specific data such as saved headlines, search
30+
history, or other personalized content tailored to individual users.
31+
32+
* 💾 **Robust Data Management:** Securely manage core news application data,
33+
including headlines, categories, and sources, through a well-structured
34+
and protected API.
35+
36+
* 🔧 **Solid Technical Foundation:** Built with Dart and the high-performance
37+
Dart Frog framework, offering a maintainable codebase, standardized API
38+
responses, and built-in access control for developers.
39+
40+
## 🔌 API Endpoints
11641

117-
**Data Operations:**
118-
119-
1. **Get All Items (Collection)**
120-
* **Method:** `GET`
121-
* **Path:** `/api/v1/data?model=<model_name>`
122-
* **Optional Query Parameters:** `limit=<int>`, `startAfterId=<string>`, other filtering params.
123-
* **Success Response:** `200 OK` with `SuccessApiResponse<PaginatedResponse<T>>`.
124-
* **Example:** `GET /api/v1/data?model=headline&limit=10`
125-
126-
2. **Create Item**
127-
* **Method:** `POST`
128-
* **Path:** `/api/v1/data?model=<model_name>`
129-
* **Request Body:** JSON object representing the item to create (using `camelCase` keys).
130-
* **Success Response:** `201 Created` with `SuccessApiResponse<T>` containing the created item.
131-
* **Example:** `POST /api/v1/data?model=category` with body `{"name": "Sports", "description": "News about sports"}` (Requires Bearer token)
132-
133-
3. **Get Item by ID**
134-
* **Method:** `GET`
135-
* **Path:** `/api/v1/data/<item_id>?model=<model_name>`
136-
* **Authentication:** Required (Bearer Token).
137-
* **Success Response:** `200 OK` with `SuccessApiResponse<T>`.
138-
* **Error Response:** `401 Unauthorized`, `404 Not Found`.
139-
* **Example:** `GET /api/v1/data/some-headline-id?model=headline` (Requires Bearer token)
140-
141-
4. **Update Item by ID**
142-
* **Method:** `PUT`
143-
* **Path:** `/api/v1/data/<item_id>?model=<model_name>`
144-
* **Authentication:** Required (Bearer Token).
145-
* **Request Body:** JSON object representing the complete updated item (must include `id`, using `camelCase` keys).
146-
* **Success Response:** `200 OK` with `SuccessApiResponse<T>`.
147-
* **Error Response:** `401 Unauthorized`, `404 Not Found`, `400 Bad Request`.
148-
* **Example:** `PUT /api/v1/data/some-category-id?model=category` with updated category JSON (Requires Bearer token).
149-
150-
5. **Delete Item by ID**
151-
* **Method:** `DELETE`
152-
* **Path:** `/api/v1/data/<item_id>?model=<model_name>`
153-
* **Authentication:** Required (Bearer Token).
154-
* **Success Response:** `204 No Content`.
155-
* **Error Response:** `401 Unauthorized`, `404 Not Found`.
156-
* **Example:** `DELETE /api/v1/data/some-source-id?model=source` (Requires Bearer token)
157-
158-
### User Settings (`/api/v1/users/{userId}/settings`)
159-
160-
These endpoints manage application settings for an authenticated user. The `{userId}` in the path must match the ID of the authenticated user.
161-
162-
**Authentication Required for all operations.**
163-
164-
**Standard Response Structure:** Uses the same `SuccessApiResponse` and error structure as the Data API.
165-
166-
**Settings Operations:**
167-
168-
1. **Get Display Settings**
169-
* **Method:** `GET`
170-
* **Path:** `/api/v1/users/{userId}/settings/display`
171-
* **Success Response:** `200 OK` with `SuccessApiResponse<DisplaySettings>`.
172-
* **Error Response:** `401 Unauthorized`, `403 Forbidden`.
173-
* **Example:** `GET /api/v1/users/user-abc-123/settings/display` (Requires Bearer token for user-abc-123)
174-
175-
2. **Update Display Settings**
176-
* **Method:** `PUT`
177-
* **Path:** `/api/v1/users/{userId}/settings/display`
178-
* **Request Body:** JSON object representing the complete `DisplaySettings` (using `camelCase` keys).
179-
* **Success Response:** `200 OK` with `SuccessApiResponse<DisplaySettings>` containing the updated settings.
180-
* **Error Response:** `401 Unauthorized`, `403 Forbidden`, `400 Bad Request`.
181-
* **Example:** `PUT /api/v1/users/user-abc-123/settings/display` with body `{"baseTheme": "dark", ...}` (Requires Bearer token for user-abc-123).
182-
183-
3. **Get Language Setting**
184-
* **Method:** `GET`
185-
* **Path:** `/api/v1/users/{userId}/settings/language`
186-
* **Success Response:** `200 OK` with `SuccessApiResponse<Map<String, String>>` (e.g., `{"data": {"language": "en"}, ...}`).
187-
* **Error Response:** `401 Unauthorized`, `403 Forbidden`.
188-
* **Example:** `GET /api/v1/users/user-abc-123/settings/language` (Requires Bearer token for user-abc-123)
189-
190-
4. **Update Language Setting**
191-
* **Method:** `PUT`
192-
* **Path:** `/api/v1/users/{userId}/settings/language`
193-
* **Request Body:** JSON object `{"language": "<code>"}` (e.g., `{"language": "es"}`).
194-
* **Success Response:** `200 OK` with `SuccessApiResponse<Map<String, String>>` containing the updated language setting.
195-
* **Error Response:** `401 Unauthorized`, `403 Forbidden`, `400 Bad Request`.
196-
* **Example:** `PUT /api/v1/users/user-abc-123/settings/language` with body `{"language": "fr"}` (Requires Bearer token for user-abc-123).
197-
198-
5. **Clear All Settings**
199-
* **Method:** `DELETE`
200-
* **Path:** `/api/v1/users/{userId}/settings`
201-
* **Success Response:** `204 No Content`.
202-
* **Error Response:** `401 Unauthorized`, `403 Forbidden`.
203-
* **Example:** `DELETE /api/v1/users/user-abc-123/settings` (Requires Bearer token for user-abc-123)
204-
205-
## Setup & Running
42+
`ht_api` provides a clear and organized API surface under the `/api/v1/` path.
43+
Key endpoint groups cover authentication, data access, and user settings.
44+
45+
For complete API specifications, detailed endpoint documentation,
46+
request/response schemas, and error codes, please refer to the dedicated
47+
documentation website [todo:Link to the docs website].
48+
49+
## 🔑 Access and Licensing
50+
51+
`ht_api` is source-available as part of the Headlines Toolkit ecosystem.
52+
53+
The source code for `ht_api` is available for review as part of the Headlines
54+
Toolkit ecosystem. To acquire a commercial license for building unlimited news
55+
applications with the Headlines Toolkit repositories, please visit the
56+
[Headlines Toolkit GitHub organization page](https://github.com/headlines-toolkit)
57+
for more details.
58+
59+
## 💻 Setup & Running
20660

20761
1. **Prerequisites:**
20862
* Dart SDK (`>=3.0.0`)
@@ -220,16 +74,21 @@ These endpoints manage application settings for an authenticated user. The `{use
22074
```bash
22175
dart_frog dev
22276
```
223-
The API will typically be available at `http://localhost:8080`. Fixture data from `lib/src/fixtures/` will be loaded into the in-memory repositories on startup.
77+
The API will typically be available at `http://localhost:8080`. Fixture data
78+
from `lib/src/fixtures/` will be loaded into the in-memory repositories on
79+
startup.
22480

225-
## Testing
81+
## Testing
22682

227-
* Run tests and check coverage (aim for >= 90%):
228-
```bash
229-
# Ensure very_good_cli is activated: dart pub global activate very_good_cli
230-
very_good test --min-coverage 90
231-
```
232-
233-
## License
83+
Ensure the API is robust and meets quality standards by running the test suite:
84+
85+
```bash
86+
# Ensure very_good_cli is activated: dart pub global activate very_good_cli
87+
very_good test --min-coverage 90
88+
```
89+
90+
Aim for a minimum of 90% line coverage.
91+
92+
## 📄 License
23493

23594
This package is licensed under the [PolyForm Free Trial](LICENSE). Please review the terms before use.

analysis_options.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ analyzer:
88
lines_longer_than_80_chars: ignore
99
avoid_dynamic_calls: ignore
1010
avoid_catching_errors: ignore
11+
document_ignores: ignore
1112
exclude:
1213
- build/**
1314
linter:
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 =
32+
context.read<ModelConfig<dynamic>>(); // Provided by data/_middleware
33+
final method = context.request.method;
34+
35+
// Determine the required permission configuration based on the HTTP method
36+
ModelActionPermission requiredPermissionConfig;
37+
switch (method) {
38+
case HttpMethod.get:
39+
requiredPermissionConfig = modelConfig.getPermission;
40+
case HttpMethod.post:
41+
requiredPermissionConfig = modelConfig.postPermission;
42+
case HttpMethod.put:
43+
requiredPermissionConfig = modelConfig.putPermission;
44+
case HttpMethod.delete:
45+
requiredPermissionConfig = modelConfig.deletePermission;
46+
default:
47+
// Should ideally be caught earlier by Dart Frog's routing,
48+
// but as a safeguard, deny unsupported methods.
49+
throw const ForbiddenException(
50+
'Method not supported for this resource.',
51+
);
52+
}
53+
54+
// Perform the permission check based on the configuration type
55+
switch (requiredPermissionConfig.type) {
56+
case RequiredPermissionType.none:
57+
// No specific permission required (beyond authentication if applicable)
58+
// This case is primarily for documentation/completeness if a route
59+
// group didn't require authentication, but the /data route does.
60+
// For the /data route, 'none' effectively means 'authenticated users allowed'.
61+
break;
62+
case RequiredPermissionType.adminOnly:
63+
// Requires the user to be an admin
64+
if (!permissionService.isAdmin(user)) {
65+
throw const ForbiddenException(
66+
'Only administrators can perform this action.',
67+
);
68+
}
69+
case RequiredPermissionType.specificPermission:
70+
// Requires a specific permission string
71+
final permission = requiredPermissionConfig.permission;
72+
if (permission == null) {
73+
// This indicates a configuration error in ModelRegistry
74+
print(
75+
'[AuthorizationMiddleware] Configuration Error: specificPermission '
76+
'type requires a permission string for model "$modelName", method "$method".',
77+
);
78+
throw const OperationFailedException(
79+
'Internal Server Error: Authorization configuration error.',
80+
);
81+
}
82+
if (!permissionService.hasPermission(user, permission)) {
83+
throw const ForbiddenException(
84+
'You do not have permission to perform this action.',
85+
);
86+
}
87+
}
88+
89+
// If all checks pass, proceed to the next handler in the chain.
90+
// Instance-level ownership checks (if requiredPermissionConfig.requiresOwnershipCheck is true)
91+
// are handled by the route handlers themselves.
92+
return handler(context);
93+
};
94+
};
95+
}

0 commit comments

Comments
 (0)