mirror of
https://github.com/LogicLabs-OU/OpenArchiver.git
synced 2026-04-06 00:31:57 +02:00
OpenAPI specs for API docs
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
import { useSidebar } from 'vitepress-openapi';
|
||||
import spec from '../api/openapi.json';
|
||||
|
||||
export default defineConfig({
|
||||
head: [
|
||||
@@ -95,7 +97,12 @@ export default defineConfig({
|
||||
{ text: 'Integrity Check', link: '/api/integrity' },
|
||||
{ text: 'Search', link: '/api/search' },
|
||||
{ text: 'Storage', link: '/api/storage' },
|
||||
{ text: 'Upload', link: '/api/upload' },
|
||||
{ text: 'Jobs', link: '/api/jobs' },
|
||||
{ text: 'Users', link: '/api/users' },
|
||||
{ text: 'IAM', link: '/api/iam' },
|
||||
{ text: 'API Keys', link: '/api/api-keys' },
|
||||
{ text: 'Settings', link: '/api/settings' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
19
docs/.vitepress/theme/index.ts
Normal file
19
docs/.vitepress/theme/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import DefaultTheme from 'vitepress/theme';
|
||||
import type { EnhanceAppContext } from 'vitepress';
|
||||
import { theme, useOpenapi } from 'vitepress-openapi/client';
|
||||
import 'vitepress-openapi/dist/style.css';
|
||||
import spec from '../../api/openapi.json';
|
||||
|
||||
export default {
|
||||
...DefaultTheme,
|
||||
enhanceApp({ app, router, siteData }: EnhanceAppContext) {
|
||||
// Delegate to DefaultTheme first
|
||||
DefaultTheme.enhanceApp?.({ app, router, siteData });
|
||||
|
||||
// Install vitepress-openapi theme: registers i18n plugin + all OA components
|
||||
theme.enhanceApp({ app, router, siteData });
|
||||
|
||||
// Initialize the global OpenAPI spec
|
||||
useOpenapi({ spec });
|
||||
},
|
||||
};
|
||||
19
docs/api/api-keys.md
Normal file
19
docs/api/api-keys.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
# API Keys
|
||||
|
||||
Generate and manage API keys for programmatic access to the Open Archiver API. API keys are scoped to the user that created them and carry the same permissions as that user. The raw key value is only shown once at creation time.
|
||||
|
||||
## Generate an API Key
|
||||
|
||||
<OAOperation operationId="generateApiKey" />
|
||||
|
||||
## List API Keys
|
||||
|
||||
<OAOperation operationId="getApiKeys" />
|
||||
|
||||
## Delete an API Key
|
||||
|
||||
<OAOperation operationId="deleteApiKey" />
|
||||
@@ -1,107 +1,19 @@
|
||||
# Archived Email Service API
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
The Archived Email Service is responsible for retrieving archived emails and their details from the database and storage.
|
||||
# Archived Email API
|
||||
|
||||
## Endpoints
|
||||
Endpoints for retrieving and deleting archived emails. All endpoints require authentication and the appropriate `archive` permission.
|
||||
|
||||
All endpoints in this service require authentication.
|
||||
## List Emails for an Ingestion Source
|
||||
|
||||
### GET /api/v1/archived-emails/ingestion-source/:ingestionSourceId
|
||||
<OAOperation operationId="getArchivedEmails" />
|
||||
|
||||
Retrieves a paginated list of archived emails for a specific ingestion source.
|
||||
## Get a Single Email
|
||||
|
||||
**Access:** Authenticated
|
||||
<OAOperation operationId="getArchivedEmailById" />
|
||||
|
||||
#### URL Parameters
|
||||
## Delete an Email
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :------------------ | :----- | :------------------------------------------------ |
|
||||
| `ingestionSourceId` | string | The ID of the ingestion source to get emails for. |
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Parameter | Type | Description | Default |
|
||||
| :-------- | :----- | :------------------------------ | :------ |
|
||||
| `page` | number | The page number for pagination. | 1 |
|
||||
| `limit` | number | The number of items per page. | 10 |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200 OK:** A paginated list of archived emails.
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "email-id",
|
||||
"subject": "Test Email",
|
||||
"from": "sender@example.com",
|
||||
"sentAt": "2023-10-27T10:00:00.000Z",
|
||||
"hasAttachments": true,
|
||||
"recipients": [{ "name": "Recipient 1", "email": "recipient1@example.com" }]
|
||||
}
|
||||
],
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
}
|
||||
```
|
||||
|
||||
- **500 Internal Server Error:** An unexpected error occurred.
|
||||
|
||||
### GET /api/v1/archived-emails/:id
|
||||
|
||||
Retrieves a single archived email by its ID, including its raw content and attachments.
|
||||
|
||||
**Access:** Authenticated
|
||||
|
||||
#### URL Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :----- | :---------------------------- |
|
||||
| `id` | string | The ID of the archived email. |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200 OK:** The archived email details.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "email-id",
|
||||
"subject": "Test Email",
|
||||
"from": "sender@example.com",
|
||||
"sentAt": "2023-10-27T10:00:00.000Z",
|
||||
"hasAttachments": true,
|
||||
"recipients": [{ "name": "Recipient 1", "email": "recipient1@example.com" }],
|
||||
"raw": "...",
|
||||
"attachments": [
|
||||
{
|
||||
"id": "attachment-id",
|
||||
"filename": "document.pdf",
|
||||
"mimeType": "application/pdf",
|
||||
"sizeBytes": 12345
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **404 Not Found:** The archived email with the specified ID was not found.
|
||||
- **500 Internal Server Error:** An unexpected error occurred.
|
||||
|
||||
## Service Methods
|
||||
|
||||
### `getArchivedEmails(ingestionSourceId: string, page: number, limit: number): Promise<PaginatedArchivedEmails>`
|
||||
|
||||
Retrieves a paginated list of archived emails from the database for a given ingestion source.
|
||||
|
||||
- **ingestionSourceId:** The ID of the ingestion source.
|
||||
- **page:** The page number for pagination.
|
||||
- **limit:** The number of items per page.
|
||||
- **Returns:** A promise that resolves to a `PaginatedArchivedEmails` object.
|
||||
|
||||
### `getArchivedEmailById(emailId: string): Promise<ArchivedEmail | null>`
|
||||
|
||||
Retrieves a single archived email by its ID, including its raw content and attachments.
|
||||
|
||||
- **emailId:** The ID of the archived email.
|
||||
- **Returns:** A promise that resolves to an `ArchivedEmail` object or `null` if not found.
|
||||
<OAOperation operationId="deleteArchivedEmail" />
|
||||
|
||||
@@ -1,84 +1,19 @@
|
||||
# Auth Service API
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
The Auth Service is responsible for handling user authentication, including login and token verification.
|
||||
# Auth API
|
||||
|
||||
## Endpoints
|
||||
Handles user authentication including initial setup, login, and application setup status.
|
||||
|
||||
### POST /api/v1/auth/login
|
||||
## Setup
|
||||
|
||||
Authenticates a user and returns a JWT if the credentials are valid.
|
||||
<OAOperation operationId="authSetup" />
|
||||
|
||||
**Access:** Public
|
||||
## Login
|
||||
|
||||
**Rate Limiting:** This endpoint is rate-limited to prevent brute-force attacks.
|
||||
<OAOperation operationId="authLogin" />
|
||||
|
||||
#### Request Body
|
||||
## Check Setup Status
|
||||
|
||||
| Field | Type | Description |
|
||||
| :--------- | :----- | :------------------------ |
|
||||
| `email` | string | The user's email address. |
|
||||
| `password` | string | The user's password. |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200 OK:** Authentication successful.
|
||||
|
||||
```json
|
||||
{
|
||||
"accessToken": "your.jwt.token",
|
||||
"user": {
|
||||
"id": "user-id",
|
||||
"email": "user@example.com",
|
||||
"role": "user"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **400 Bad Request:** Email or password not provided.
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Email and password are required"
|
||||
}
|
||||
```
|
||||
|
||||
- **401 Unauthorized:** Invalid credentials.
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Invalid credentials"
|
||||
}
|
||||
```
|
||||
|
||||
- **500 Internal Server Error:** An unexpected error occurred.
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "An internal server error occurred"
|
||||
}
|
||||
```
|
||||
|
||||
## Service Methods
|
||||
|
||||
### `verifyPassword(password: string, hash: string): Promise<boolean>`
|
||||
|
||||
Compares a plain-text password with a hashed password to verify its correctness.
|
||||
|
||||
- **password:** The plain-text password.
|
||||
- **hash:** The hashed password to compare against.
|
||||
- **Returns:** A promise that resolves to `true` if the password is valid, otherwise `false`.
|
||||
|
||||
### `login(email: string, password: string): Promise<LoginResponse | null>`
|
||||
|
||||
Handles the user login process. It finds the user by email, verifies the password, and generates a JWT upon successful authentication.
|
||||
|
||||
- **email:** The user's email.
|
||||
- **password:** The user's password.
|
||||
- **Returns:** A promise that resolves to a `LoginResponse` object containing the `accessToken` and `user` details, or `null` if authentication fails.
|
||||
|
||||
### `verifyToken(token: string): Promise<AuthTokenPayload | null>`
|
||||
|
||||
Verifies the authenticity and expiration of a JWT.
|
||||
|
||||
- **token:** The JWT string to verify.
|
||||
- **Returns:** A promise that resolves to the token's `AuthTokenPayload` if valid, otherwise `null`.
|
||||
<OAOperation operationId="authStatus" />
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
# API Authentication
|
||||
|
||||
To access protected API endpoints, you need to include a user-generated API key in the `X-API-KEY` header of your requests.
|
||||
The API supports two authentication methods. Use whichever fits your use case.
|
||||
|
||||
## 1. Creating an API Key
|
||||
## Method 1: JWT (User Login)
|
||||
|
||||
You can create, manage, and view your API keys through the application's user interface.
|
||||
Obtain a short-lived JWT by calling `POST /v1/auth/login` with your email and password, then pass it as a Bearer token in the `Authorization` header.
|
||||
|
||||
1. Navigate to **Settings > API Keys** in the dashboard.
|
||||
2. Click the **"Generate API Key"** button.
|
||||
3. Provide a descriptive name for your key and select an expiration period.
|
||||
4. The new API key will be displayed. **Copy this key immediately and store it in a secure location. You will not be able to see it again.**
|
||||
**Example:**
|
||||
|
||||
## 2. Making Authenticated Requests
|
||||
```http
|
||||
GET /api/v1/dashboard/stats
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
Once you have your API key, you must include it in the `X-API-KEY` header of all subsequent requests to protected API endpoints.
|
||||
## Method 2: API Key
|
||||
|
||||
Long-lived API keys are suited for automated scripts and integrations. Create one in **Settings > API Keys**, then pass it in the `X-API-KEY` header.
|
||||
|
||||
**Example:**
|
||||
|
||||
@@ -22,4 +28,13 @@ GET /api/v1/dashboard/stats
|
||||
X-API-KEY: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
|
||||
```
|
||||
|
||||
If the API key is missing, expired, or invalid, the API will respond with a `401 Unauthorized` status code.
|
||||
### Creating an API Key
|
||||
|
||||
1. Navigate to **Settings > API Keys** in the dashboard.
|
||||
2. Click **"Generate API Key"**.
|
||||
3. Provide a descriptive name and select an expiration period (max 2 years).
|
||||
4. Copy the key immediately — it will not be shown again.
|
||||
|
||||
---
|
||||
|
||||
If the token or API key is missing, expired, or invalid, the API responds with `401 Unauthorized`.
|
||||
|
||||
@@ -1,114 +1,27 @@
|
||||
# Dashboard Service API
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
The Dashboard Service provides endpoints for retrieving statistics and data for the main dashboard.
|
||||
# Dashboard API
|
||||
|
||||
## Endpoints
|
||||
Aggregated statistics and summaries for the dashboard UI. Requires `read:dashboard` permission.
|
||||
|
||||
All endpoints in this service require authentication.
|
||||
## Get Stats
|
||||
|
||||
### GET /api/v1/dashboard/stats
|
||||
<OAOperation operationId="getDashboardStats" />
|
||||
|
||||
Retrieves overall statistics, including the total number of archived emails, total storage used, and the number of failed ingestions in the last 7 days.
|
||||
## Get Ingestion History
|
||||
|
||||
**Access:** Authenticated
|
||||
<OAOperation operationId="getIngestionHistory" />
|
||||
|
||||
#### Responses
|
||||
## Get Ingestion Source Summaries
|
||||
|
||||
- **200 OK:** An object containing the dashboard statistics.
|
||||
<OAOperation operationId="getDashboardIngestionSources" />
|
||||
|
||||
```json
|
||||
{
|
||||
"totalEmailsArchived": 12345,
|
||||
"totalStorageUsed": 54321098,
|
||||
"failedIngestionsLast7Days": 3
|
||||
}
|
||||
```
|
||||
## Get Recent Syncs
|
||||
|
||||
### GET /api/v1/dashboard/ingestion-history
|
||||
<OAOperation operationId="getRecentSyncs" />
|
||||
|
||||
Retrieves the email ingestion history for the last 30 days, grouped by day.
|
||||
## Get Indexed Email Insights
|
||||
|
||||
**Access:** Authenticated
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200 OK:** An object containing the ingestion history.
|
||||
|
||||
```json
|
||||
{
|
||||
"history": [
|
||||
{
|
||||
"date": "2023-09-27T00:00:00.000Z",
|
||||
"count": 150
|
||||
},
|
||||
{
|
||||
"date": "2023-09-28T00:00:00.000Z",
|
||||
"count": 200
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/v1/dashboard/ingestion-sources
|
||||
|
||||
Retrieves a list of all ingestion sources along with their status and storage usage.
|
||||
|
||||
**Access:** Authenticated
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200 OK:** An array of ingestion source objects.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "source-id-1",
|
||||
"name": "Google Workspace",
|
||||
"provider": "google",
|
||||
"status": "active",
|
||||
"storageUsed": 12345678
|
||||
},
|
||||
{
|
||||
"id": "source-id-2",
|
||||
"name": "Microsoft 365",
|
||||
"provider": "microsoft",
|
||||
"status": "error",
|
||||
"storageUsed": 87654321
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### GET /api/v1/dashboard/recent-syncs
|
||||
|
||||
Retrieves a list of recent synchronization jobs. (Note: This is currently a placeholder and will return an empty array).
|
||||
|
||||
**Access:** Authenticated
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200 OK:** An empty array.
|
||||
|
||||
```json
|
||||
[]
|
||||
```
|
||||
|
||||
### GET /api/v1/dashboard/indexed-insights
|
||||
|
||||
Retrieves insights from the indexed email data, such as the top senders.
|
||||
|
||||
**Access:** Authenticated
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200 OK:** An object containing indexed insights.
|
||||
|
||||
```json
|
||||
{
|
||||
"topSenders": [
|
||||
{
|
||||
"sender": "user@example.com",
|
||||
"count": 42
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
<OAOperation operationId="getIndexedInsights" />
|
||||
|
||||
27
docs/api/iam.md
Normal file
27
docs/api/iam.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
# IAM API
|
||||
|
||||
Manage Identity and Access Management roles and their CASL policy statements. Role management requires Super Admin (`manage:all`) permission. Reading roles requires `read:roles` permission.
|
||||
|
||||
## List All Roles
|
||||
|
||||
<OAOperation operationId="getRoles" />
|
||||
|
||||
## Create a Role
|
||||
|
||||
<OAOperation operationId="createRole" />
|
||||
|
||||
## Get a Role
|
||||
|
||||
<OAOperation operationId="getRoleById" />
|
||||
|
||||
## Update a Role
|
||||
|
||||
<OAOperation operationId="updateRole" />
|
||||
|
||||
## Delete a Role
|
||||
|
||||
<OAOperation operationId="deleteRole" />
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
# API Overview
|
||||
|
||||
Welcome to the Open Archiver API documentation. This section provides detailed information about the available API endpoints.
|
||||
|
||||
@@ -1,196 +1,39 @@
|
||||
# Ingestion Service API
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
The Ingestion Service manages ingestion sources, which are configurations for connecting to email providers and importing emails.
|
||||
# Ingestion API
|
||||
|
||||
## Endpoints
|
||||
Manage ingestion sources — the configured connections to email providers (Google Workspace, Microsoft 365, IMAP, and file imports). Credentials are never returned in responses.
|
||||
|
||||
All endpoints in this service require authentication.
|
||||
## Create an Ingestion Source
|
||||
|
||||
### POST /api/v1/ingestion-sources
|
||||
<OAOperation operationId="createIngestionSource" />
|
||||
|
||||
Creates a new ingestion source.
|
||||
## List Ingestion Sources
|
||||
|
||||
**Access:** Authenticated
|
||||
<OAOperation operationId="listIngestionSources" />
|
||||
|
||||
#### Request Body
|
||||
## Get an Ingestion Source
|
||||
|
||||
The request body should be a `CreateIngestionSourceDto` object.
|
||||
<OAOperation operationId="getIngestionSourceById" />
|
||||
|
||||
```typescript
|
||||
interface CreateIngestionSourceDto {
|
||||
name: string;
|
||||
provider: 'google_workspace' | 'microsoft_365' | 'generic_imap' | 'pst_import' | 'eml_import' | 'mbox_import';
|
||||
providerConfig: IngestionCredentials;
|
||||
}
|
||||
```
|
||||
## Update an Ingestion Source
|
||||
|
||||
#### Example: Creating an Mbox Import Source with File Upload
|
||||
<OAOperation operationId="updateIngestionSource" />
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Mbox Import",
|
||||
"provider": "mbox_import",
|
||||
"providerConfig": {
|
||||
"type": "mbox_import",
|
||||
"uploadedFileName": "emails.mbox",
|
||||
"uploadedFilePath": "open-archiver/tmp/uuid-emails.mbox"
|
||||
}
|
||||
}
|
||||
```
|
||||
## Delete an Ingestion Source
|
||||
|
||||
#### Example: Creating an Mbox Import Source with Local File Path
|
||||
<OAOperation operationId="deleteIngestionSource" />
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Mbox Import",
|
||||
"provider": "mbox_import",
|
||||
"providerConfig": {
|
||||
"type": "mbox_import",
|
||||
"localFilePath": "/path/to/emails.mbox"
|
||||
}
|
||||
}
|
||||
```
|
||||
## Trigger Initial Import
|
||||
|
||||
**Note:** When using `localFilePath`, the file will not be deleted after import. When using `uploadedFilePath` (via the upload API), the file will be automatically deleted after import. The same applies to `pst_import` and `eml_import` providers.
|
||||
<OAOperation operationId="triggerInitialImport" />
|
||||
|
||||
**Important regarding `localFilePath`:** When running OpenArchiver in a Docker container (which is the standard deployment), `localFilePath` refers to the path **inside the Docker container**, not on the host machine.
|
||||
To use a local file:
|
||||
1. **Recommended:** Place your file inside the directory defined by `STORAGE_LOCAL_ROOT_PATH` (e.g., inside a `temp` folder). Since this directory is already mounted as a volume, the file will be accessible at the same path inside the container.
|
||||
2. **Alternative:** Mount a specific directory containing your files as a volume in `docker-compose.yml`. For example, add `- /path/to/my/files:/imports` to the `volumes` section and use `/imports/myfile.pst` as the `localFilePath`.
|
||||
## Pause an Ingestion Source
|
||||
|
||||
#### Responses
|
||||
<OAOperation operationId="pauseIngestionSource" />
|
||||
|
||||
- **201 Created:** The newly created ingestion source.
|
||||
- **500 Internal Server Error:** An unexpected error occurred.
|
||||
## Force Sync
|
||||
|
||||
### GET /api/v1/ingestion-sources
|
||||
|
||||
Retrieves all ingestion sources.
|
||||
|
||||
**Access:** Authenticated
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200 OK:** An array of ingestion source objects.
|
||||
- **500 Internal Server Error:** An unexpected error occurred.
|
||||
|
||||
### GET /api/v1/ingestion-sources/:id
|
||||
|
||||
Retrieves a single ingestion source by its ID.
|
||||
|
||||
**Access:** Authenticated
|
||||
|
||||
#### URL Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :----- | :------------------------------ |
|
||||
| `id` | string | The ID of the ingestion source. |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200 OK:** The ingestion source object.
|
||||
- **404 Not Found:** Ingestion source not found.
|
||||
- **500 Internal Server Error:** An unexpected error occurred.
|
||||
|
||||
### PUT /api/v1/ingestion-sources/:id
|
||||
|
||||
Updates an existing ingestion source.
|
||||
|
||||
**Access:** Authenticated
|
||||
|
||||
#### URL Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :----- | :------------------------------ |
|
||||
| `id` | string | The ID of the ingestion source. |
|
||||
|
||||
#### Request Body
|
||||
|
||||
The request body should be an `UpdateIngestionSourceDto` object.
|
||||
|
||||
```typescript
|
||||
interface UpdateIngestionSourceDto {
|
||||
name?: string;
|
||||
provider?: 'google' | 'microsoft' | 'generic_imap';
|
||||
providerConfig?: IngestionCredentials;
|
||||
status?: 'pending_auth' | 'auth_success' | 'importing' | 'active' | 'paused' | 'error';
|
||||
}
|
||||
```
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200 OK:** The updated ingestion source object.
|
||||
- **404 Not Found:** Ingestion source not found.
|
||||
- **500 Internal Server Error:** An unexpected error occurred.
|
||||
|
||||
### DELETE /api/v1/ingestion-sources/:id
|
||||
|
||||
Deletes an ingestion source and all associated data.
|
||||
|
||||
**Access:** Authenticated
|
||||
|
||||
#### URL Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :----- | :------------------------------ |
|
||||
| `id` | string | The ID of the ingestion source. |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **204 No Content:** The ingestion source was deleted successfully.
|
||||
- **404 Not Found:** Ingestion source not found.
|
||||
- **500 Internal Server Error:** An unexpected error occurred.
|
||||
|
||||
### POST /api/v1/ingestion-sources/:id/import
|
||||
|
||||
Triggers the initial import process for an ingestion source.
|
||||
|
||||
**Access:** Authenticated
|
||||
|
||||
#### URL Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :----- | :------------------------------ |
|
||||
| `id` | string | The ID of the ingestion source. |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **202 Accepted:** The initial import was triggered successfully.
|
||||
- **404 Not Found:** Ingestion source not found.
|
||||
- **500 Internal Server Error:** An unexpected error occurred.
|
||||
|
||||
### POST /api/v1/ingestion-sources/:id/pause
|
||||
|
||||
Pauses an active ingestion source.
|
||||
|
||||
**Access:** Authenticated
|
||||
|
||||
#### URL Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :----- | :------------------------------ |
|
||||
| `id` | string | The ID of the ingestion source. |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200 OK:** The updated ingestion source object with a `paused` status.
|
||||
- **404 Not Found:** Ingestion source not found.
|
||||
- **500 Internal Server Error:** An unexpected error occurred.
|
||||
|
||||
### POST /api/v1/ingestion-sources/:id/sync
|
||||
|
||||
Triggers a forced synchronization for an ingestion source.
|
||||
|
||||
**Access:** Authenticated
|
||||
|
||||
#### URL Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :----- | :------------------------------ |
|
||||
| `id` | string | The ID of the ingestion source. |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **202 Accepted:** The force sync was triggered successfully.
|
||||
- **404 Not Found:** Ingestion source not found.
|
||||
- **500 Internal Server Error:** An unexpected error occurred.
|
||||
<OAOperation operationId="triggerForceSync" />
|
||||
|
||||
@@ -1,51 +1,11 @@
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
# Integrity Check API
|
||||
|
||||
The Integrity Check API provides an endpoint to verify the cryptographic hash of an archived email and its attachments against the stored values in the database. This allows you to ensure that the stored files have not been tampered with or corrupted since they were archived.
|
||||
Verify the SHA-256 hash of an archived email and all its attachments against the hashes stored at archival time.
|
||||
|
||||
## Check Email Integrity
|
||||
|
||||
Verifies the integrity of a specific archived email and all of its associated attachments.
|
||||
|
||||
- **URL:** `/api/v1/integrity/:id`
|
||||
- **Method:** `GET`
|
||||
- **URL Params:**
|
||||
- `id=[string]` (required) - The UUID of the archived email to check.
|
||||
- **Permissions:** `read:archive`
|
||||
- **Success Response:**
|
||||
- **Code:** 200 OK
|
||||
- **Content:** `IntegrityCheckResult[]`
|
||||
|
||||
### Response Body `IntegrityCheckResult`
|
||||
|
||||
An array of objects, each representing the result of an integrity check for a single file (either the email itself or an attachment).
|
||||
|
||||
| Field | Type | Description |
|
||||
| :--------- | :------------------------ | :-------------------------------------------------------------------------- |
|
||||
| `type` | `'email' \| 'attachment'` | The type of the file being checked. |
|
||||
| `id` | `string` | The UUID of the email or attachment. |
|
||||
| `filename` | `string` (optional) | The filename of the attachment. This field is only present for attachments. |
|
||||
| `isValid` | `boolean` | `true` if the current hash matches the stored hash, otherwise `false`. |
|
||||
| `reason` | `string` (optional) | A reason for the failure. Only present if `isValid` is `false`. |
|
||||
|
||||
### Example Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "email",
|
||||
"id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
|
||||
"isValid": true
|
||||
},
|
||||
{
|
||||
"type": "attachment",
|
||||
"id": "b2c3d4e5-f6a7-8901-2345-67890abcdef1",
|
||||
"filename": "document.pdf",
|
||||
"isValid": false,
|
||||
"reason": "Stored hash does not match current hash."
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
- **Error Response:**
|
||||
- **Code:** 404 Not Found
|
||||
- **Content:** `{ "message": "Archived email not found" }`
|
||||
<OAOperation operationId="checkIntegrity" />
|
||||
|
||||
133
docs/api/jobs.md
133
docs/api/jobs.md
@@ -1,135 +1,20 @@
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
# Jobs API
|
||||
|
||||
The Jobs API provides endpoints for monitoring the job queues and the jobs within them.
|
||||
|
||||
## Overview
|
||||
|
||||
Open Archiver uses a job queue system to handle asynchronous tasks like email ingestion and indexing. The system is built on Redis and BullMQ.
|
||||
Monitor BullMQ job queues for asynchronous tasks such as email ingestion, indexing, and sync scheduling. Requires Super Admin (`manage:all`) permission.
|
||||
|
||||
There are two queues:
|
||||
|
||||
- **`ingestion`** — handles all email ingestion and sync jobs (`initial-import`, `continuous-sync`, `process-mailbox`, `sync-cycle-finished`, `schedule-continuous-sync`)
|
||||
- **`indexing`** — handles batched Meilisearch document indexing (`index-email-batch`)
|
||||
|
||||
Sync cycle coordination (tracking when all mailboxes in a sync have completed) is managed via the `sync_sessions` database table rather than BullMQ's built-in flow system. This keeps Redis memory usage stable regardless of how many mailboxes are being synced.
|
||||
## List All Queues
|
||||
|
||||
### Job Statuses
|
||||
<OAOperation operationId="getQueues" />
|
||||
|
||||
Jobs can have one of the following statuses:
|
||||
## Get Jobs in a Queue
|
||||
|
||||
- **active:** The job is currently being processed.
|
||||
- **completed:** The job has been completed successfully.
|
||||
- **failed:** The job has failed after all retry attempts.
|
||||
- **delayed:** The job is delayed and will be processed at a later time.
|
||||
- **waiting:** The job is waiting to be processed.
|
||||
- **paused:** The job is paused and will not be processed until it is resumed.
|
||||
|
||||
### Errors
|
||||
|
||||
When a job fails, the `failedReason` and `stacktrace` fields will contain information about the error. The `error` field will also be populated with the `failedReason` for easier access.
|
||||
|
||||
### Job Preservation
|
||||
|
||||
Jobs are preserved for a limited time after they are completed or failed. This means that the job counts and the jobs that you see in the API are for a limited time.
|
||||
|
||||
- **Completed jobs:** The last 1000 completed jobs are preserved.
|
||||
- **Failed jobs:** The last 5000 failed jobs are preserved.
|
||||
|
||||
## Get All Queues
|
||||
|
||||
- **Endpoint:** `GET /v1/jobs/queues`
|
||||
- **Description:** Retrieves a list of all job queues and their job counts.
|
||||
- **Permissions:** `manage:all`
|
||||
- **Responses:**
|
||||
- `200 OK`: Returns a list of queue overviews.
|
||||
- `401 Unauthorized`: If the user is not authenticated.
|
||||
- `403 Forbidden`: If the user does not have the required permissions.
|
||||
|
||||
### Response Body
|
||||
|
||||
```json
|
||||
{
|
||||
"queues": [
|
||||
{
|
||||
"name": "ingestion",
|
||||
"counts": {
|
||||
"active": 0,
|
||||
"completed": 56,
|
||||
"failed": 4,
|
||||
"delayed": 3,
|
||||
"waiting": 0,
|
||||
"paused": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "indexing",
|
||||
"counts": {
|
||||
"active": 0,
|
||||
"completed": 0,
|
||||
"failed": 0,
|
||||
"delayed": 0,
|
||||
"waiting": 0,
|
||||
"paused": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Get Queue Jobs
|
||||
|
||||
- **Endpoint:** `GET /v1/jobs/queues/:queueName`
|
||||
- **Description:** Retrieves a list of jobs within a specific queue, with pagination and filtering by status.
|
||||
- **Permissions:** `manage:all`
|
||||
- **URL Parameters:**
|
||||
- `queueName` (string, required): The name of the queue to retrieve jobs from.
|
||||
- **Query Parameters:**
|
||||
- `status` (string, optional): The status of the jobs to retrieve. Can be one of `active`, `completed`, `failed`, `delayed`, `waiting`, `paused`. Defaults to `failed`.
|
||||
- `page` (number, optional): The page number to retrieve. Defaults to `1`.
|
||||
- `limit` (number, optional): The number of jobs to retrieve per page. Defaults to `10`.
|
||||
- **Responses:**
|
||||
- `200 OK`: Returns a detailed view of the queue, including a paginated list of jobs.
|
||||
- `401 Unauthorized`: If the user is not authenticated.
|
||||
- `403 Forbidden`: If the user does not have the required permissions.
|
||||
- `404 Not Found`: If the specified queue does not exist.
|
||||
|
||||
### Response Body
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "ingestion",
|
||||
"counts": {
|
||||
"active": 0,
|
||||
"completed": 56,
|
||||
"failed": 4,
|
||||
"delayed": 3,
|
||||
"waiting": 0,
|
||||
"paused": 0
|
||||
},
|
||||
"jobs": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "initial-import",
|
||||
"data": {
|
||||
"ingestionSourceId": "clx1y2z3a0000b4d2e5f6g7h8"
|
||||
},
|
||||
"state": "failed",
|
||||
"failedReason": "Error: Connection timed out",
|
||||
"timestamp": 1678886400000,
|
||||
"processedOn": 1678886401000,
|
||||
"finishedOn": 1678886402000,
|
||||
"attemptsMade": 5,
|
||||
"stacktrace": ["..."],
|
||||
"returnValue": null,
|
||||
"ingestionSourceId": "clx1y2z3a0000b4d2e5f6g7h8",
|
||||
"error": "Error: Connection timed out"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"currentPage": 1,
|
||||
"totalPages": 1,
|
||||
"totalJobs": 4,
|
||||
"limit": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
<OAOperation operationId="getQueueJobs" />
|
||||
|
||||
3544
docs/api/openapi.json
Normal file
3544
docs/api/openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,7 @@
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
# Rate Limiting
|
||||
|
||||
The API implements rate limiting as a security measure to protect your instance from denial-of-service (DoS) and brute-force attacks. This is a crucial feature for maintaining the security and stability of the application.
|
||||
|
||||
@@ -1,50 +1,11 @@
|
||||
# Search Service API
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
The Search Service provides an endpoint for searching indexed emails.
|
||||
# Search API
|
||||
|
||||
## Endpoints
|
||||
Full-text search over indexed archived emails, powered by Meilisearch.
|
||||
|
||||
All endpoints in this service require authentication.
|
||||
## Search Emails
|
||||
|
||||
### GET /api/v1/search
|
||||
|
||||
Performs a search query against the indexed emails.
|
||||
|
||||
**Access:** Authenticated
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Parameter | Type | Description | Default |
|
||||
| :----------------- | :----- | :--------------------------------------------------------------------- | :------ |
|
||||
| `keywords` | string | The search query. | |
|
||||
| `page` | number | The page number for pagination. | 1 |
|
||||
| `limit` | number | The number of items per page. | 10 |
|
||||
| `matchingStrategy` | string | The matching strategy to use (`all` or `last`). | `last` |
|
||||
| `filters` | object | Key-value pairs for filtering results (e.g., `from=user@example.com`). | |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200 OK:** A search result object.
|
||||
|
||||
```json
|
||||
{
|
||||
"hits": [
|
||||
{
|
||||
"id": "email-id",
|
||||
"subject": "Test Email",
|
||||
"from": "sender@example.com",
|
||||
"_formatted": {
|
||||
"subject": "<em>Test</em> Email"
|
||||
}
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"limit": 10,
|
||||
"totalPages": 1,
|
||||
"processingTimeMs": 5
|
||||
}
|
||||
```
|
||||
|
||||
- **400 Bad Request:** Keywords are required.
|
||||
- **500 Internal Server Error:** An unexpected error occurred.
|
||||
<OAOperation operationId="searchEmails" />
|
||||
|
||||
15
docs/api/settings.md
Normal file
15
docs/api/settings.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
# Settings API
|
||||
|
||||
Read and update system-wide configuration. The `GET` endpoint is public. The `PUT` endpoint requires `manage:settings` permission.
|
||||
|
||||
## Get System Settings
|
||||
|
||||
<OAOperation operationId="getSystemSettings" />
|
||||
|
||||
## Update System Settings
|
||||
|
||||
<OAOperation operationId="updateSystemSettings" />
|
||||
@@ -1,26 +1,11 @@
|
||||
# Storage Service API
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
The Storage Service provides an endpoint for downloading files from the configured storage provider.
|
||||
# Storage API
|
||||
|
||||
## Endpoints
|
||||
Download files from the configured storage backend (local filesystem or S3-compatible). Requires `read:archive` permission.
|
||||
|
||||
All endpoints in this service require authentication.
|
||||
## Download a File
|
||||
|
||||
### GET /api/v1/storage/download
|
||||
|
||||
Downloads a file from the storage.
|
||||
|
||||
**Access:** Authenticated
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :----- | :------------------------------------------------ |
|
||||
| `path` | string | The path to the file within the storage provider. |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200 OK:** The file stream.
|
||||
- **400 Bad Request:** File path is required or invalid.
|
||||
- **404 Not Found:** File not found.
|
||||
- **500 Internal Server Error:** An unexpected error occurred.
|
||||
<OAOperation operationId="downloadFile" />
|
||||
|
||||
11
docs/api/upload.md
Normal file
11
docs/api/upload.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
# Upload API
|
||||
|
||||
Upload files (PST, EML, MBOX) to temporary storage before creating a file-based ingestion source. The returned `filePath` should be passed as `uploadedFilePath` in the ingestion source `providerConfig`.
|
||||
|
||||
## Upload a File
|
||||
|
||||
<OAOperation operationId="uploadFile" />
|
||||
39
docs/api/users.md
Normal file
39
docs/api/users.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
# Users API
|
||||
|
||||
Manage user accounts. Creating, updating, and deleting users requires Super Admin (`manage:all`) permission.
|
||||
|
||||
## List All Users
|
||||
|
||||
<OAOperation operationId="getUsers" />
|
||||
|
||||
## Create a User
|
||||
|
||||
<OAOperation operationId="createUser" />
|
||||
|
||||
## Get a User
|
||||
|
||||
<OAOperation operationId="getUser" />
|
||||
|
||||
## Update a User
|
||||
|
||||
<OAOperation operationId="updateUser" />
|
||||
|
||||
## Delete a User
|
||||
|
||||
<OAOperation operationId="deleteUser" />
|
||||
|
||||
## Get Current User Profile
|
||||
|
||||
<OAOperation operationId="getProfile" />
|
||||
|
||||
## Update Current User Profile
|
||||
|
||||
<OAOperation operationId="updateProfile" />
|
||||
|
||||
## Update Password
|
||||
|
||||
<OAOperation operationId="updatePassword" />
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
Welcome to Open Archiver! This guide will help you get started with setting up and using the platform.
|
||||
|
||||
## What is Open Archiver? 🛡️
|
||||
## What is Open Archiver?
|
||||
|
||||
**A secure, sovereign, and affordable open-source platform for email archiving and eDiscovery.**
|
||||
|
||||
Open Archiver provides a robust, self-hosted solution for archiving, storing, indexing, and searching emails from major platforms, including Google Workspace (Gmail), Microsoft 365, as well as generic IMAP-enabled email inboxes. Use Open Archiver to keep a permanent, tamper-proof record of your communication history, free from vendor lock-in.
|
||||
|
||||
## Key Features ✨
|
||||
## Key Features
|
||||
|
||||
- **Universal Ingestion**: Connect to Google Workspace, Microsoft 365, and standard IMAP servers to perform initial bulk imports and maintain continuous, real-time synchronization.
|
||||
- **Secure & Efficient Storage**: Emails are stored in the standard `.eml` format. The system uses deduplication and compression to minimize storage costs. All data is encrypted at rest.
|
||||
@@ -17,7 +17,7 @@ Open Archiver provides a robust, self-hosted solution for archiving, storing, in
|
||||
- **Compliance & Retention**: Define granular retention policies to automatically manage the lifecycle of your data. Place legal holds on communications to prevent deletion during litigation (TBD).
|
||||
- **Comprehensive Auditing**: An immutable audit trail logs all system activities, ensuring you have a clear record of who accessed what and when (TBD).
|
||||
|
||||
## Installation 🚀
|
||||
## Installation
|
||||
|
||||
To get your own instance of Open Archiver running, follow our detailed installation guide:
|
||||
|
||||
@@ -31,7 +31,7 @@ After deploying the application, you will need to configure one or more ingestio
|
||||
- [Connecting to Microsoft 365](./user-guides/email-providers/microsoft-365.md)
|
||||
- [Connecting to a Generic IMAP Server](./user-guides/email-providers/imap.md)
|
||||
|
||||
## Contributing ❤️
|
||||
## Contributing
|
||||
|
||||
We welcome contributions from the community!
|
||||
|
||||
|
||||
@@ -19,8 +19,9 @@
|
||||
"db:migrate:dev": "dotenv -- pnpm --filter @open-archiver/backend db:migrate:dev",
|
||||
"docker-start:oss": "concurrently \"pnpm start:workers\" \"pnpm start:oss\"",
|
||||
"docker-start:enterprise": "concurrently \"pnpm start:workers\" \"pnpm start:enterprise\"",
|
||||
"docs:dev": "vitepress dev docs --port 3009",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:gen-spec": "node packages/backend/scripts/generate-openapi-spec.mjs",
|
||||
"docs:dev": "pnpm docs:gen-spec && vitepress dev docs --port 3009",
|
||||
"docs:build": "pnpm docs:gen-spec && vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check ."
|
||||
@@ -35,7 +36,8 @@
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"typescript": "5.8.3",
|
||||
"vitepress": "^1.6.4"
|
||||
"vitepress": "^1.6.4",
|
||||
"vitepress-openapi": "^0.1.18"
|
||||
},
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"engines": {
|
||||
|
||||
@@ -79,7 +79,9 @@
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^24.0.12",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"@types/swagger-jsdoc": "^6.0.4",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.8.3"
|
||||
|
||||
735
packages/backend/scripts/generate-openapi-spec.mjs
Normal file
735
packages/backend/scripts/generate-openapi-spec.mjs
Normal file
@@ -0,0 +1,735 @@
|
||||
/**
|
||||
* Generates the OpenAPI specification from swagger-jsdoc annotations in the route files.
|
||||
* Outputs the spec to docs/api/openapi.json for use with vitepress-openapi.
|
||||
*
|
||||
* Run: node packages/backend/scripts/generate-openapi-spec.mjs
|
||||
*/
|
||||
import swaggerJsdoc from 'swagger-jsdoc';
|
||||
import { writeFileSync, mkdirSync } from 'fs';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const options = {
|
||||
definition: {
|
||||
openapi: '3.1.0',
|
||||
info: {
|
||||
title: 'Open Archiver API',
|
||||
version: '1.0.0',
|
||||
description:
|
||||
'REST API for Open Archiver — an open-source email archiving platform. All authenticated endpoints require a Bearer JWT token obtained from `POST /v1/auth/login`, or an API key passed as a Bearer token.',
|
||||
license: {
|
||||
name: 'SEE LICENSE IN LICENSE',
|
||||
url: 'https://github.com/LogicLabs-OU/OpenArchiver/blob/main/LICENSE',
|
||||
},
|
||||
contact: {
|
||||
name: 'Open Archiver',
|
||||
url: 'https://openarchiver.com',
|
||||
},
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: 'http://localhost:3001',
|
||||
description: 'Local development',
|
||||
},
|
||||
],
|
||||
// Both security schemes apply globally; individual endpoints may override
|
||||
security: [{ bearerAuth: [] }, { apiKeyAuth: [] }],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description:
|
||||
'JWT obtained from `POST /v1/auth/login`. Pass as `Authorization: Bearer <token>`.',
|
||||
},
|
||||
apiKeyAuth: {
|
||||
type: 'apiKey',
|
||||
in: 'header',
|
||||
name: 'X-API-KEY',
|
||||
description:
|
||||
'API key generated via `POST /v1/api-keys`. Pass as `X-API-KEY: <key>`.',
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
Unauthorized: {
|
||||
description: 'Authentication is required or the token is invalid/expired.',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/ErrorMessage' },
|
||||
example: { message: 'Unauthorized' },
|
||||
},
|
||||
},
|
||||
},
|
||||
Forbidden: {
|
||||
description:
|
||||
'The authenticated user does not have permission to perform this action.',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/ErrorMessage' },
|
||||
example: { message: 'Forbidden' },
|
||||
},
|
||||
},
|
||||
},
|
||||
NotFound: {
|
||||
description: 'The requested resource was not found.',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/ErrorMessage' },
|
||||
example: { message: 'Not found' },
|
||||
},
|
||||
},
|
||||
},
|
||||
InternalServerError: {
|
||||
description: 'An unexpected error occurred on the server.',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/ErrorMessage' },
|
||||
example: { message: 'Internal server error' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
schemas: {
|
||||
// --- Shared utility schemas ---
|
||||
ErrorMessage: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Human-readable error description.',
|
||||
example: 'An error occurred.',
|
||||
},
|
||||
},
|
||||
required: ['message'],
|
||||
},
|
||||
MessageResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: {
|
||||
type: 'string',
|
||||
example: 'Operation completed successfully.',
|
||||
},
|
||||
},
|
||||
required: ['message'],
|
||||
},
|
||||
ValidationError: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: {
|
||||
type: 'string',
|
||||
example: 'Request body is invalid.',
|
||||
},
|
||||
errors: {
|
||||
type: 'string',
|
||||
description: 'Zod validation error details.',
|
||||
},
|
||||
},
|
||||
required: ['message'],
|
||||
},
|
||||
// --- Auth ---
|
||||
LoginResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
description: 'JWT for authenticating subsequent requests.',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
},
|
||||
user: {
|
||||
$ref: '#/components/schemas/User',
|
||||
},
|
||||
},
|
||||
required: ['accessToken', 'user'],
|
||||
},
|
||||
// --- Users ---
|
||||
User: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', example: 'clx1y2z3a0000b4d2' },
|
||||
first_name: { type: 'string', nullable: true, example: 'Jane' },
|
||||
last_name: { type: 'string', nullable: true, example: 'Doe' },
|
||||
email: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
example: 'jane.doe@example.com',
|
||||
},
|
||||
role: {
|
||||
$ref: '#/components/schemas/Role',
|
||||
nullable: true,
|
||||
},
|
||||
createdAt: { type: 'string', format: 'date-time' },
|
||||
},
|
||||
required: ['id', 'email', 'createdAt'],
|
||||
},
|
||||
// --- IAM ---
|
||||
Role: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', example: 'clx1y2z3a0000b4d2' },
|
||||
slug: { type: 'string', nullable: true, example: 'predefined_super_admin' },
|
||||
name: { type: 'string', example: 'Super Admin' },
|
||||
policies: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/CaslPolicy' },
|
||||
},
|
||||
createdAt: { type: 'string', format: 'date-time' },
|
||||
updatedAt: { type: 'string', format: 'date-time' },
|
||||
},
|
||||
required: ['id', 'name', 'policies', 'createdAt', 'updatedAt'],
|
||||
},
|
||||
CaslPolicy: {
|
||||
type: 'object',
|
||||
description:
|
||||
'An CASL-style permission policy statement. `action` and `subject` can be strings or arrays of strings. `conditions` optionally restricts access to specific resource attributes.',
|
||||
properties: {
|
||||
action: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
example: 'read',
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
example: ['read', 'search'],
|
||||
},
|
||||
],
|
||||
},
|
||||
subject: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
example: 'archive',
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
example: ['archive', 'ingestion'],
|
||||
},
|
||||
],
|
||||
},
|
||||
conditions: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Optional attribute-level conditions. Supports `${user.id}` interpolation.',
|
||||
example: { userId: '${user.id}' },
|
||||
},
|
||||
},
|
||||
required: ['action', 'subject'],
|
||||
},
|
||||
// --- API Keys ---
|
||||
ApiKey: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', example: 'clx1y2z3a0000b4d2' },
|
||||
name: { type: 'string', example: 'CI/CD Pipeline Key' },
|
||||
key: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Partial/masked key — the raw value is only available at creation time.',
|
||||
example: 'oa_live_abc1...',
|
||||
},
|
||||
expiresAt: { type: 'string', format: 'date-time' },
|
||||
createdAt: { type: 'string', format: 'date-time' },
|
||||
},
|
||||
required: ['id', 'name', 'expiresAt', 'createdAt'],
|
||||
},
|
||||
// --- Ingestion ---
|
||||
SafeIngestionSource: {
|
||||
type: 'object',
|
||||
description: 'An ingestion source with sensitive credential fields removed.',
|
||||
properties: {
|
||||
id: { type: 'string', example: 'clx1y2z3a0000b4d2' },
|
||||
name: { type: 'string', example: 'Company Google Workspace' },
|
||||
provider: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'google_workspace',
|
||||
'microsoft_365',
|
||||
'generic_imap',
|
||||
'pst_import',
|
||||
'eml_import',
|
||||
'mbox_import',
|
||||
],
|
||||
example: 'google_workspace',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'active',
|
||||
'paused',
|
||||
'error',
|
||||
'pending_auth',
|
||||
'syncing',
|
||||
'importing',
|
||||
'auth_success',
|
||||
'imported',
|
||||
],
|
||||
example: 'active',
|
||||
},
|
||||
createdAt: { type: 'string', format: 'date-time' },
|
||||
updatedAt: { type: 'string', format: 'date-time' },
|
||||
lastSyncStartedAt: { type: 'string', format: 'date-time', nullable: true },
|
||||
lastSyncFinishedAt: { type: 'string', format: 'date-time', nullable: true },
|
||||
lastSyncStatusMessage: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['id', 'name', 'provider', 'status', 'createdAt', 'updatedAt'],
|
||||
},
|
||||
CreateIngestionSourceDto: {
|
||||
type: 'object',
|
||||
required: ['name', 'provider', 'providerConfig'],
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
example: 'Company Google Workspace',
|
||||
},
|
||||
provider: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'google_workspace',
|
||||
'microsoft_365',
|
||||
'generic_imap',
|
||||
'pst_import',
|
||||
'eml_import',
|
||||
'mbox_import',
|
||||
],
|
||||
},
|
||||
providerConfig: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Provider-specific configuration. See the ingestion source guides for the required fields per provider.',
|
||||
example: {
|
||||
serviceAccountKeyJson: '{"type":"service_account",...}',
|
||||
impersonatedAdminEmail: 'admin@example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
UpdateIngestionSourceDto: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
provider: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'google_workspace',
|
||||
'microsoft_365',
|
||||
'generic_imap',
|
||||
'pst_import',
|
||||
'eml_import',
|
||||
'mbox_import',
|
||||
],
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'active',
|
||||
'paused',
|
||||
'error',
|
||||
'pending_auth',
|
||||
'syncing',
|
||||
'importing',
|
||||
'auth_success',
|
||||
'imported',
|
||||
],
|
||||
},
|
||||
providerConfig: { type: 'object' },
|
||||
},
|
||||
},
|
||||
// --- Archived Emails ---
|
||||
Recipient: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', nullable: true, example: 'John Doe' },
|
||||
email: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
example: 'john.doe@example.com',
|
||||
},
|
||||
},
|
||||
required: ['email'],
|
||||
},
|
||||
Attachment: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', example: 'clx1y2z3a0000b4d2' },
|
||||
filename: { type: 'string', example: 'invoice.pdf' },
|
||||
mimeType: { type: 'string', nullable: true, example: 'application/pdf' },
|
||||
sizeBytes: { type: 'integer', example: 204800 },
|
||||
storagePath: {
|
||||
type: 'string',
|
||||
example: 'open-archiver/attachments/abc123.pdf',
|
||||
},
|
||||
},
|
||||
required: ['id', 'filename', 'sizeBytes', 'storagePath'],
|
||||
},
|
||||
// Minimal representation of an email within a thread (returned alongside ArchivedEmail)
|
||||
ThreadEmail: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'ArchivedEmail ID.',
|
||||
example: 'clx1y2z3a0000b4d2',
|
||||
},
|
||||
subject: { type: 'string', nullable: true, example: 'Re: Q4 Invoice' },
|
||||
sentAt: { type: 'string', format: 'date-time' },
|
||||
senderEmail: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
example: 'finance@vendor.com',
|
||||
},
|
||||
},
|
||||
required: ['id', 'sentAt', 'senderEmail'],
|
||||
},
|
||||
ArchivedEmail: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', example: 'clx1y2z3a0000b4d2' },
|
||||
ingestionSourceId: { type: 'string', example: 'clx1y2z3a0000b4d2' },
|
||||
userEmail: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
example: 'user@company.com',
|
||||
},
|
||||
messageIdHeader: { type: 'string', nullable: true },
|
||||
sentAt: { type: 'string', format: 'date-time' },
|
||||
subject: { type: 'string', nullable: true, example: 'Q4 Invoice' },
|
||||
senderName: { type: 'string', nullable: true, example: 'Finance Dept' },
|
||||
senderEmail: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
example: 'finance@vendor.com',
|
||||
},
|
||||
recipients: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/Recipient' },
|
||||
},
|
||||
storagePath: { type: 'string' },
|
||||
storageHashSha256: {
|
||||
type: 'string',
|
||||
description:
|
||||
'SHA-256 hash of the raw email file, stored at archival time.',
|
||||
},
|
||||
sizeBytes: { type: 'integer' },
|
||||
isIndexed: { type: 'boolean' },
|
||||
hasAttachments: { type: 'boolean' },
|
||||
isOnLegalHold: { type: 'boolean' },
|
||||
archivedAt: { type: 'string', format: 'date-time' },
|
||||
attachments: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/Attachment' },
|
||||
},
|
||||
thread: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Other emails in the same thread, ordered by sentAt. Only present on single-email GET responses.',
|
||||
items: { $ref: '#/components/schemas/ThreadEmail' },
|
||||
},
|
||||
path: { type: 'string', nullable: true },
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'id',
|
||||
'ingestionSourceId',
|
||||
'userEmail',
|
||||
'sentAt',
|
||||
'senderEmail',
|
||||
'recipients',
|
||||
'storagePath',
|
||||
'storageHashSha256',
|
||||
'sizeBytes',
|
||||
'isIndexed',
|
||||
'hasAttachments',
|
||||
'isOnLegalHold',
|
||||
'archivedAt',
|
||||
],
|
||||
},
|
||||
PaginatedArchivedEmails: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
items: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/ArchivedEmail' },
|
||||
},
|
||||
total: { type: 'integer', example: 1234 },
|
||||
page: { type: 'integer', example: 1 },
|
||||
limit: { type: 'integer', example: 10 },
|
||||
},
|
||||
required: ['items', 'total', 'page', 'limit'],
|
||||
},
|
||||
// --- Search ---
|
||||
SearchResults: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
hits: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Array of matching archived email objects, potentially with highlighted fields.',
|
||||
items: { type: 'object' },
|
||||
},
|
||||
total: { type: 'integer', example: 42 },
|
||||
page: { type: 'integer', example: 1 },
|
||||
limit: { type: 'integer', example: 10 },
|
||||
totalPages: { type: 'integer', example: 5 },
|
||||
processingTimeMs: {
|
||||
type: 'integer',
|
||||
description: 'Meilisearch query processing time in milliseconds.',
|
||||
example: 12,
|
||||
},
|
||||
},
|
||||
required: ['hits', 'total', 'page', 'limit', 'totalPages', 'processingTimeMs'],
|
||||
},
|
||||
// --- Integrity ---
|
||||
IntegrityCheckResult: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['email', 'attachment'],
|
||||
description:
|
||||
'Whether this result is for the email itself or one of its attachments.',
|
||||
},
|
||||
id: { type: 'string', example: 'clx1y2z3a0000b4d2' },
|
||||
filename: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Attachment filename. Only present when `type` is `attachment`.',
|
||||
example: 'invoice.pdf',
|
||||
},
|
||||
isValid: {
|
||||
type: 'boolean',
|
||||
description: 'True if the stored and computed hashes match.',
|
||||
},
|
||||
reason: {
|
||||
type: 'string',
|
||||
description: 'Human-readable explanation if `isValid` is false.',
|
||||
},
|
||||
storedHash: {
|
||||
type: 'string',
|
||||
description: 'SHA-256 hash stored at archival time.',
|
||||
example: 'a3f1b2c4...',
|
||||
},
|
||||
computedHash: {
|
||||
type: 'string',
|
||||
description: 'SHA-256 hash computed during this verification run.',
|
||||
example: 'a3f1b2c4...',
|
||||
},
|
||||
},
|
||||
required: ['type', 'id', 'isValid', 'storedHash', 'computedHash'],
|
||||
},
|
||||
// --- Jobs ---
|
||||
QueueCounts: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
active: { type: 'integer', example: 0 },
|
||||
completed: { type: 'integer', example: 56 },
|
||||
failed: { type: 'integer', example: 4 },
|
||||
delayed: { type: 'integer', example: 0 },
|
||||
waiting: { type: 'integer', example: 0 },
|
||||
paused: { type: 'integer', example: 0 },
|
||||
},
|
||||
required: ['active', 'completed', 'failed', 'delayed', 'waiting', 'paused'],
|
||||
},
|
||||
QueueOverview: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', example: 'ingestion' },
|
||||
counts: { $ref: '#/components/schemas/QueueCounts' },
|
||||
},
|
||||
required: ['name', 'counts'],
|
||||
},
|
||||
Job: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', nullable: true, example: '1' },
|
||||
name: { type: 'string', example: 'initial-import' },
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Job payload data.',
|
||||
example: { ingestionSourceId: 'clx1y2z3a0000b4d2' },
|
||||
},
|
||||
state: {
|
||||
type: 'string',
|
||||
enum: ['active', 'completed', 'failed', 'delayed', 'waiting', 'paused'],
|
||||
example: 'failed',
|
||||
},
|
||||
failedReason: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
example: 'Error: Connection timed out',
|
||||
},
|
||||
timestamp: { type: 'integer', example: 1678886400000 },
|
||||
processedOn: { type: 'integer', nullable: true, example: 1678886401000 },
|
||||
finishedOn: { type: 'integer', nullable: true, example: 1678886402000 },
|
||||
attemptsMade: { type: 'integer', example: 5 },
|
||||
stacktrace: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
returnValue: { nullable: true },
|
||||
ingestionSourceId: { type: 'string', nullable: true },
|
||||
error: {
|
||||
description: 'Shorthand copy of `failedReason` for easier access.',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'id',
|
||||
'name',
|
||||
'data',
|
||||
'state',
|
||||
'timestamp',
|
||||
'attemptsMade',
|
||||
'stacktrace',
|
||||
],
|
||||
},
|
||||
QueueDetails: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', example: 'ingestion' },
|
||||
counts: { $ref: '#/components/schemas/QueueCounts' },
|
||||
jobs: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/Job' },
|
||||
},
|
||||
pagination: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
currentPage: { type: 'integer', example: 1 },
|
||||
totalPages: { type: 'integer', example: 3 },
|
||||
totalJobs: { type: 'integer', example: 25 },
|
||||
limit: { type: 'integer', example: 10 },
|
||||
},
|
||||
required: ['currentPage', 'totalPages', 'totalJobs', 'limit'],
|
||||
},
|
||||
},
|
||||
required: ['name', 'counts', 'jobs', 'pagination'],
|
||||
},
|
||||
// --- Dashboard ---
|
||||
DashboardStats: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
totalEmailsArchived: { type: 'integer', example: 125000 },
|
||||
totalStorageUsed: {
|
||||
type: 'integer',
|
||||
description: 'Total storage used by all archived emails in bytes.',
|
||||
example: 5368709120,
|
||||
},
|
||||
failedIngestionsLast7Days: {
|
||||
type: 'integer',
|
||||
description:
|
||||
'Number of ingestion sources in error state updated in the last 7 days.',
|
||||
example: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
IngestionSourceStats: {
|
||||
type: 'object',
|
||||
description: 'Summary of an ingestion source including its storage usage.',
|
||||
properties: {
|
||||
id: { type: 'string', example: 'clx1y2z3a0000b4d2' },
|
||||
name: { type: 'string', example: 'Company Google Workspace' },
|
||||
provider: { type: 'string', example: 'google_workspace' },
|
||||
status: { type: 'string', example: 'active' },
|
||||
storageUsed: {
|
||||
type: 'integer',
|
||||
description:
|
||||
'Total bytes stored for emails from this ingestion source.',
|
||||
example: 1073741824,
|
||||
},
|
||||
},
|
||||
required: ['id', 'name', 'provider', 'status', 'storageUsed'],
|
||||
},
|
||||
RecentSync: {
|
||||
type: 'object',
|
||||
description: 'Summary of a recent sync session.',
|
||||
properties: {
|
||||
id: { type: 'string', example: 'clx1y2z3a0000b4d2' },
|
||||
sourceName: { type: 'string', example: 'Company Google Workspace' },
|
||||
startTime: { type: 'string', format: 'date-time' },
|
||||
duration: {
|
||||
type: 'integer',
|
||||
description: 'Duration in milliseconds.',
|
||||
example: 4500,
|
||||
},
|
||||
emailsProcessed: { type: 'integer', example: 120 },
|
||||
status: { type: 'string', example: 'completed' },
|
||||
},
|
||||
required: [
|
||||
'id',
|
||||
'sourceName',
|
||||
'startTime',
|
||||
'duration',
|
||||
'emailsProcessed',
|
||||
'status',
|
||||
],
|
||||
},
|
||||
IndexedInsights: {
|
||||
type: 'object',
|
||||
description: 'Insights derived from the search index.',
|
||||
properties: {
|
||||
topSenders: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sender: { type: 'string', example: 'finance@vendor.com' },
|
||||
count: { type: 'integer', example: 342 },
|
||||
},
|
||||
required: ['sender', 'count'],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['topSenders'],
|
||||
},
|
||||
// --- Settings ---
|
||||
SystemSettings: {
|
||||
type: 'object',
|
||||
description: 'Non-sensitive system configuration values.',
|
||||
properties: {
|
||||
language: {
|
||||
type: 'string',
|
||||
enum: ['en', 'es', 'fr', 'de', 'it', 'pt', 'nl', 'ja', 'et', 'el'],
|
||||
example: 'en',
|
||||
description: 'Default UI language code.',
|
||||
},
|
||||
theme: {
|
||||
type: 'string',
|
||||
enum: ['light', 'dark', 'system'],
|
||||
example: 'system',
|
||||
description: 'Default color theme.',
|
||||
},
|
||||
supportEmail: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
nullable: true,
|
||||
example: 'support@example.com',
|
||||
description: 'Public-facing support email address.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Scan all route files for @openapi annotations
|
||||
apis: [resolve(__dirname, '../src/api/routes/*.ts')],
|
||||
};
|
||||
|
||||
const spec = swaggerJsdoc(options);
|
||||
|
||||
// Output to docs/ directory so VitePress can consume it
|
||||
const outputPath = resolve(__dirname, '../../../docs/api/openapi.json');
|
||||
mkdirSync(dirname(outputPath), { recursive: true });
|
||||
writeFileSync(outputPath, JSON.stringify(spec, null, 2));
|
||||
|
||||
console.log(`✅ OpenAPI spec generated: ${outputPath}`);
|
||||
console.log(` Paths: ${Object.keys(spec.paths ?? {}).length}, Tags: ${(spec.tags ?? []).length}`);
|
||||
@@ -59,17 +59,29 @@ export class ArchivedEmailController {
|
||||
};
|
||||
|
||||
public deleteArchivedEmail = async (req: Request, res: Response): Promise<Response> => {
|
||||
// Guard: return 400 if deletion is disabled in system settings before touching anything else
|
||||
try {
|
||||
checkDeletionEnabled();
|
||||
const { id } = req.params;
|
||||
const userId = req.user?.sub;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ message: req.t('errors.unauthorized') });
|
||||
}
|
||||
const actor = await this.userService.findById(userId);
|
||||
if (!actor) {
|
||||
return res.status(401).json({ message: req.t('errors.unauthorized') });
|
||||
}
|
||||
} catch (error) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({
|
||||
message:
|
||||
error instanceof Error ? error.message : req.t('errors.deletionDisabled'),
|
||||
});
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const userId = req.user?.sub;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ message: req.t('errors.unauthorized') });
|
||||
}
|
||||
const actor = await this.userService.findById(userId);
|
||||
if (!actor) {
|
||||
return res.status(401).json({ message: req.t('errors.unauthorized') });
|
||||
}
|
||||
|
||||
try {
|
||||
await ArchivedEmailService.deleteArchivedEmail(id, actor, req.ip || 'unknown');
|
||||
return res.status(204).send();
|
||||
} catch (error) {
|
||||
@@ -78,6 +90,10 @@ export class ArchivedEmailController {
|
||||
if (error.message === 'Archived email not found') {
|
||||
return res.status(404).json({ message: req.t('archivedEmail.notFound') });
|
||||
}
|
||||
// Retention policy / legal hold blocks are user-facing 400 errors
|
||||
if (error.message.startsWith('Deletion blocked by retention policy')) {
|
||||
return res.status(400).json({ message: error.message });
|
||||
}
|
||||
return res.status(500).json({ message: error.message });
|
||||
}
|
||||
return res.status(500).json({ message: req.t('errors.internalServerError') });
|
||||
|
||||
@@ -7,8 +7,127 @@ export const apiKeyRoutes = (authService: AuthService): Router => {
|
||||
const router = Router();
|
||||
const controller = new ApiKeyController();
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/api-keys:
|
||||
* post:
|
||||
* summary: Generate an API key
|
||||
* description: >
|
||||
* Generates a new API key for the authenticated user. The raw key value is only returned once at creation time.
|
||||
* The key name must be between 1–255 characters. Expiry is required and must be within 730 days (2 years).
|
||||
* Disabled in demo mode.
|
||||
* operationId: generateApiKey
|
||||
* tags:
|
||||
* - API Keys
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - name
|
||||
* - expiresInDays
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* minLength: 1
|
||||
* maxLength: 255
|
||||
* example: "CI/CD Pipeline Key"
|
||||
* expiresInDays:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 730
|
||||
* example: 90
|
||||
* responses:
|
||||
* '201':
|
||||
* description: API key created. The raw `key` value is only shown once.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* key:
|
||||
* type: string
|
||||
* description: The raw API key. Store this securely — it will not be shown again.
|
||||
* example: "oa_live_abc123..."
|
||||
* '400':
|
||||
* description: Validation error (name too short/long, expiry out of range).
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ValidationError'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* description: Disabled in demo mode.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
* get:
|
||||
* summary: List API keys
|
||||
* description: Returns all API keys belonging to the currently authenticated user. The raw key value is not included.
|
||||
* operationId: getApiKeys
|
||||
* tags:
|
||||
* - API Keys
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* responses:
|
||||
* '200':
|
||||
* description: List of API keys (without raw key values).
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/ApiKey'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
*/
|
||||
router.post('/', requireAuth(authService), controller.generateApiKey);
|
||||
router.get('/', requireAuth(authService), controller.getApiKeys);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/api-keys/{id}:
|
||||
* delete:
|
||||
* summary: Delete an API key
|
||||
* description: Permanently revokes and deletes an API key by ID. Only the owning user can delete their own keys. Disabled in demo mode.
|
||||
* operationId: deleteApiKey
|
||||
* tags:
|
||||
* - API Keys
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* description: The ID of the API key to delete.
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* responses:
|
||||
* '204':
|
||||
* description: API key deleted. No content returned.
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* description: Disabled in demo mode.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.delete('/:id', requireAuth(authService), controller.deleteApiKey);
|
||||
|
||||
return router;
|
||||
|
||||
@@ -13,12 +13,126 @@ export const createArchivedEmailRouter = (
|
||||
// Secure all routes in this module
|
||||
router.use(requireAuth(authService));
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/archived-emails/ingestion-source/{ingestionSourceId}:
|
||||
* get:
|
||||
* summary: List archived emails for an ingestion source
|
||||
* description: Returns a paginated list of archived emails belonging to the specified ingestion source. Requires `read:archive` permission.
|
||||
* operationId: getArchivedEmails
|
||||
* tags:
|
||||
* - Archived Emails
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: ingestionSourceId
|
||||
* in: path
|
||||
* required: true
|
||||
* description: The ID of the ingestion source to retrieve emails for.
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* - name: page
|
||||
* in: query
|
||||
* required: false
|
||||
* description: Page number for pagination.
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* example: 1
|
||||
* - name: limit
|
||||
* in: query
|
||||
* required: false
|
||||
* description: Number of items per page.
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* example: 10
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Paginated list of archived emails.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/PaginatedArchivedEmails'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.get(
|
||||
'/ingestion-source/:ingestionSourceId',
|
||||
requirePermission('read', 'archive'),
|
||||
archivedEmailController.getArchivedEmails
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/archived-emails/{id}:
|
||||
* get:
|
||||
* summary: Get a single archived email
|
||||
* description: Retrieves the full details of a single archived email by ID, including attachments and thread. Requires `read:archive` permission.
|
||||
* operationId: getArchivedEmailById
|
||||
* tags:
|
||||
* - Archived Emails
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* description: The ID of the archived email.
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Archived email details.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ArchivedEmail'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '404':
|
||||
* $ref: '#/components/responses/NotFound'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
* delete:
|
||||
* summary: Delete an archived email
|
||||
* description: Permanently deletes an archived email by ID. Deletion must be enabled in system settings and the email must not be on legal hold. Requires `delete:archive` permission.
|
||||
* operationId: deleteArchivedEmail
|
||||
* tags:
|
||||
* - Archived Emails
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* description: The ID of the archived email to delete.
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* responses:
|
||||
* '204':
|
||||
* description: Email deleted successfully. No content returned.
|
||||
* '400':
|
||||
* description: Deletion is disabled in system settings, or the email is blocked by a retention policy / legal hold.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '404':
|
||||
* $ref: '#/components/responses/NotFound'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.get(
|
||||
'/:id',
|
||||
requirePermission('read', 'archive'),
|
||||
|
||||
@@ -5,23 +5,141 @@ export const createAuthRouter = (authController: AuthController): Router => {
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* @route POST /api/v1/auth/setup
|
||||
* @description Creates the initial administrator user.
|
||||
* @access Public
|
||||
* @openapi
|
||||
* /v1/auth/setup:
|
||||
* post:
|
||||
* summary: Initial setup
|
||||
* description: Creates the initial administrator user. Can only be called once when no users exist.
|
||||
* operationId: authSetup
|
||||
* tags:
|
||||
* - Auth
|
||||
* security: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - email
|
||||
* - password
|
||||
* - first_name
|
||||
* - last_name
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* example: admin@example.com
|
||||
* password:
|
||||
* type: string
|
||||
* format: password
|
||||
* example: "securepassword123"
|
||||
* first_name:
|
||||
* type: string
|
||||
* example: Admin
|
||||
* last_name:
|
||||
* type: string
|
||||
* example: User
|
||||
* responses:
|
||||
* '201':
|
||||
* description: Admin user created and logged in successfully.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/LoginResponse'
|
||||
* '400':
|
||||
* description: All fields are required.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
* '403':
|
||||
* description: Setup has already been completed (users already exist).
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.post('/setup', authController.setup);
|
||||
|
||||
/**
|
||||
* @route POST /api/v1/auth/login
|
||||
* @description Authenticates a user and returns a JWT.
|
||||
* @access Public
|
||||
* @openapi
|
||||
* /v1/auth/login:
|
||||
* post:
|
||||
* summary: Login
|
||||
* description: Authenticates a user with email and password and returns a JWT access token.
|
||||
* operationId: authLogin
|
||||
* tags:
|
||||
* - Auth
|
||||
* security: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - email
|
||||
* - password
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* example: user@example.com
|
||||
* password:
|
||||
* type: string
|
||||
* format: password
|
||||
* example: "securepassword123"
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Authentication successful.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/LoginResponse'
|
||||
* '400':
|
||||
* description: Email and password are required.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
* '401':
|
||||
* description: Invalid credentials.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.post('/login', authController.login);
|
||||
|
||||
/**
|
||||
* @route GET /api/v1/auth/status
|
||||
* @description Checks if the application has been set up.
|
||||
* @access Public
|
||||
* @openapi
|
||||
* /v1/auth/status:
|
||||
* get:
|
||||
* summary: Check setup status
|
||||
* description: Returns whether the application has been set up (i.e., whether an admin user exists).
|
||||
* operationId: authStatus
|
||||
* tags:
|
||||
* - Auth
|
||||
* security: []
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Setup status returned.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* needsSetup:
|
||||
* type: boolean
|
||||
* description: True if no admin user exists and setup is required.
|
||||
* example: false
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.get('/status', authController.status);
|
||||
|
||||
|
||||
@@ -9,26 +9,168 @@ export const createDashboardRouter = (authService: AuthService): Router => {
|
||||
|
||||
router.use(requireAuth(authService));
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/dashboard/stats:
|
||||
* get:
|
||||
* summary: Get dashboard stats
|
||||
* description: Returns high-level statistics including total archived emails, total storage used, and failed ingestions in the last 7 days. Requires `read:dashboard` permission.
|
||||
* operationId: getDashboardStats
|
||||
* tags:
|
||||
* - Dashboard
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Dashboard statistics.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/DashboardStats'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/Forbidden'
|
||||
*/
|
||||
router.get(
|
||||
'/stats',
|
||||
requirePermission('read', 'dashboard', 'dashboard.permissionRequired'),
|
||||
dashboardController.getStats
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/dashboard/ingestion-history:
|
||||
* get:
|
||||
* summary: Get ingestion history
|
||||
* description: Returns time-series data of email ingestion counts for the last 30 days. Requires `read:dashboard` permission.
|
||||
* operationId: getIngestionHistory
|
||||
* tags:
|
||||
* - Dashboard
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Ingestion history wrapped in a `history` array.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* history:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* date:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: Truncated to day precision (UTC).
|
||||
* count:
|
||||
* type: integer
|
||||
* required:
|
||||
* - history
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/Forbidden'
|
||||
*/
|
||||
router.get(
|
||||
'/ingestion-history',
|
||||
requirePermission('read', 'dashboard', 'dashboard.permissionRequired'),
|
||||
dashboardController.getIngestionHistory
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/dashboard/ingestion-sources:
|
||||
* get:
|
||||
* summary: Get ingestion source summaries
|
||||
* description: Returns a summary list of ingestion sources with their storage usage. Requires `read:dashboard` permission.
|
||||
* operationId: getDashboardIngestionSources
|
||||
* tags:
|
||||
* - Dashboard
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* responses:
|
||||
* '200':
|
||||
* description: List of ingestion source summaries.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/IngestionSourceStats'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/Forbidden'
|
||||
*/
|
||||
router.get(
|
||||
'/ingestion-sources',
|
||||
requirePermission('read', 'dashboard', 'dashboard.permissionRequired'),
|
||||
dashboardController.getIngestionSources
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/dashboard/recent-syncs:
|
||||
* get:
|
||||
* summary: Get recent sync activity
|
||||
* description: Returns the most recent sync sessions across all ingestion sources. Requires `read:dashboard` permission.
|
||||
* operationId: getRecentSyncs
|
||||
* tags:
|
||||
* - Dashboard
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* responses:
|
||||
* '200':
|
||||
* description: List of recent sync sessions.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/RecentSync'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/Forbidden'
|
||||
*/
|
||||
router.get(
|
||||
'/recent-syncs',
|
||||
requirePermission('read', 'dashboard', 'dashboard.permissionRequired'),
|
||||
dashboardController.getRecentSyncs
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/dashboard/indexed-insights:
|
||||
* get:
|
||||
* summary: Get indexed email insights
|
||||
* description: Returns top-sender statistics from the search index. Requires `read:dashboard` permission.
|
||||
* operationId: getIndexedInsights
|
||||
* tags:
|
||||
* - Dashboard
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Indexed email insights.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/IndexedInsights'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/Forbidden'
|
||||
*/
|
||||
router.get(
|
||||
'/indexed-insights',
|
||||
requirePermission('read', 'dashboard', 'dashboard.permissionRequired'),
|
||||
|
||||
@@ -10,16 +10,116 @@ export const createIamRouter = (iamController: IamController, authService: AuthS
|
||||
router.use(requireAuth(authService));
|
||||
|
||||
/**
|
||||
* @route GET /api/v1/iam/roles
|
||||
* @description Gets all roles.
|
||||
* @access Private
|
||||
* @openapi
|
||||
* /v1/iam/roles:
|
||||
* get:
|
||||
* summary: List all roles
|
||||
* description: Returns all IAM roles. If predefined roles do not yet exist, they are created automatically. Requires `read:roles` permission.
|
||||
* operationId: getRoles
|
||||
* tags:
|
||||
* - IAM
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* responses:
|
||||
* '200':
|
||||
* description: List of roles.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Role'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.get('/roles', requirePermission('read', 'roles'), iamController.getRoles);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/iam/roles/{id}:
|
||||
* get:
|
||||
* summary: Get a role
|
||||
* description: Returns a single IAM role by ID. Requires `read:roles` permission.
|
||||
* operationId: getRoleById
|
||||
* tags:
|
||||
* - IAM
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Role details.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Role'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '404':
|
||||
* $ref: '#/components/responses/NotFound'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.get('/roles/:id', requirePermission('read', 'roles'), iamController.getRoleById);
|
||||
|
||||
/**
|
||||
* Only super admin has the ability to modify existing roles or create new roles.
|
||||
* @openapi
|
||||
* /v1/iam/roles:
|
||||
* post:
|
||||
* summary: Create a role
|
||||
* description: Creates a new IAM role with the given name and CASL policies. Requires `manage:all` (Super Admin) permission.
|
||||
* operationId: createRole
|
||||
* tags:
|
||||
* - IAM
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - name
|
||||
* - policies
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* example: "Compliance Officer"
|
||||
* policies:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/CaslPolicy'
|
||||
* responses:
|
||||
* '201':
|
||||
* description: Role created.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Role'
|
||||
* '400':
|
||||
* description: Missing fields or invalid policy.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/Forbidden'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.post(
|
||||
'/roles',
|
||||
@@ -27,12 +127,94 @@ export const createIamRouter = (iamController: IamController, authService: AuthS
|
||||
iamController.createRole
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/iam/roles/{id}:
|
||||
* delete:
|
||||
* summary: Delete a role
|
||||
* description: Permanently deletes an IAM role. Requires `manage:all` (Super Admin) permission.
|
||||
* operationId: deleteRole
|
||||
* tags:
|
||||
* - IAM
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* responses:
|
||||
* '204':
|
||||
* description: Role deleted. No content returned.
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/Forbidden'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.delete(
|
||||
'/roles/:id',
|
||||
requirePermission('manage', 'all', 'iam.requiresSuperAdminRole'),
|
||||
iamController.deleteRole
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/iam/roles/{id}:
|
||||
* put:
|
||||
* summary: Update a role
|
||||
* description: Updates the name or policies of an IAM role. Requires `manage:all` (Super Admin) permission.
|
||||
* operationId: updateRole
|
||||
* tags:
|
||||
* - IAM
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* example: "Senior Compliance Officer"
|
||||
* policies:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/CaslPolicy'
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Updated role.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Role'
|
||||
* '400':
|
||||
* description: No update fields provided or invalid policy.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/Forbidden'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.put(
|
||||
'/roles/:id',
|
||||
requirePermission('manage', 'all', 'iam.requiresSuperAdminRole'),
|
||||
|
||||
@@ -13,24 +13,278 @@ export const createIngestionRouter = (
|
||||
// Secure all routes in this module
|
||||
router.use(requireAuth(authService));
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/ingestion-sources:
|
||||
* post:
|
||||
* summary: Create an ingestion source
|
||||
* description: Creates a new ingestion source and validates the connection. Returns the created source without credentials. Requires `create:ingestion` permission.
|
||||
* operationId: createIngestionSource
|
||||
* tags:
|
||||
* - Ingestion
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/CreateIngestionSourceDto'
|
||||
* responses:
|
||||
* '201':
|
||||
* description: Ingestion source created successfully.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/SafeIngestionSource'
|
||||
* '400':
|
||||
* description: Invalid input or connection test failed.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* get:
|
||||
* summary: List ingestion sources
|
||||
* description: Returns all ingestion sources accessible to the authenticated user. Credentials are excluded from the response. Requires `read:ingestion` permission.
|
||||
* operationId: listIngestionSources
|
||||
* tags:
|
||||
* - Ingestion
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Array of ingestion sources.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/SafeIngestionSource'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.post('/', requirePermission('create', 'ingestion'), ingestionController.create);
|
||||
|
||||
router.get('/', requirePermission('read', 'ingestion'), ingestionController.findAll);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/ingestion-sources/{id}:
|
||||
* get:
|
||||
* summary: Get an ingestion source
|
||||
* description: Returns a single ingestion source by ID. Credentials are excluded. Requires `read:ingestion` permission.
|
||||
* operationId: getIngestionSourceById
|
||||
* tags:
|
||||
* - Ingestion
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Ingestion source details.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/SafeIngestionSource'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '404':
|
||||
* $ref: '#/components/responses/NotFound'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
* put:
|
||||
* summary: Update an ingestion source
|
||||
* description: Updates configuration for an existing ingestion source. Requires `update:ingestion` permission.
|
||||
* operationId: updateIngestionSource
|
||||
* tags:
|
||||
* - Ingestion
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/UpdateIngestionSourceDto'
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Updated ingestion source.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/SafeIngestionSource'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '404':
|
||||
* $ref: '#/components/responses/NotFound'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
* delete:
|
||||
* summary: Delete an ingestion source
|
||||
* description: Permanently deletes an ingestion source. Deletion must be enabled in system settings. Requires `delete:ingestion` permission.
|
||||
* operationId: deleteIngestionSource
|
||||
* tags:
|
||||
* - Ingestion
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* responses:
|
||||
* '204':
|
||||
* description: Ingestion source deleted. No content returned.
|
||||
* '400':
|
||||
* description: Deletion disabled or constraint error.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '404':
|
||||
* $ref: '#/components/responses/NotFound'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.get('/:id', requirePermission('read', 'ingestion'), ingestionController.findById);
|
||||
|
||||
router.put('/:id', requirePermission('update', 'ingestion'), ingestionController.update);
|
||||
|
||||
router.delete('/:id', requirePermission('delete', 'ingestion'), ingestionController.delete);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/ingestion-sources/{id}/import:
|
||||
* post:
|
||||
* summary: Trigger initial import
|
||||
* description: Enqueues an initial import job for the ingestion source. This imports all historical emails. Requires `create:ingestion` permission.
|
||||
* operationId: triggerInitialImport
|
||||
* tags:
|
||||
* - Ingestion
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* responses:
|
||||
* '202':
|
||||
* description: Initial import job accepted and queued.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/MessageResponse'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '404':
|
||||
* $ref: '#/components/responses/NotFound'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.post(
|
||||
'/:id/import',
|
||||
requirePermission('create', 'ingestion'),
|
||||
ingestionController.triggerInitialImport
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/ingestion-sources/{id}/pause:
|
||||
* post:
|
||||
* summary: Pause an ingestion source
|
||||
* description: Sets the ingestion source status to `paused`, stopping continuous sync. Requires `update:ingestion` permission.
|
||||
* operationId: pauseIngestionSource
|
||||
* tags:
|
||||
* - Ingestion
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Ingestion source paused. Returns the updated source.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/SafeIngestionSource'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '404':
|
||||
* $ref: '#/components/responses/NotFound'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.post('/:id/pause', requirePermission('update', 'ingestion'), ingestionController.pause);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/ingestion-sources/{id}/sync:
|
||||
* post:
|
||||
* summary: Force sync
|
||||
* description: Triggers an out-of-schedule continuous sync for the ingestion source. Requires `sync:ingestion` permission.
|
||||
* operationId: triggerForceSync
|
||||
* tags:
|
||||
* - Ingestion
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* responses:
|
||||
* '202':
|
||||
* description: Force sync job accepted and queued.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/MessageResponse'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '404':
|
||||
* $ref: '#/components/responses/NotFound'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.post(
|
||||
'/:id/sync',
|
||||
requirePermission('sync', 'ingestion'),
|
||||
|
||||
@@ -10,6 +10,49 @@ export const integrityRoutes = (authService: AuthService): Router => {
|
||||
|
||||
router.use(requireAuth(authService));
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/integrity/{id}:
|
||||
* get:
|
||||
* summary: Check email integrity
|
||||
* description: Verifies the SHA-256 hash of an archived email and all its attachments against the hashes stored at archival time. Returns per-item integrity results. Requires `read:archive` permission.
|
||||
* operationId: checkIntegrity
|
||||
* tags:
|
||||
* - Integrity
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* description: UUID of the archived email to verify.
|
||||
* schema:
|
||||
* type: string
|
||||
* format: uuid
|
||||
* example: "550e8400-e29b-41d4-a716-446655440000"
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Integrity check results for the email and its attachments.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/IntegrityCheckResult'
|
||||
* '400':
|
||||
* description: Invalid UUID format.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ValidationError'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '404':
|
||||
* $ref: '#/components/responses/NotFound'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.get('/:id', requirePermission('read', 'archive'), controller.checkIntegrity);
|
||||
|
||||
return router;
|
||||
|
||||
@@ -10,11 +10,121 @@ export const createJobsRouter = (authService: AuthService): Router => {
|
||||
|
||||
router.use(requireAuth(authService));
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/jobs/queues:
|
||||
* get:
|
||||
* summary: List all queues
|
||||
* description: Returns all BullMQ job queues and their current job counts broken down by status. Requires `manage:all` (Super Admin) permission.
|
||||
* operationId: getQueues
|
||||
* tags:
|
||||
* - Jobs
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* responses:
|
||||
* '200':
|
||||
* description: List of queue overviews.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* queues:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/QueueOverview'
|
||||
* example:
|
||||
* queues:
|
||||
* - name: ingestion
|
||||
* counts:
|
||||
* active: 0
|
||||
* completed: 56
|
||||
* failed: 4
|
||||
* delayed: 3
|
||||
* waiting: 0
|
||||
* paused: 0
|
||||
* - name: indexing
|
||||
* counts:
|
||||
* active: 0
|
||||
* completed: 0
|
||||
* failed: 0
|
||||
* delayed: 0
|
||||
* waiting: 0
|
||||
* paused: 0
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/Forbidden'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.get(
|
||||
'/queues',
|
||||
requirePermission('manage', 'all', 'user.requiresSuperAdminRole'),
|
||||
jobsController.getQueues
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/jobs/queues/{queueName}:
|
||||
* get:
|
||||
* summary: Get jobs in a queue
|
||||
* description: Returns a paginated list of jobs within a specific queue, filtered by status. Requires `manage:all` (Super Admin) permission.
|
||||
* operationId: getQueueJobs
|
||||
* tags:
|
||||
* - Jobs
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: queueName
|
||||
* in: path
|
||||
* required: true
|
||||
* description: The name of the queue (e.g. `ingestion` or `indexing`).
|
||||
* schema:
|
||||
* type: string
|
||||
* example: ingestion
|
||||
* - name: status
|
||||
* in: query
|
||||
* required: false
|
||||
* description: Filter jobs by status.
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [active, completed, failed, delayed, waiting, paused]
|
||||
* default: failed
|
||||
* - name: page
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* - name: limit
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Detailed view of the queue including paginated jobs.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/QueueDetails'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/Forbidden'
|
||||
* '404':
|
||||
* description: Queue not found.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.get(
|
||||
'/queues/:queueName',
|
||||
requirePermission('manage', 'all', 'user.requiresSuperAdminRole'),
|
||||
|
||||
@@ -12,6 +12,68 @@ export const createSearchRouter = (
|
||||
|
||||
router.use(requireAuth(authService));
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/search:
|
||||
* get:
|
||||
* summary: Search archived emails
|
||||
* description: Performs a full-text search across indexed archived emails using Meilisearch. Requires `search:archive` permission.
|
||||
* operationId: searchEmails
|
||||
* tags:
|
||||
* - Search
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: keywords
|
||||
* in: query
|
||||
* required: true
|
||||
* description: The search query string.
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "invoice Q4"
|
||||
* - name: page
|
||||
* in: query
|
||||
* required: false
|
||||
* description: Page number for pagination.
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* example: 1
|
||||
* - name: limit
|
||||
* in: query
|
||||
* required: false
|
||||
* description: Number of results per page.
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* example: 10
|
||||
* - name: matchingStrategy
|
||||
* in: query
|
||||
* required: false
|
||||
* description: Meilisearch matching strategy. `last` returns results containing at least one keyword; `all` requires all keywords; `frequency` sorts by keyword frequency.
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [last, all, frequency]
|
||||
* default: last
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Search results.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/SearchResults'
|
||||
* '400':
|
||||
* description: Keywords parameter is required.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.get('/', requirePermission('search', 'archive'), searchController.search);
|
||||
|
||||
return router;
|
||||
|
||||
@@ -7,10 +7,56 @@ import { AuthService } from '../../services/AuthService';
|
||||
export const createSettingsRouter = (authService: AuthService): Router => {
|
||||
const router = Router();
|
||||
|
||||
// Public route to get non-sensitive settings. settings read should not be scoped with a permission because all end users need the settings data in the frontend. However, for sensitive settings data, we need to add a new permission subject to limit access. So this route should only expose non-sensitive settings data.
|
||||
/**
|
||||
* @returns SystemSettings
|
||||
* @openapi
|
||||
* /v1/settings/system:
|
||||
* get:
|
||||
* summary: Get system settings
|
||||
* description: >
|
||||
* Returns non-sensitive system settings such as language, timezone, and feature flags.
|
||||
* This endpoint is public — no authentication required. Sensitive settings are never exposed.
|
||||
* operationId: getSystemSettings
|
||||
* tags:
|
||||
* - Settings
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Current system settings.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/SystemSettings'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
* put:
|
||||
* summary: Update system settings
|
||||
* description: Updates system settings. Requires `manage:settings` permission.
|
||||
* operationId: updateSystemSettings
|
||||
* tags:
|
||||
* - Settings
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/SystemSettings'
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Updated system settings.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/SystemSettings'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/Forbidden'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
// Public route to get non-sensitive settings. All end users need the settings data in the frontend.
|
||||
router.get('/system', settingsController.getSystemSettings);
|
||||
|
||||
// Protected route to update settings
|
||||
|
||||
@@ -13,6 +13,60 @@ export const createStorageRouter = (
|
||||
// Secure all routes in this module
|
||||
router.use(requireAuth(authService));
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/storage/download:
|
||||
* get:
|
||||
* summary: Download a stored file
|
||||
* description: >
|
||||
* Downloads a file from the configured storage backend (local filesystem or S3-compatible).
|
||||
* The path is sanitized to prevent directory traversal attacks.
|
||||
* Requires `read:archive` permission.
|
||||
* operationId: downloadFile
|
||||
* tags:
|
||||
* - Storage
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: path
|
||||
* in: query
|
||||
* required: true
|
||||
* description: The relative storage path of the file to download.
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "open-archiver/emails/abc123.eml"
|
||||
* responses:
|
||||
* '200':
|
||||
* description: The file content as a binary stream. The `Content-Disposition` header is set to trigger a browser download.
|
||||
* headers:
|
||||
* Content-Disposition:
|
||||
* description: Attachment filename.
|
||||
* schema:
|
||||
* type: string
|
||||
* example: 'attachment; filename="abc123.eml"'
|
||||
* content:
|
||||
* application/octet-stream:
|
||||
* schema:
|
||||
* type: string
|
||||
* format: binary
|
||||
* '400':
|
||||
* description: File path is required or invalid.
|
||||
* content:
|
||||
* text/plain:
|
||||
* schema:
|
||||
* type: string
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '404':
|
||||
* description: File not found in storage.
|
||||
* content:
|
||||
* text/plain:
|
||||
* schema:
|
||||
* type: string
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.get('/download', requirePermission('read', 'archive'), storageController.downloadFile);
|
||||
|
||||
return router;
|
||||
|
||||
@@ -9,6 +9,55 @@ export const createUploadRouter = (authService: AuthService): Router => {
|
||||
|
||||
router.use(requireAuth(authService));
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/upload:
|
||||
* post:
|
||||
* summary: Upload a file
|
||||
* description: >
|
||||
* Uploads a file (PST, EML, MBOX, or other) to temporary storage for subsequent use in an ingestion source.
|
||||
* Returns the storage path, which should be passed as `uploadedFilePath` when creating a file-based ingestion source.
|
||||
* Requires `create:ingestion` permission.
|
||||
* operationId: uploadFile
|
||||
* tags:
|
||||
* - Upload
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* multipart/form-data:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* file:
|
||||
* type: string
|
||||
* format: binary
|
||||
* description: The file to upload.
|
||||
* responses:
|
||||
* '200':
|
||||
* description: File uploaded successfully. Returns the storage path.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* filePath:
|
||||
* type: string
|
||||
* description: The storage path of the uploaded file. Use this as `uploadedFilePath` when creating a file-based ingestion source.
|
||||
* example: "open-archiver/tmp/uuid-filename.pst"
|
||||
* '400':
|
||||
* description: Invalid multipart request.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '500':
|
||||
* $ref: '#/components/responses/InternalServerError'
|
||||
*/
|
||||
router.post('/', requirePermission('create', 'ingestion'), uploadFile);
|
||||
|
||||
return router;
|
||||
|
||||
@@ -9,16 +9,235 @@ export const createUserRouter = (authService: AuthService): Router => {
|
||||
|
||||
router.use(requireAuth(authService));
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/users:
|
||||
* get:
|
||||
* summary: List all users
|
||||
* description: Returns all user accounts in the system. Requires `read:users` permission.
|
||||
* operationId: getUsers
|
||||
* tags:
|
||||
* - Users
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* responses:
|
||||
* '200':
|
||||
* description: List of users.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/User'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
*/
|
||||
router.get('/', requirePermission('read', 'users'), userController.getUsers);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/users/profile:
|
||||
* get:
|
||||
* summary: Get current user profile
|
||||
* description: Returns the profile of the currently authenticated user.
|
||||
* operationId: getProfile
|
||||
* tags:
|
||||
* - Users
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Current user's profile.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/User'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '404':
|
||||
* $ref: '#/components/responses/NotFound'
|
||||
* patch:
|
||||
* summary: Update current user profile
|
||||
* description: Updates the email, first name, or last name of the currently authenticated user. Disabled in demo mode.
|
||||
* operationId: updateProfile
|
||||
* tags:
|
||||
* - Users
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* first_name:
|
||||
* type: string
|
||||
* last_name:
|
||||
* type: string
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Updated user profile.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/User'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* description: Disabled in demo mode.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
*/
|
||||
router.get('/profile', userController.getProfile);
|
||||
router.patch('/profile', userController.updateProfile);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/users/profile/password:
|
||||
* post:
|
||||
* summary: Update password
|
||||
* description: Updates the password of the currently authenticated user. The current password must be provided for verification. Disabled in demo mode.
|
||||
* operationId: updatePassword
|
||||
* tags:
|
||||
* - Users
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - currentPassword
|
||||
* - newPassword
|
||||
* properties:
|
||||
* currentPassword:
|
||||
* type: string
|
||||
* format: password
|
||||
* newPassword:
|
||||
* type: string
|
||||
* format: password
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Password updated successfully.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/MessageResponse'
|
||||
* '400':
|
||||
* description: Current password is incorrect.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* description: Disabled in demo mode.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
*/
|
||||
router.post('/profile/password', userController.updatePassword);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/users/{id}:
|
||||
* get:
|
||||
* summary: Get a user
|
||||
* description: Returns a single user by ID. Requires `read:users` permission.
|
||||
* operationId: getUser
|
||||
* tags:
|
||||
* - Users
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* responses:
|
||||
* '200':
|
||||
* description: User details.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/User'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '404':
|
||||
* $ref: '#/components/responses/NotFound'
|
||||
*/
|
||||
router.get('/:id', requirePermission('read', 'users'), userController.getUser);
|
||||
|
||||
/**
|
||||
* Only super admin has the ability to modify existing users or create new users.
|
||||
* @openapi
|
||||
* /v1/users:
|
||||
* post:
|
||||
* summary: Create a user
|
||||
* description: Creates a new user account and optionally assigns a role. Requires `manage:all` (Super Admin) permission.
|
||||
* operationId: createUser
|
||||
* tags:
|
||||
* - Users
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - email
|
||||
* - first_name
|
||||
* - last_name
|
||||
* - password
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* example: jane.doe@example.com
|
||||
* first_name:
|
||||
* type: string
|
||||
* example: Jane
|
||||
* last_name:
|
||||
* type: string
|
||||
* example: Doe
|
||||
* password:
|
||||
* type: string
|
||||
* format: password
|
||||
* example: "securepassword123"
|
||||
* roleId:
|
||||
* type: string
|
||||
* description: Optional role ID to assign to the user.
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* responses:
|
||||
* '201':
|
||||
* description: User created.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/User'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/Forbidden'
|
||||
*/
|
||||
router.post(
|
||||
'/',
|
||||
@@ -26,12 +245,94 @@ export const createUserRouter = (authService: AuthService): Router => {
|
||||
userController.createUser
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/users/{id}:
|
||||
* put:
|
||||
* summary: Update a user
|
||||
* description: Updates a user's email, name, or role assignment. Requires `manage:all` (Super Admin) permission.
|
||||
* operationId: updateUser
|
||||
* tags:
|
||||
* - Users
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* first_name:
|
||||
* type: string
|
||||
* last_name:
|
||||
* type: string
|
||||
* roleId:
|
||||
* type: string
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Updated user.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/User'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/Forbidden'
|
||||
* '404':
|
||||
* $ref: '#/components/responses/NotFound'
|
||||
*/
|
||||
router.put(
|
||||
'/:id',
|
||||
requirePermission('manage', 'all', 'user.requiresSuperAdminRole'),
|
||||
userController.updateUser
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/users/{id}:
|
||||
* delete:
|
||||
* summary: Delete a user
|
||||
* description: Permanently deletes a user. Cannot delete the last remaining user. Requires `manage:all` (Super Admin) permission.
|
||||
* operationId: deleteUser
|
||||
* tags:
|
||||
* - Users
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* - apiKeyAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "clx1y2z3a0000b4d2"
|
||||
* responses:
|
||||
* '204':
|
||||
* description: User deleted. No content returned.
|
||||
* '400':
|
||||
* description: Cannot delete the only remaining user.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorMessage'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/Forbidden'
|
||||
*/
|
||||
router.delete(
|
||||
'/:id',
|
||||
requirePermission('manage', 'all', 'user.requiresSuperAdminRole'),
|
||||
|
||||
360
pnpm-lock.yaml
generated
360
pnpm-lock.yaml
generated
@@ -33,6 +33,9 @@ importers:
|
||||
vitepress:
|
||||
specifier: ^1.6.4
|
||||
version: 1.6.4(@algolia/client-search@5.34.1)(@types/node@24.0.13)(axios@1.10.0)(lightningcss@1.30.1)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.8.3)
|
||||
vitepress-openapi:
|
||||
specifier: ^0.1.18
|
||||
version: 0.1.18(vitepress@1.6.4(@algolia/client-search@5.34.1)(@types/node@24.0.13)(axios@1.10.0)(lightningcss@1.30.1)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.8.3))(vue@3.5.18(typescript@5.8.3))
|
||||
|
||||
apps/open-archiver:
|
||||
dependencies:
|
||||
@@ -231,9 +234,15 @@ importers:
|
||||
'@types/nodemailer':
|
||||
specifier: ^7.0.11
|
||||
version: 7.0.11
|
||||
'@types/swagger-jsdoc':
|
||||
specifier: ^6.0.4
|
||||
version: 6.0.4
|
||||
'@types/yauzl':
|
||||
specifier: ^2.10.3
|
||||
version: 2.10.3
|
||||
swagger-jsdoc:
|
||||
specifier: ^6.2.8
|
||||
version: 6.2.8(openapi-types@12.1.3)
|
||||
ts-node-dev:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(@types/node@24.0.13)(typescript@5.8.3)
|
||||
@@ -475,6 +484,21 @@ packages:
|
||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@apidevtools/json-schema-ref-parser@9.1.2':
|
||||
resolution: {integrity: sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==}
|
||||
|
||||
'@apidevtools/openapi-schemas@2.1.0':
|
||||
resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@apidevtools/swagger-methods@3.0.2':
|
||||
resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==}
|
||||
|
||||
'@apidevtools/swagger-parser@10.0.3':
|
||||
resolution: {integrity: sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==}
|
||||
peerDependencies:
|
||||
openapi-types: '>=7'
|
||||
|
||||
'@aws-crypto/crc32@5.2.0':
|
||||
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@@ -1141,12 +1165,24 @@ packages:
|
||||
'@floating-ui/core@1.7.2':
|
||||
resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==}
|
||||
|
||||
'@floating-ui/core@1.7.5':
|
||||
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
|
||||
|
||||
'@floating-ui/dom@1.7.2':
|
||||
resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==}
|
||||
|
||||
'@floating-ui/dom@1.7.6':
|
||||
resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
|
||||
|
||||
'@floating-ui/utils@0.2.10':
|
||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||
|
||||
'@floating-ui/utils@0.2.11':
|
||||
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
|
||||
|
||||
'@floating-ui/vue@1.1.11':
|
||||
resolution: {integrity: sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==}
|
||||
|
||||
'@gar/promisify@1.1.3':
|
||||
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
|
||||
|
||||
@@ -1164,6 +1200,9 @@ packages:
|
||||
'@internationalized/date@3.8.2':
|
||||
resolution: {integrity: sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==}
|
||||
|
||||
'@internationalized/number@3.6.5':
|
||||
resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==}
|
||||
|
||||
'@ioredis/commands@1.2.0':
|
||||
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
|
||||
|
||||
@@ -1191,6 +1230,9 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
|
||||
'@jsdevtools/ono@7.1.3':
|
||||
resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
|
||||
|
||||
'@layerstack/svelte-actions@1.0.1-next.12':
|
||||
resolution: {integrity: sha512-dndWTlYu8b1u6vw2nrO7NssccoACArGG75WoNlyVC13KuENZlWdKE9Q79/wlnbq00NeQMNKMjJwRMsrKQj2ULA==}
|
||||
|
||||
@@ -1794,6 +1836,14 @@ packages:
|
||||
peerDependencies:
|
||||
vite: ^5.2.0 || ^6 || ^7
|
||||
|
||||
'@tanstack/virtual-core@3.13.23':
|
||||
resolution: {integrity: sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==}
|
||||
|
||||
'@tanstack/vue-virtual@3.13.23':
|
||||
resolution: {integrity: sha512-b5jPluAR6U3eOq6GWAYSpj3ugnAIZgGR0e6aGAgyRse0Yu6MVQQ0ZWm9SArSXWtageogn6bkVD8D//c4IjW3xQ==}
|
||||
peerDependencies:
|
||||
vue: ^2.7.0 || ^3.0.0
|
||||
|
||||
'@tootallnate/once@1.1.2':
|
||||
resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -1853,6 +1903,9 @@ packages:
|
||||
'@types/http-errors@2.0.5':
|
||||
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
|
||||
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
|
||||
|
||||
@@ -1919,6 +1972,9 @@ packages:
|
||||
'@types/strip-json-comments@0.0.30':
|
||||
resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==}
|
||||
|
||||
'@types/swagger-jsdoc@6.0.4':
|
||||
resolution: {integrity: sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==}
|
||||
|
||||
'@types/unist@3.0.3':
|
||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||
|
||||
@@ -1994,6 +2050,11 @@ packages:
|
||||
'@vueuse/core@12.8.2':
|
||||
resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==}
|
||||
|
||||
'@vueuse/core@14.2.1':
|
||||
resolution: {integrity: sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==}
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
'@vueuse/integrations@12.8.2':
|
||||
resolution: {integrity: sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==}
|
||||
peerDependencies:
|
||||
@@ -2038,9 +2099,17 @@ packages:
|
||||
'@vueuse/metadata@12.8.2':
|
||||
resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==}
|
||||
|
||||
'@vueuse/metadata@14.2.1':
|
||||
resolution: {integrity: sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==}
|
||||
|
||||
'@vueuse/shared@12.8.2':
|
||||
resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==}
|
||||
|
||||
'@vueuse/shared@14.2.1':
|
||||
resolution: {integrity: sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==}
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
'@xmldom/xmldom@0.8.10':
|
||||
resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -2130,6 +2199,13 @@ packages:
|
||||
argparse@1.0.10:
|
||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
aria-query@5.3.2:
|
||||
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2255,6 +2331,9 @@ packages:
|
||||
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
call-me-maybe@1.0.2:
|
||||
resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==}
|
||||
|
||||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
|
||||
@@ -2287,6 +2366,9 @@ packages:
|
||||
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
clean-stack@2.2.0:
|
||||
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2324,10 +2406,18 @@ packages:
|
||||
comma-separated-tokens@2.0.3:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
|
||||
commander@6.2.0:
|
||||
resolution: {integrity: sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
commander@7.2.0:
|
||||
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
commander@9.5.0:
|
||||
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
|
||||
engines: {node: ^12.20.0 || >=14}
|
||||
|
||||
commondir@1.0.1:
|
||||
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
|
||||
|
||||
@@ -2550,6 +2640,9 @@ packages:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
defu@6.1.4:
|
||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||
|
||||
delaunator@5.0.1:
|
||||
resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
|
||||
|
||||
@@ -2589,6 +2682,10 @@ packages:
|
||||
dingbat-to-unicode@1.0.1:
|
||||
resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==}
|
||||
|
||||
doctrine@3.0.0:
|
||||
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||
|
||||
@@ -2823,6 +2920,10 @@ packages:
|
||||
estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
|
||||
esutils@2.0.3:
|
||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
etag@1.8.1:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -2986,6 +3087,10 @@ packages:
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
hasBin: true
|
||||
|
||||
glob@7.1.6:
|
||||
resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
@@ -3228,6 +3333,10 @@ packages:
|
||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
hasBin: true
|
||||
|
||||
jsbn@1.1.0:
|
||||
resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==}
|
||||
|
||||
@@ -3362,6 +3471,10 @@ packages:
|
||||
lodash.defaults@4.2.0:
|
||||
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
|
||||
|
||||
lodash.get@4.4.2:
|
||||
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
|
||||
deprecated: This package is deprecated. Use the optional chaining (?.) operator instead.
|
||||
|
||||
lodash.includes@4.3.0:
|
||||
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||
|
||||
@@ -3371,6 +3484,10 @@ packages:
|
||||
lodash.isboolean@3.0.3:
|
||||
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||
|
||||
lodash.isequal@4.5.0:
|
||||
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
|
||||
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
|
||||
|
||||
lodash.isinteger@4.0.4:
|
||||
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
||||
|
||||
@@ -3383,6 +3500,9 @@ packages:
|
||||
lodash.isstring@4.0.1:
|
||||
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
||||
|
||||
lodash.mergewith@4.6.2:
|
||||
resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==}
|
||||
|
||||
lodash.once@4.1.1:
|
||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||
|
||||
@@ -3407,6 +3527,11 @@ packages:
|
||||
peerDependencies:
|
||||
svelte: ^3 || ^4 || ^5.0.0-next.42
|
||||
|
||||
lucide-vue-next@0.577.0:
|
||||
resolution: {integrity: sha512-py05bAfv9SHVJqscbiOnjcnLlEmOffA58a+7XhZuFxrs6txe1E8VoR1ngWGTYO+9aVKABAz8l3ee3PqiQN9QPA==}
|
||||
peerDependencies:
|
||||
vue: '>=3.0.1'
|
||||
|
||||
luxon@3.7.1:
|
||||
resolution: {integrity: sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -3440,6 +3565,9 @@ packages:
|
||||
mark.js@8.11.1:
|
||||
resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
|
||||
|
||||
markdown-it-link-attributes@4.0.1:
|
||||
resolution: {integrity: sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3700,6 +3828,9 @@ packages:
|
||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
ohash@2.0.11:
|
||||
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
|
||||
|
||||
on-exit-leak-free@2.1.2:
|
||||
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -3714,6 +3845,9 @@ packages:
|
||||
oniguruma-to-es@3.1.1:
|
||||
resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==}
|
||||
|
||||
openapi-types@12.1.3:
|
||||
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
||||
|
||||
option@0.2.4:
|
||||
resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==}
|
||||
|
||||
@@ -4042,6 +4176,11 @@ packages:
|
||||
regex@6.0.1:
|
||||
resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==}
|
||||
|
||||
reka-ui@2.9.2:
|
||||
resolution: {integrity: sha512-/t4e6y1hcG+uDuRfpg6tbMz3uUEvRzNco6NeYTufoJeUghy5Iosxos5YL/p+ieAsid84sdMX9OrgDqpEuCJhBw==}
|
||||
peerDependencies:
|
||||
vue: '>= 3.4.0'
|
||||
|
||||
require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -4382,6 +4521,15 @@ packages:
|
||||
peerDependencies:
|
||||
svelte: '>=3.49.0'
|
||||
|
||||
swagger-jsdoc@6.2.8:
|
||||
resolution: {integrity: sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
hasBin: true
|
||||
|
||||
swagger-parser@10.0.3:
|
||||
resolution: {integrity: sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
tabbable@6.2.0:
|
||||
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
||||
|
||||
@@ -4662,6 +4810,13 @@ packages:
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
vitepress-openapi@0.1.18:
|
||||
resolution: {integrity: sha512-2vm3vWywPJ3xThvPxuUaUFIfHz3lCLXT3bV6vh71wny+XB3IkJUfyJxPDDEjdCHVPIoNSVyPDR74iVszacF24w==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
|
||||
peerDependencies:
|
||||
vitepress: '>=1.0.0'
|
||||
vue: ^3.0.0
|
||||
|
||||
vitepress@1.6.4:
|
||||
resolution: {integrity: sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==}
|
||||
hasBin: true
|
||||
@@ -4674,6 +4829,17 @@ packages:
|
||||
postcss:
|
||||
optional: true
|
||||
|
||||
vue-demi@0.14.10:
|
||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@vue/composition-api': ^1.0.0-rc.1
|
||||
vue: ^3.0.0-0 || ^2.6.0
|
||||
peerDependenciesMeta:
|
||||
'@vue/composition-api':
|
||||
optional: true
|
||||
|
||||
vue@3.5.18:
|
||||
resolution: {integrity: sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==}
|
||||
peerDependencies:
|
||||
@@ -4736,6 +4902,10 @@ packages:
|
||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
yaml@2.0.0-1:
|
||||
resolution: {integrity: sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -4752,6 +4922,11 @@ packages:
|
||||
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
z-schema@5.0.5:
|
||||
resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
zimmerframe@1.1.2:
|
||||
resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==}
|
||||
|
||||
@@ -4877,6 +5052,27 @@ snapshots:
|
||||
'@jridgewell/gen-mapping': 0.3.12
|
||||
'@jridgewell/trace-mapping': 0.3.29
|
||||
|
||||
'@apidevtools/json-schema-ref-parser@9.1.2':
|
||||
dependencies:
|
||||
'@jsdevtools/ono': 7.1.3
|
||||
'@types/json-schema': 7.0.15
|
||||
call-me-maybe: 1.0.2
|
||||
js-yaml: 4.1.1
|
||||
|
||||
'@apidevtools/openapi-schemas@2.1.0': {}
|
||||
|
||||
'@apidevtools/swagger-methods@3.0.2': {}
|
||||
|
||||
'@apidevtools/swagger-parser@10.0.3(openapi-types@12.1.3)':
|
||||
dependencies:
|
||||
'@apidevtools/json-schema-ref-parser': 9.1.2
|
||||
'@apidevtools/openapi-schemas': 2.1.0
|
||||
'@apidevtools/swagger-methods': 3.0.2
|
||||
'@jsdevtools/ono': 7.1.3
|
||||
call-me-maybe: 1.0.2
|
||||
openapi-types: 12.1.3
|
||||
z-schema: 5.0.5
|
||||
|
||||
'@aws-crypto/crc32@5.2.0':
|
||||
dependencies:
|
||||
'@aws-crypto/util': 5.2.0
|
||||
@@ -5646,13 +5842,33 @@ snapshots:
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/core@1.7.5':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.11
|
||||
|
||||
'@floating-ui/dom@1.7.2':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.7.2
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/dom@1.7.6':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.7.5
|
||||
'@floating-ui/utils': 0.2.11
|
||||
|
||||
'@floating-ui/utils@0.2.10': {}
|
||||
|
||||
'@floating-ui/utils@0.2.11': {}
|
||||
|
||||
'@floating-ui/vue@1.1.11(vue@3.5.18(typescript@5.8.3))':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.6
|
||||
'@floating-ui/utils': 0.2.11
|
||||
vue-demi: 0.14.10(vue@3.5.18(typescript@5.8.3))
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@gar/promisify@1.1.3':
|
||||
optional: true
|
||||
|
||||
@@ -5671,6 +5887,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.17
|
||||
|
||||
'@internationalized/number@3.6.5':
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.17
|
||||
|
||||
'@ioredis/commands@1.2.0': {}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
@@ -5705,6 +5925,8 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.4
|
||||
|
||||
'@jsdevtools/ono@7.1.3': {}
|
||||
|
||||
'@layerstack/svelte-actions@1.0.1-next.12':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.2
|
||||
@@ -6388,6 +6610,13 @@ snapshots:
|
||||
tailwindcss: 4.1.11
|
||||
vite: 6.3.5(@types/node@24.0.13)(jiti@2.4.2)(lightningcss@1.30.1)
|
||||
|
||||
'@tanstack/virtual-core@3.13.23': {}
|
||||
|
||||
'@tanstack/vue-virtual@3.13.23(vue@3.5.18(typescript@5.8.3))':
|
||||
dependencies:
|
||||
'@tanstack/virtual-core': 3.13.23
|
||||
vue: 3.5.18(typescript@5.8.3)
|
||||
|
||||
'@tootallnate/once@1.1.2':
|
||||
optional: true
|
||||
|
||||
@@ -6453,6 +6682,8 @@ snapshots:
|
||||
|
||||
'@types/http-errors@2.0.5': {}
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
@@ -6523,6 +6754,8 @@ snapshots:
|
||||
|
||||
'@types/strip-json-comments@0.0.30': {}
|
||||
|
||||
'@types/swagger-jsdoc@6.0.4': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
||||
'@types/uuid@9.0.8': {}
|
||||
@@ -6637,6 +6870,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vueuse/core@14.2.1(vue@3.5.18(typescript@5.8.3))':
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.21
|
||||
'@vueuse/metadata': 14.2.1
|
||||
'@vueuse/shared': 14.2.1(vue@3.5.18(typescript@5.8.3))
|
||||
vue: 3.5.18(typescript@5.8.3)
|
||||
|
||||
'@vueuse/integrations@12.8.2(axios@1.10.0)(focus-trap@7.6.5)(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@vueuse/core': 12.8.2(typescript@5.8.3)
|
||||
@@ -6650,12 +6890,18 @@ snapshots:
|
||||
|
||||
'@vueuse/metadata@12.8.2': {}
|
||||
|
||||
'@vueuse/metadata@14.2.1': {}
|
||||
|
||||
'@vueuse/shared@12.8.2(typescript@5.8.3)':
|
||||
dependencies:
|
||||
vue: 3.5.18(typescript@5.8.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vueuse/shared@14.2.1(vue@3.5.18(typescript@5.8.3))':
|
||||
dependencies:
|
||||
vue: 3.5.18(typescript@5.8.3)
|
||||
|
||||
'@xmldom/xmldom@0.8.10': {}
|
||||
|
||||
abbrev@1.1.1:
|
||||
@@ -6764,6 +7010,12 @@ snapshots:
|
||||
dependencies:
|
||||
sprintf-js: 1.0.3
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
aria-query@5.3.2: {}
|
||||
|
||||
async@3.2.6: {}
|
||||
@@ -6928,6 +7180,8 @@ snapshots:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
get-intrinsic: 1.3.0
|
||||
|
||||
call-me-maybe@1.0.2: {}
|
||||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
chalk@4.1.2:
|
||||
@@ -6961,6 +7215,10 @@ snapshots:
|
||||
|
||||
chownr@3.0.0: {}
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
|
||||
clean-stack@2.2.0:
|
||||
optional: true
|
||||
|
||||
@@ -6991,8 +7249,13 @@ snapshots:
|
||||
|
||||
comma-separated-tokens@2.0.3: {}
|
||||
|
||||
commander@6.2.0: {}
|
||||
|
||||
commander@7.2.0: {}
|
||||
|
||||
commander@9.5.0:
|
||||
optional: true
|
||||
|
||||
commondir@1.0.1: {}
|
||||
|
||||
compress-commons@6.0.2:
|
||||
@@ -7200,6 +7463,8 @@ snapshots:
|
||||
|
||||
deepmerge@4.3.1: {}
|
||||
|
||||
defu@6.1.4: {}
|
||||
|
||||
delaunator@5.0.1:
|
||||
dependencies:
|
||||
robust-predicates: 3.0.2
|
||||
@@ -7227,6 +7492,10 @@ snapshots:
|
||||
|
||||
dingbat-to-unicode@1.0.1: {}
|
||||
|
||||
doctrine@3.0.0:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
@@ -7441,6 +7710,8 @@ snapshots:
|
||||
|
||||
estree-walker@2.0.2: {}
|
||||
|
||||
esutils@2.0.3: {}
|
||||
|
||||
etag@1.8.1: {}
|
||||
|
||||
event-target-shim@5.0.1: {}
|
||||
@@ -7638,6 +7909,15 @@ snapshots:
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
glob@7.1.6:
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
inflight: 1.0.6
|
||||
inherits: 2.0.4
|
||||
minimatch: 3.1.2
|
||||
once: 1.4.0
|
||||
path-is-absolute: 1.0.1
|
||||
|
||||
glob@7.2.3:
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
@@ -7917,6 +8197,10 @@ snapshots:
|
||||
|
||||
joycon@3.1.1: {}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
jsbn@1.1.0: {}
|
||||
|
||||
json-bigint@1.0.0:
|
||||
@@ -8076,12 +8360,16 @@ snapshots:
|
||||
|
||||
lodash.defaults@4.2.0: {}
|
||||
|
||||
lodash.get@4.4.2: {}
|
||||
|
||||
lodash.includes@4.3.0: {}
|
||||
|
||||
lodash.isarguments@3.1.0: {}
|
||||
|
||||
lodash.isboolean@3.0.3: {}
|
||||
|
||||
lodash.isequal@4.5.0: {}
|
||||
|
||||
lodash.isinteger@4.0.4: {}
|
||||
|
||||
lodash.isnumber@3.0.3: {}
|
||||
@@ -8090,6 +8378,8 @@ snapshots:
|
||||
|
||||
lodash.isstring@4.0.1: {}
|
||||
|
||||
lodash.mergewith@4.6.2: {}
|
||||
|
||||
lodash.once@4.1.1: {}
|
||||
|
||||
lodash@4.17.21: {}
|
||||
@@ -8113,6 +8403,10 @@ snapshots:
|
||||
dependencies:
|
||||
svelte: 5.35.5
|
||||
|
||||
lucide-vue-next@0.577.0(vue@3.5.18(typescript@5.8.3)):
|
||||
dependencies:
|
||||
vue: 3.5.18(typescript@5.8.3)
|
||||
|
||||
luxon@3.7.1: {}
|
||||
|
||||
lz-string@1.5.0: {}
|
||||
@@ -8180,6 +8474,8 @@ snapshots:
|
||||
|
||||
mark.js@8.11.1: {}
|
||||
|
||||
markdown-it-link-attributes@4.0.1: {}
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mdast-util-to-hast@13.2.0:
|
||||
@@ -8431,6 +8727,8 @@ snapshots:
|
||||
|
||||
object-inspect@1.13.4: {}
|
||||
|
||||
ohash@2.0.11: {}
|
||||
|
||||
on-exit-leak-free@2.1.2: {}
|
||||
|
||||
on-finished@2.4.1:
|
||||
@@ -8447,6 +8745,8 @@ snapshots:
|
||||
regex: 6.0.1
|
||||
regex-recursion: 6.0.2
|
||||
|
||||
openapi-types@12.1.3: {}
|
||||
|
||||
option@0.2.4: {}
|
||||
|
||||
p-map@4.0.0:
|
||||
@@ -8726,6 +9026,22 @@ snapshots:
|
||||
dependencies:
|
||||
regex-utilities: 2.3.0
|
||||
|
||||
reka-ui@2.9.2(vue@3.5.18(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.2
|
||||
'@floating-ui/vue': 1.1.11(vue@3.5.18(typescript@5.8.3))
|
||||
'@internationalized/date': 3.8.2
|
||||
'@internationalized/number': 3.6.5
|
||||
'@tanstack/vue-virtual': 3.13.23(vue@3.5.18(typescript@5.8.3))
|
||||
'@vueuse/core': 14.2.1(vue@3.5.18(typescript@5.8.3))
|
||||
'@vueuse/shared': 14.2.1(vue@3.5.18(typescript@5.8.3))
|
||||
aria-hidden: 1.2.6
|
||||
defu: 6.1.4
|
||||
ohash: 2.0.11
|
||||
vue: 3.5.18(typescript@5.8.3)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
@@ -9135,6 +9451,23 @@ snapshots:
|
||||
'@sveltekit-i18n/parser-default': 1.1.1
|
||||
svelte: 5.35.5
|
||||
|
||||
swagger-jsdoc@6.2.8(openapi-types@12.1.3):
|
||||
dependencies:
|
||||
commander: 6.2.0
|
||||
doctrine: 3.0.0
|
||||
glob: 7.1.6
|
||||
lodash.mergewith: 4.6.2
|
||||
swagger-parser: 10.0.3(openapi-types@12.1.3)
|
||||
yaml: 2.0.0-1
|
||||
transitivePeerDependencies:
|
||||
- openapi-types
|
||||
|
||||
swagger-parser@10.0.3(openapi-types@12.1.3):
|
||||
dependencies:
|
||||
'@apidevtools/swagger-parser': 10.0.3(openapi-types@12.1.3)
|
||||
transitivePeerDependencies:
|
||||
- openapi-types
|
||||
|
||||
tabbable@6.2.0: {}
|
||||
|
||||
tailwind-merge@3.0.2: {}
|
||||
@@ -9385,6 +9718,19 @@ snapshots:
|
||||
optionalDependencies:
|
||||
vite: 6.3.5(@types/node@24.0.13)(jiti@2.4.2)(lightningcss@1.30.1)
|
||||
|
||||
vitepress-openapi@0.1.18(vitepress@1.6.4(@algolia/client-search@5.34.1)(@types/node@24.0.13)(axios@1.10.0)(lightningcss@1.30.1)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.8.3))(vue@3.5.18(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@vueuse/core': 14.2.1(vue@3.5.18(typescript@5.8.3))
|
||||
class-variance-authority: 0.7.1
|
||||
clsx: 2.1.1
|
||||
lucide-vue-next: 0.577.0(vue@3.5.18(typescript@5.8.3))
|
||||
markdown-it-link-attributes: 4.0.1
|
||||
reka-ui: 2.9.2(vue@3.5.18(typescript@5.8.3))
|
||||
vitepress: 1.6.4(@algolia/client-search@5.34.1)(@types/node@24.0.13)(axios@1.10.0)(lightningcss@1.30.1)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.8.3)
|
||||
vue: 3.5.18(typescript@5.8.3)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
|
||||
vitepress@1.6.4(@algolia/client-search@5.34.1)(@types/node@24.0.13)(axios@1.10.0)(lightningcss@1.30.1)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@docsearch/css': 3.8.2
|
||||
@@ -9434,6 +9780,10 @@ snapshots:
|
||||
- typescript
|
||||
- universal-cookie
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.18(typescript@5.8.3)):
|
||||
dependencies:
|
||||
vue: 3.5.18(typescript@5.8.3)
|
||||
|
||||
vue@3.5.18(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.18
|
||||
@@ -9488,6 +9838,8 @@ snapshots:
|
||||
|
||||
yallist@5.0.0: {}
|
||||
|
||||
yaml@2.0.0-1: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@17.7.2:
|
||||
@@ -9507,6 +9859,14 @@ snapshots:
|
||||
|
||||
yn@3.1.1: {}
|
||||
|
||||
z-schema@5.0.5:
|
||||
dependencies:
|
||||
lodash.get: 4.4.2
|
||||
lodash.isequal: 4.5.0
|
||||
validator: 13.12.0
|
||||
optionalDependencies:
|
||||
commander: 9.5.0
|
||||
|
||||
zimmerframe@1.1.2: {}
|
||||
|
||||
zip-stream@6.0.1:
|
||||
|
||||
Reference in New Issue
Block a user