Skip to content

Role based access control implementaion #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 67 additions & 208 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,208 +1,62 @@
# ht_api

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

## Overview

`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.

## API Endpoints:

### Authentication (`/api/v1/auth`)

These endpoints handle user authentication flows.

**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>`.

**Authentication Operations:**

1. **Request Sign-In Code**
* **Method:** `POST`
* **Path:** `/api/v1/auth/request-code`
* **Request Body:** JSON object `{"email": "[email protected]"}`.
* **Success Response:** `202 Accepted` (Indicates request accepted, email sending initiated).
* **Example:** `POST /api/v1/auth/request-code` with body `{"email": "[email protected]"}`

2. **Verify Sign-In Code**
* **Method:** `POST`
* **Path:** `/api/v1/auth/verify-code`
* **Request Body:** JSON object `{"email": "[email protected]", "code": "123456"}`.
* **Success Response:** `200 OK` with `SuccessApiResponse<AuthSuccessResponse>` containing the `User` object and the authentication `token`.
* **Error Response:** `400 Bad Request` (e.g., invalid code/email format), `400 Bad Request` via `InvalidInputException` (e.g., code incorrect/expired).
* **Example:** `POST /api/v1/auth/verify-code` with body `{"email": "[email protected]", "code": "654321"}`

3. **Sign In Anonymously**
* **Method:** `POST`
* **Path:** `/api/v1/auth/anonymous`
* **Request Body:** None.
* **Success Response:** `200 OK` with `SuccessApiResponse<AuthSuccessResponse>` containing the anonymous `User` object and the authentication `token`.
* **Example:** `POST /api/v1/auth/anonymous`

4. **Initiate Account Linking (Anonymous User)**
* **Method:** `POST`
* **Path:** `/api/v1/auth/link-email`
* **Authentication:** Required (Bearer Token of an *anonymous* user).
* **Request Body:** JSON object `{"email": "[email protected]"}`.
* **Success Response:** `202 Accepted` (Indicates request accepted, email sending initiated).
* **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).
* **Example:** `POST /api/v1/auth/link-email` with body `{"email": "[email protected]"}` and `Authorization: Bearer <anonymous_token>` header.

5. **Complete Account Linking (Anonymous User)**
* **Method:** `POST`
* **Path:** `/api/v1/auth/verify-link-email`
* **Authentication:** Required (Bearer Token of the *anonymous* user who initiated the link).
* **Request Body:** JSON object `{"code": "123456"}`.
* **Success Response:** `200 OK` with `SuccessApiResponse<AuthSuccessResponse>` containing the updated (now permanent) `User` object and a **new** authentication `token`.
* **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).
* **Example:** `POST /api/v1/auth/verify-link-email` with body `{"code": "654321"}` and `Authorization: Bearer <anonymous_token>` header.

6. **Get Current User Details**
* **Method:** `GET`
* **Path:** `/api/v1/auth/me`
* **Authentication:** Required (Bearer Token).
* **Success Response:** `200 OK` with `SuccessApiResponse<User>` containing the details of the authenticated user.
* **Error Response:** `401 Unauthorized`.
* **Example:** `GET /api/v1/auth/me` with `Authorization: Bearer <token>` header.

7. **Sign Out**
* **Method:** `POST`
* **Path:** `/api/v1/auth/sign-out`
* **Authentication:** Required (Bearer Token).
* **Request Body:** None.
* **Success Response:** `204 No Content` (Indicates successful server-side action, if any). Client is responsible for clearing local token.
* **Error Response:** `401 Unauthorized`.
* **Example:** `POST /api/v1/auth/sign-out` with `Authorization: Bearer <token>` header.

8. **Delete Account**
* **Method:** `DELETE`
* **Path:** `/api/v1/auth/delete-account`
* **Authentication:** Required (Bearer Token).
* **Request Body:** None.
* **Success Response:** `204 No Content` (Indicates successful deletion).
* **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.
* **Example:** `DELETE /api/v1/auth/delete-account` with `Authorization: Bearer <token>` header.

### Data (`/api/v1/data`)

**Authentication required for all operations.**

This endpoint serves as the single entry point for accessing different data models. The specific model is determined by the `model` query parameter.

**Supported `model` values (currently global):** `headline`, `category`, `source`, `country`

**Standard Response Structure:** (Applies to both Data and Settings APIs)

* **Success:**
```json
{
"data": <item_or_paginated_list_or_settings_object>,
"metadata": {
"request_id": "unique-uuid-v4-per-request",
"timestamp": "iso-8601-utc-timestamp"
}
}
```
* **Error:**
```json
{
"error": {
"code": "ERROR_CODE_STRING",
"message": "Descriptive error message"
}
}
```
🚀 Accelerate the development of your news application backend with **ht_api**, the
dedicated API service for the Headlines Toolkit. Built on the high-performance
Dart Frog framework, `ht_api` provides the essential server-side infrastructure
specifically designed to power robust and feature-rich news applications.

`ht_api` is a core component of the **Headlines Toolkit**, a comprehensive,
source-available ecosystem designed for building feature-rich news
applications, which also includes a Flutter mobile app and a web-based content
management dashboard.

## ✨ Key Capabilities

* 🔒 **Effortless User Authentication:** Provide secure and seamless user access
with flexible flows including passwordless sign-in, anonymous access, and
the ability to easily link anonymous accounts to permanent ones. Focus on
user experience while `ht_api` handles the security complexities.

* ⚙️ **Synchronized App Settings:** Ensure a consistent and personalized user
experience across devices by effortlessly syncing application preferences
like theme, language, font styles, and more.

* 👤 **Personalized User Preferences:** Enable richer user interactions by
managing and syncing user-specific data such as saved headlines, search
history, or other personalized content tailored to individual users.

* 💾 **Robust Data Management:** Securely manage core news application data,
including headlines, categories, and sources, through a well-structured
and protected API.

* 🔧 **Solid Technical Foundation:** Built with Dart and the high-performance
Dart Frog framework, offering a maintainable codebase, standardized API
responses, and built-in access control for developers.

## 🔌 API Endpoints

**Data Operations:**

1. **Get All Items (Collection)**
* **Method:** `GET`
* **Path:** `/api/v1/data?model=<model_name>`
* **Optional Query Parameters:** `limit=<int>`, `startAfterId=<string>`, other filtering params.
* **Success Response:** `200 OK` with `SuccessApiResponse<PaginatedResponse<T>>`.
* **Example:** `GET /api/v1/data?model=headline&limit=10`

2. **Create Item**
* **Method:** `POST`
* **Path:** `/api/v1/data?model=<model_name>`
* **Request Body:** JSON object representing the item to create (using `camelCase` keys).
* **Success Response:** `201 Created` with `SuccessApiResponse<T>` containing the created item.
* **Example:** `POST /api/v1/data?model=category` with body `{"name": "Sports", "description": "News about sports"}` (Requires Bearer token)

3. **Get Item by ID**
* **Method:** `GET`
* **Path:** `/api/v1/data/<item_id>?model=<model_name>`
* **Authentication:** Required (Bearer Token).
* **Success Response:** `200 OK` with `SuccessApiResponse<T>`.
* **Error Response:** `401 Unauthorized`, `404 Not Found`.
* **Example:** `GET /api/v1/data/some-headline-id?model=headline` (Requires Bearer token)

4. **Update Item by ID**
* **Method:** `PUT`
* **Path:** `/api/v1/data/<item_id>?model=<model_name>`
* **Authentication:** Required (Bearer Token).
* **Request Body:** JSON object representing the complete updated item (must include `id`, using `camelCase` keys).
* **Success Response:** `200 OK` with `SuccessApiResponse<T>`.
* **Error Response:** `401 Unauthorized`, `404 Not Found`, `400 Bad Request`.
* **Example:** `PUT /api/v1/data/some-category-id?model=category` with updated category JSON (Requires Bearer token).

5. **Delete Item by ID**
* **Method:** `DELETE`
* **Path:** `/api/v1/data/<item_id>?model=<model_name>`
* **Authentication:** Required (Bearer Token).
* **Success Response:** `204 No Content`.
* **Error Response:** `401 Unauthorized`, `404 Not Found`.
* **Example:** `DELETE /api/v1/data/some-source-id?model=source` (Requires Bearer token)

### User Settings (`/api/v1/users/{userId}/settings`)

These endpoints manage application settings for an authenticated user. The `{userId}` in the path must match the ID of the authenticated user.

**Authentication Required for all operations.**

**Standard Response Structure:** Uses the same `SuccessApiResponse` and error structure as the Data API.

**Settings Operations:**

1. **Get Display Settings**
* **Method:** `GET`
* **Path:** `/api/v1/users/{userId}/settings/display`
* **Success Response:** `200 OK` with `SuccessApiResponse<DisplaySettings>`.
* **Error Response:** `401 Unauthorized`, `403 Forbidden`.
* **Example:** `GET /api/v1/users/user-abc-123/settings/display` (Requires Bearer token for user-abc-123)

2. **Update Display Settings**
* **Method:** `PUT`
* **Path:** `/api/v1/users/{userId}/settings/display`
* **Request Body:** JSON object representing the complete `DisplaySettings` (using `camelCase` keys).
* **Success Response:** `200 OK` with `SuccessApiResponse<DisplaySettings>` containing the updated settings.
* **Error Response:** `401 Unauthorized`, `403 Forbidden`, `400 Bad Request`.
* **Example:** `PUT /api/v1/users/user-abc-123/settings/display` with body `{"baseTheme": "dark", ...}` (Requires Bearer token for user-abc-123).

3. **Get Language Setting**
* **Method:** `GET`
* **Path:** `/api/v1/users/{userId}/settings/language`
* **Success Response:** `200 OK` with `SuccessApiResponse<Map<String, String>>` (e.g., `{"data": {"language": "en"}, ...}`).
* **Error Response:** `401 Unauthorized`, `403 Forbidden`.
* **Example:** `GET /api/v1/users/user-abc-123/settings/language` (Requires Bearer token for user-abc-123)

4. **Update Language Setting**
* **Method:** `PUT`
* **Path:** `/api/v1/users/{userId}/settings/language`
* **Request Body:** JSON object `{"language": "<code>"}` (e.g., `{"language": "es"}`).
* **Success Response:** `200 OK` with `SuccessApiResponse<Map<String, String>>` containing the updated language setting.
* **Error Response:** `401 Unauthorized`, `403 Forbidden`, `400 Bad Request`.
* **Example:** `PUT /api/v1/users/user-abc-123/settings/language` with body `{"language": "fr"}` (Requires Bearer token for user-abc-123).

5. **Clear All Settings**
* **Method:** `DELETE`
* **Path:** `/api/v1/users/{userId}/settings`
* **Success Response:** `204 No Content`.
* **Error Response:** `401 Unauthorized`, `403 Forbidden`.
* **Example:** `DELETE /api/v1/users/user-abc-123/settings` (Requires Bearer token for user-abc-123)

## Setup & Running
`ht_api` provides a clear and organized API surface under the `/api/v1/` path.
Key endpoint groups cover authentication, data access, and user settings.

For complete API specifications, detailed endpoint documentation,
request/response schemas, and error codes, please refer to the dedicated
documentation website [todo:Link to the docs website].

## 🔑 Access and Licensing

`ht_api` is source-available as part of the Headlines Toolkit ecosystem.

The source code for `ht_api` is available for review as part of the Headlines
Toolkit ecosystem. To acquire a commercial license for building unlimited news
applications with the Headlines Toolkit repositories, please visit the
[Headlines Toolkit GitHub organization page](https://github.com/headlines-toolkit)
for more details.

## 💻 Setup & Running

1. **Prerequisites:**
* Dart SDK (`>=3.0.0`)
Expand All @@ -220,16 +74,21 @@ These endpoints manage application settings for an authenticated user. The `{use
```bash
dart_frog dev
```
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.
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.

## Testing
## Testing

* Run tests and check coverage (aim for >= 90%):
```bash
# Ensure very_good_cli is activated: dart pub global activate very_good_cli
very_good test --min-coverage 90
```

## License
Ensure the API is robust and meets quality standards by running the test suite:

```bash
# Ensure very_good_cli is activated: dart pub global activate very_good_cli
very_good test --min-coverage 90
```

Aim for a minimum of 90% line coverage.

## 📄 License

This package is licensed under the [PolyForm Free Trial](LICENSE). Please review the terms before use.
1 change: 1 addition & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ analyzer:
lines_longer_than_80_chars: ignore
avoid_dynamic_calls: ignore
avoid_catching_errors: ignore
document_ignores: ignore
exclude:
- build/**
linter:
Expand Down
95 changes: 95 additions & 0 deletions lib/src/middlewares/authorization_middleware.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import 'package:dart_frog/dart_frog.dart';
import 'package:ht_api/src/rbac/permission_service.dart';
import 'package:ht_api/src/registry/model_registry.dart';
import 'package:ht_shared/ht_shared.dart'; // For User, ForbiddenException

/// {@template authorization_middleware}
/// Middleware to enforce role-based permissions and model-specific access rules.
///
/// This middleware reads the authenticated [User], the requested `modelName`,
/// the `HttpMethod`, and the `ModelConfig` from the request context. It then
/// determines the required permission based on the `ModelConfig` metadata for
/// the specific HTTP method and checks if the authenticated user has that
/// permission using the [PermissionService].
///
/// If the user does not have the required permission, it throws a
/// [ForbiddenException], which should be caught by the 'errorHandler' middleware.
///
/// This middleware runs *after* authentication and model validation.
/// It does NOT perform instance-level ownership checks; those are handled
/// by the route handlers (`index.dart`, `[id].dart`) if required by the
/// `ModelActionPermission.requiresOwnershipCheck` flag.
/// {@endtemplate}
Middleware authorizationMiddleware() {
return (handler) {
return (context) async {
// Read dependencies from the context.
// User is guaranteed non-null by requireAuthentication() middleware.
final user = context.read<User>();
final permissionService = context.read<PermissionService>();
final modelName = context.read<String>(); // Provided by data/_middleware
final modelConfig =
context.read<ModelConfig<dynamic>>(); // Provided by data/_middleware
final method = context.request.method;

// Determine the required permission configuration based on the HTTP method
ModelActionPermission requiredPermissionConfig;
switch (method) {
case HttpMethod.get:
requiredPermissionConfig = modelConfig.getPermission;
case HttpMethod.post:
requiredPermissionConfig = modelConfig.postPermission;
case HttpMethod.put:
requiredPermissionConfig = modelConfig.putPermission;
case HttpMethod.delete:
requiredPermissionConfig = modelConfig.deletePermission;
default:
// Should ideally be caught earlier by Dart Frog's routing,
// but as a safeguard, deny unsupported methods.
throw const ForbiddenException(
'Method not supported for this resource.',
);
}

// Perform the permission check based on the configuration type
switch (requiredPermissionConfig.type) {
case RequiredPermissionType.none:
// No specific permission required (beyond authentication if applicable)
// This case is primarily for documentation/completeness if a route
// group didn't require authentication, but the /data route does.
// For the /data route, 'none' effectively means 'authenticated users allowed'.
break;
case RequiredPermissionType.adminOnly:
// Requires the user to be an admin
if (!permissionService.isAdmin(user)) {
throw const ForbiddenException(
'Only administrators can perform this action.',
);
}
case RequiredPermissionType.specificPermission:
// Requires a specific permission string
final permission = requiredPermissionConfig.permission;
if (permission == null) {
// This indicates a configuration error in ModelRegistry
print(
'[AuthorizationMiddleware] Configuration Error: specificPermission '
'type requires a permission string for model "$modelName", method "$method".',
);
throw const OperationFailedException(
'Internal Server Error: Authorization configuration error.',
);
}
if (!permissionService.hasPermission(user, permission)) {
throw const ForbiddenException(
'You do not have permission to perform this action.',
);
}
}

// If all checks pass, proceed to the next handler in the chain.
// Instance-level ownership checks (if requiredPermissionConfig.requiresOwnershipCheck is true)
// are handled by the route handlers themselves.
return handler(context);
};
};
}
Loading