fix: merge main, address code review feedback for security fix PR #816
- Merge origin/main into branch (resolve conflict in integrations_dashboard.html) - Add defensive JSON parsing with try/except for integration.config - Wrap tester() call in try/except to prevent 500 errors from bad config - Add i18n key integrations.connection_test_failed_fallback in en.json - Reference i18n key in template JS fallback message - Update SECURITY_AUDIT.md: add fix date (2026-03-23), update doc date - Remove accidental revert.sh file - Fix missing MagicMock/patch imports in test file - Add tests for invalid JSON config and tester exception error paths Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/DocuElevate/sessions/daebb70e-059a-4601-8864-88eef49f99cf
This commit is contained in:
+858
-17
@@ -27,9 +27,11 @@ DocuElevate implements rate limiting to protect against abuse and DoS attacks. R
|
||||
### Default Limits
|
||||
|
||||
- **Default endpoints**: 100 requests per minute
|
||||
- **File upload**: 600 requests per minute
|
||||
- **File upload**: 600 requests per minute (global) + 20 per user per 60 s (per-user, health-aware)
|
||||
- **Authentication**: 10 requests per minute
|
||||
|
||||
**Per-user upload rate limiting**: Upload endpoints (`/api/ui-upload`, `/api/process-url`) enforce a per-user sliding-window limit that adapts to system load. Under heavy queue depth or high CPU usage, the effective limit is reduced automatically. See the [Configuration Guide](ConfigurationGuide.md#per-user-upload-rate-limiting) for details.
|
||||
|
||||
**Note**: Document processing endpoints (OCR, metadata extraction) use built-in queue throttling to control processing rates and prevent upstream API overloads. No additional API-level rate limit is applied to processing endpoints.
|
||||
|
||||
### Rate Limit Headers
|
||||
@@ -53,6 +55,10 @@ RATE_LIMITING_ENABLED=true
|
||||
RATE_LIMIT_DEFAULT=100/minute
|
||||
RATE_LIMIT_UPLOAD=600/minute
|
||||
RATE_LIMIT_AUTH=10/minute
|
||||
|
||||
# Per-user upload rate limiting (health-aware)
|
||||
UPLOAD_RATE_LIMIT_PER_USER=20 # Max uploads per user per window
|
||||
UPLOAD_RATE_LIMIT_WINDOW=60 # Sliding window in seconds
|
||||
```
|
||||
|
||||
See [Configuration Guide](ConfigurationGuide.md) for more details.
|
||||
@@ -115,7 +121,8 @@ curl -X GET "http://<your-docuelevate-instance>/api/files" \
|
||||
|--------|----------|-------------|
|
||||
| `POST` | `/api/api-tokens/` | Create a new token |
|
||||
| `GET` | `/api/api-tokens/` | List all your tokens |
|
||||
| `DELETE` | `/api/api-tokens/{id}` | Revoke a token |
|
||||
| `DELETE` | `/api/api-tokens/{id}` | Revoke (active) or permanently delete (revoked) a token |
|
||||
| `POST` | `/api/api-tokens/{id}/reactivate` | Reactivate a revoked token |
|
||||
|
||||
### Session Authentication
|
||||
|
||||
@@ -235,17 +242,33 @@ The DocuElevate browser extension uses this endpoint to send files directly from
|
||||
|
||||
**POST** `/api/ui-upload`
|
||||
|
||||
Upload one or more files from your computer for processing.
|
||||
Upload a file from your computer for processing.
|
||||
|
||||
**Request**:
|
||||
- Multipart form data with file(s)
|
||||
- Multipart form data with a single `file` field
|
||||
|
||||
**Response**:
|
||||
**Response** (new file):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"file_ids": [123, 124],
|
||||
"message": "Files uploaded and queued for processing"
|
||||
"task_id": "abc-123",
|
||||
"status": "queued",
|
||||
"original_filename": "invoice.pdf",
|
||||
"stored_filename": "a1b2c3d4.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (exact duplicate, when `ENABLE_DEDUPLICATION=True`):
|
||||
```json
|
||||
{
|
||||
"status": "duplicate",
|
||||
"original_filename": "invoice.pdf",
|
||||
"stored_filename": "e5f6a7b8.pdf",
|
||||
"duplicate_of": {
|
||||
"duplicate_type": "exact",
|
||||
"original_file_id": 42,
|
||||
"original_filename": "invoice.pdf",
|
||||
"message": "This file is an exact duplicate of an already-processed document. It has not been queued for processing again."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1356,7 +1379,7 @@ Test an integration connection without saving. Useful for "Test connection" UI b
|
||||
{"success": true, "message": "IMAP connection successful"}
|
||||
```
|
||||
|
||||
Supported connection tests: `IMAP`, `S3`, `WEBDAV`, `NEXTCLOUD`. Other types return a message that testing is not yet supported.
|
||||
Supported connection tests: `DROPBOX`, `IMAP`, `S3`, `WEBDAV`, `NEXTCLOUD`. Other types return a message that testing is not yet supported.
|
||||
|
||||
### GET /api/integrations/quota/
|
||||
|
||||
@@ -1381,6 +1404,55 @@ Get the current user's integration quota usage.
|
||||
}
|
||||
```
|
||||
|
||||
## Cloud Provider Folder Browser
|
||||
|
||||
Browse folders in connected cloud storage providers. These endpoints are used by the OAuth callback pages to let users select a target folder after authorization.
|
||||
|
||||
### POST /api/dropbox/list-folders
|
||||
|
||||
List folders in a Dropbox account. Requires a short-lived OAuth access token obtained during the authorization flow.
|
||||
|
||||
**Request (form-data):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|----------------|--------|----------|--------------------------------------|
|
||||
| `access_token` | string | Yes | Dropbox OAuth access token |
|
||||
| `path` | string | No | Folder path to list (default: root) |
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"folders": [
|
||||
{ "name": "Documents", "path": "/Documents", "id": "id:abc123" },
|
||||
{ "name": "Photos", "path": "/Photos", "id": "id:def456" }
|
||||
],
|
||||
"path": "/",
|
||||
"has_more": false
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/onedrive/list-folders
|
||||
|
||||
List folders in a OneDrive account. Requires a short-lived OAuth access token obtained during the authorization flow.
|
||||
|
||||
**Request (form-data):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|----------------|--------|----------|--------------------------------------|
|
||||
| `access_token` | string | Yes | Microsoft Graph access token |
|
||||
| `path` | string | No | Folder path to list (default: root) |
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"folders": [
|
||||
{ "name": "Documents", "path": "/Documents", "id": "abc123", "child_count": 5 },
|
||||
{ "name": "Pictures", "path": "/Pictures", "id": "def456", "child_count": 12 }
|
||||
],
|
||||
"path": "/"
|
||||
}
|
||||
```
|
||||
|
||||
## Webhooks
|
||||
|
||||
Manage webhook configurations for notifying external systems when document events occur. All webhook endpoints require admin access.
|
||||
@@ -1571,6 +1643,47 @@ Lightweight endpoint returning the total number of queued + in-progress items. D
|
||||
|
||||
## Diagnostic
|
||||
|
||||
### GET /api/diagnostic/healthz/live
|
||||
|
||||
Lightweight liveness probe for Kubernetes. Returns **200 OK** as long as the process is running. This endpoint does **not** check external dependencies and is intentionally cheap.
|
||||
|
||||
**Authentication:** None (designed for kubelet probes)
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"status": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/diagnostic/healthz/ready
|
||||
|
||||
Readiness probe for Kubernetes. Verifies that the application can serve traffic by checking database and Redis connectivity.
|
||||
|
||||
**Authentication:** None (designed for kubelet probes)
|
||||
|
||||
**Response (200 OK) – ready to serve traffic:**
|
||||
```json
|
||||
{
|
||||
"status": "ready",
|
||||
"checks": {
|
||||
"database": {"status": "ok"},
|
||||
"redis": {"status": "ok"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (503 Service Unavailable) – database unreachable:**
|
||||
```json
|
||||
{
|
||||
"status": "not_ready",
|
||||
"checks": {
|
||||
"database": {"status": "error", "detail": "..."},
|
||||
"redis": {"status": "ok"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/diagnostic/health
|
||||
|
||||
System health endpoint designed for monitoring tools such as Grafana, Uptime Kuma, Prometheus blackbox exporter, or any HTTP-based health checker.
|
||||
@@ -2127,12 +2240,14 @@ Usage tracking records when each token was last used and from which IP address.
|
||||
|
||||
### POST /api/api-tokens/
|
||||
|
||||
Create a new API token.
|
||||
Create a new API token. Optionally specify a lifetime in days via
|
||||
`expires_in_days` (1–3650). If omitted the token never expires.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"name": "CI Pipeline"
|
||||
"name": "CI Pipeline",
|
||||
"expires_in_days": 90
|
||||
}
|
||||
```
|
||||
|
||||
@@ -2147,7 +2262,8 @@ Create a new API token.
|
||||
"last_used_at": null,
|
||||
"last_used_ip": null,
|
||||
"created_at": "2026-03-08T12:00:00Z",
|
||||
"revoked_at": null
|
||||
"revoked_at": null,
|
||||
"expires_at": "2026-06-06T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -2169,15 +2285,20 @@ List all tokens for the authenticated user. The full token value is never includ
|
||||
"last_used_at": "2026-03-08T15:30:00Z",
|
||||
"last_used_ip": "203.0.113.42",
|
||||
"created_at": "2026-03-08T12:00:00Z",
|
||||
"revoked_at": null
|
||||
"revoked_at": null,
|
||||
"expires_at": "2026-06-06T12:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### DELETE /api/api-tokens/{token_id}
|
||||
|
||||
Revoke a token. The token is soft-deleted (kept for audit purposes) and can no
|
||||
longer be used for authentication.
|
||||
Revoke or permanently delete a token:
|
||||
|
||||
* **Active token** – soft-revoked (kept for audit purposes, marked inactive).
|
||||
Response: `{"detail": "Token revoked"}`
|
||||
* **Already-revoked token** – permanently deleted from the database.
|
||||
Response: `{"detail": "Token deleted"}`
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
@@ -2186,6 +2307,13 @@ longer be used for authentication.
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/api-tokens/{token_id}/reactivate
|
||||
|
||||
Reactivate a previously revoked token. Clears `revoked_at` and sets
|
||||
`is_active` back to `true`.
|
||||
|
||||
**Response (200):** The updated `TokenResponse` object.
|
||||
|
||||
### Using API Tokens
|
||||
|
||||
Include the token in the `Authorization` header of any API request:
|
||||
@@ -2214,6 +2342,316 @@ print(response.json())
|
||||
```
|
||||
|
||||
|
||||
## Classification Rules
|
||||
|
||||
The classification rules API lets you manage custom document classification rules. Rules are evaluated during the `classify` pipeline step to assign a category to each document based on filename patterns, content keywords, and metadata fields.
|
||||
|
||||
### Built-in Categories
|
||||
|
||||
```bash
|
||||
GET /api/classification-rules/categories
|
||||
```
|
||||
|
||||
Returns the pre-built classification categories.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"invoice": "Invoice",
|
||||
"contract": "Contract",
|
||||
"receipt": "Receipt",
|
||||
"letter": "Letter",
|
||||
"report": "Report",
|
||||
"bank_statement": "Bank Statement",
|
||||
"tax_document": "Tax Document",
|
||||
"insurance": "Insurance Document",
|
||||
"payslip": "Payslip",
|
||||
"unknown": "Unknown"
|
||||
}
|
||||
```
|
||||
|
||||
### Rule Types
|
||||
|
||||
```bash
|
||||
GET /api/classification-rules/rule-types
|
||||
```
|
||||
|
||||
Returns the supported rule types with descriptions.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "filename_pattern",
|
||||
"label": "Filename Pattern",
|
||||
"description": "Regex pattern matched against the original filename."
|
||||
},
|
||||
{
|
||||
"type": "content_keyword",
|
||||
"label": "Content Keyword",
|
||||
"description": "Pipe-separated keywords matched against the OCR text."
|
||||
},
|
||||
{
|
||||
"type": "metadata_match",
|
||||
"label": "Metadata Match",
|
||||
"description": "field=value pattern matched against existing AI metadata."
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### List Rules
|
||||
|
||||
```bash
|
||||
GET /api/classification-rules/
|
||||
```
|
||||
|
||||
List all classification rules visible to the current user (system rules + own rules).
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"owner_id": "user@example.com",
|
||||
"name": "German Invoice Filename",
|
||||
"category": "invoice",
|
||||
"rule_type": "filename_pattern",
|
||||
"pattern": "(?i)rechnung",
|
||||
"priority": 10,
|
||||
"case_sensitive": false,
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Create Rule
|
||||
|
||||
```bash
|
||||
POST /api/classification-rules/
|
||||
```
|
||||
|
||||
Create a new custom classification rule.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"name": "German Invoice Filename",
|
||||
"category": "invoice",
|
||||
"rule_type": "filename_pattern",
|
||||
"pattern": "(?i)rechnung",
|
||||
"priority": 10,
|
||||
"case_sensitive": false,
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `name` | string | Yes | Unique rule name (per user) |
|
||||
| `category` | string | Yes | Target category (e.g. `invoice`, `contract`, or custom) |
|
||||
| `rule_type` | string | Yes | One of: `filename_pattern`, `content_keyword`, `metadata_match` |
|
||||
| `pattern` | string | Yes | Regex (filename), pipe-separated keywords (content), or `field=value` (metadata) |
|
||||
| `priority` | integer | No | Higher priority rules are evaluated first (default: 0) |
|
||||
| `case_sensitive` | boolean | No | Case-sensitive matching (default: false) |
|
||||
| `enabled` | boolean | No | Whether the rule is active (default: true) |
|
||||
|
||||
**Response (201):**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"owner_id": "user@example.com",
|
||||
"name": "German Invoice Filename",
|
||||
"category": "invoice",
|
||||
"rule_type": "filename_pattern",
|
||||
"pattern": "(?i)rechnung",
|
||||
"priority": 10,
|
||||
"case_sensitive": false,
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
### Get Rule
|
||||
|
||||
```bash
|
||||
GET /api/classification-rules/{rule_id}
|
||||
```
|
||||
|
||||
### Update Rule
|
||||
|
||||
```bash
|
||||
PUT /api/classification-rules/{rule_id}
|
||||
```
|
||||
|
||||
**Request (partial update):**
|
||||
```json
|
||||
{
|
||||
"priority": 20,
|
||||
"enabled": false
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Rule
|
||||
|
||||
```bash
|
||||
DELETE /api/classification-rules/{rule_id}
|
||||
```
|
||||
|
||||
**Response:** `204 No Content`
|
||||
|
||||
|
||||
|
||||
## Automation (Zapier / Make.com)
|
||||
|
||||
Manage automation hook subscriptions for integrating DocuElevate with external platforms like Zapier and Make.com. All endpoints require API token authentication (`Authorization: Bearer <token>`).
|
||||
|
||||
### Supported Events
|
||||
|
||||
The automation system shares event types with the [Webhooks](#webhooks) subsystem:
|
||||
|
||||
| Event | Description |
|
||||
|-------|-------------|
|
||||
| `document.uploaded` | A new document has been ingested |
|
||||
| `document.processed` | A document finished processing successfully |
|
||||
| `document.failed` | Document processing failed |
|
||||
| `user.signup` | A new user account was created |
|
||||
| `user.plan_changed` | A user's subscription plan changed |
|
||||
| `user.payment_issue` | A payment issue was reported for a user |
|
||||
|
||||
### GET /api/automation/events
|
||||
|
||||
List all valid event types that automation hooks can subscribe to.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
["document.failed", "document.processed", "document.uploaded", "user.payment_issue", "user.plan_changed", "user.signup"]
|
||||
```
|
||||
|
||||
### POST /api/automation/hooks/subscribe
|
||||
|
||||
Subscribe to DocuElevate events. Zapier and Make.com call this endpoint to register a webhook URL that receives event notifications.
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
curl -X POST "http://your-instance/api/automation/hooks/subscribe" \
|
||||
-H "Authorization: Bearer de_your_token_here" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"target_url": "https://hooks.zapier.com/hooks/catch/123456/abcdef/",
|
||||
"events": ["document.processed", "document.uploaded"],
|
||||
"hook_type": "zapier",
|
||||
"secret": "optional-signing-secret",
|
||||
"description": "My Zap for processed documents"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response (201):**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"target_url": "https://hooks.zapier.com/hooks/catch/123456/abcdef/",
|
||||
"events": ["document.processed", "document.uploaded"],
|
||||
"is_active": true,
|
||||
"hook_type": "zapier",
|
||||
"description": "My Zap for processed documents",
|
||||
"has_secret": true
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/automation/hooks
|
||||
|
||||
List all automation hook subscriptions.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"target_url": "https://hooks.zapier.com/hooks/catch/123456/abcdef/",
|
||||
"events": ["document.processed", "document.uploaded"],
|
||||
"is_active": true,
|
||||
"hook_type": "zapier",
|
||||
"description": "My Zap for processed documents",
|
||||
"has_secret": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### DELETE /api/automation/hooks/{hook_id}
|
||||
|
||||
Unsubscribe an automation hook. Zapier calls this when a Zap is turned off or deleted.
|
||||
|
||||
**Response (204):** No content.
|
||||
|
||||
### GET /api/automation/triggers/sample/{event}
|
||||
|
||||
Get sample trigger data for Zapier field mapping. Zapier uses this during Zap setup to discover available fields.
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
curl "http://your-instance/api/automation/triggers/sample/document.processed" \
|
||||
-H "Authorization: Bearer de_your_token_here"
|
||||
```
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "evt_sample0002",
|
||||
"event": "document.processed",
|
||||
"timestamp": 1710000060.0,
|
||||
"document_id": 42,
|
||||
"filename": "invoice_2024.pdf",
|
||||
"status": "processed",
|
||||
"title": "Invoice #1234",
|
||||
"owner_id": "user@example.com"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### POST /api/automation/actions/upload
|
||||
|
||||
Upload a document from an automation platform. This incoming action endpoint allows Zapier or Make.com to push documents into DocuElevate for processing.
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
curl -X POST "http://your-instance/api/automation/actions/upload" \
|
||||
-H "Authorization: Bearer de_your_token_here" \
|
||||
-F "file=@/path/to/document.pdf"
|
||||
```
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"status": "accepted",
|
||||
"filename": "document.pdf",
|
||||
"task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
}
|
||||
```
|
||||
|
||||
### Zapier-Compatible Payload Format
|
||||
|
||||
When events fire, automation hooks receive a **flat JSON payload** (no nested `data` key) that Zapier and Make.com can easily map:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "evt_a1b2c3d4e5f67890",
|
||||
"event": "document.processed",
|
||||
"timestamp": 1710000060.0,
|
||||
"document_id": 42,
|
||||
"filename": "invoice_2024.pdf",
|
||||
"status": "processed",
|
||||
"title": "Invoice #1234",
|
||||
"owner_id": "user@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
The `id` field is unique per event and is used by Zapier for deduplication. If a `secret` was provided during subscription, an `X-Webhook-Signature` header with an HMAC-SHA256 signature is included.
|
||||
|
||||
### Retry Behavior
|
||||
|
||||
Automation hook deliveries follow the same retry policy as regular webhooks: up to 3 retries with exponential backoff (60 s, 300 s, 900 s) and ±20% jitter.
|
||||
|
||||
|
||||
## Further Assistance
|
||||
|
||||
For additional help with the API, please contact our support team or refer to the [Development Guide](../CONTRIBUTING.md).
|
||||
@@ -2268,9 +2706,14 @@ List all registered push-notification devices for the current user.
|
||||
|
||||
### DELETE /api/mobile/devices/{device_id}
|
||||
|
||||
Deactivate a push-notification device. The device will no longer receive push notifications.
|
||||
Deactivate or permanently delete a push-notification device:
|
||||
|
||||
**Response (204 No Content)**
|
||||
* **Active device** – soft-deactivated (record kept, will no longer receive push notifications).
|
||||
Response: `{"detail": "Device deactivated"}`
|
||||
* **Already-inactive device** – permanently deleted from the database.
|
||||
Response: `{"detail": "Device deleted"}`
|
||||
|
||||
**Response (200)**
|
||||
|
||||
### GET /api/mobile/whoami
|
||||
|
||||
@@ -2405,3 +2848,401 @@ query GetDocument($id: Int!) {
|
||||
}
|
||||
```
|
||||
Variables: `{ "id": 42 }`
|
||||
|
||||
## System Reset
|
||||
|
||||
Admin-only endpoints for resetting the system to a clean state. Requires `ENABLE_FACTORY_RESET=True`.
|
||||
|
||||
### GET /api/admin/system-reset/status
|
||||
|
||||
Check whether the system reset feature is enabled.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"factory_reset_on_startup": false
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/admin/system-reset/full
|
||||
|
||||
Wipe all user data (database + work-files).
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"confirmation": "DELETE"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"result": {
|
||||
"database": { "files": 42, "processing_logs": 100 },
|
||||
"filesystem": { "deleted_dirs": 5, "deleted_files": 12 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/admin/system-reset/reimport
|
||||
|
||||
Move original files to a reimport folder, wipe everything, and configure the reimport folder as a watch folder for re-ingestion.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"confirmation": "REIMPORT"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"result": {
|
||||
"database": { "files": 42 },
|
||||
"filesystem": { "deleted_dirs": 5, "deleted_files": 12 },
|
||||
"reimport": { "files_moved": 42, "reimport_folder": "/workdir/reimport" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comments & Annotations
|
||||
|
||||
Threaded comments and PDF annotations for document collaboration.
|
||||
|
||||
### List Comments
|
||||
|
||||
**GET** `/api/files/{file_id}/comments`
|
||||
|
||||
Returns all comments for a document, organized into a threaded tree.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"file_id": 1,
|
||||
"comments": [
|
||||
{
|
||||
"id": 1,
|
||||
"file_id": 1,
|
||||
"user_id": "alice",
|
||||
"parent_id": null,
|
||||
"body": "Please review section 3.",
|
||||
"mentions": ["bob"],
|
||||
"is_resolved": false,
|
||||
"created_at": "2026-03-21T12:00:00+00:00",
|
||||
"updated_at": "2026-03-21T12:00:00+00:00",
|
||||
"replies": [
|
||||
{
|
||||
"id": 2,
|
||||
"file_id": 1,
|
||||
"user_id": "bob",
|
||||
"parent_id": 1,
|
||||
"body": "Done!",
|
||||
"mentions": [],
|
||||
"is_resolved": false,
|
||||
"created_at": "2026-03-21T12:05:00+00:00",
|
||||
"updated_at": "2026-03-21T12:05:00+00:00",
|
||||
"replies": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"total": 2
|
||||
}
|
||||
```
|
||||
|
||||
### Create Comment
|
||||
|
||||
**POST** `/api/files/{file_id}/comments`
|
||||
|
||||
Create a new comment on a document. @mentions are automatically extracted from the body.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"body": "Hey @bob, please review this section.",
|
||||
"parent_id": null
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201):**
|
||||
```json
|
||||
{
|
||||
"id": 3,
|
||||
"file_id": 1,
|
||||
"user_id": "alice",
|
||||
"parent_id": null,
|
||||
"body": "Hey @bob, please review this section.",
|
||||
"mentions": ["bob"],
|
||||
"is_resolved": false,
|
||||
"created_at": "2026-03-21T12:10:00+00:00",
|
||||
"updated_at": "2026-03-21T12:10:00+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
### Update Comment
|
||||
|
||||
**PUT** `/api/files/{file_id}/comments/{comment_id}`
|
||||
|
||||
Update the body of an existing comment. Only the comment author may update it.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"body": "Updated comment text @charlie"
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Comment
|
||||
|
||||
**DELETE** `/api/files/{file_id}/comments/{comment_id}`
|
||||
|
||||
Delete a comment. Only the comment author may delete it.
|
||||
|
||||
**Response:** `204 No Content`
|
||||
|
||||
### Resolve / Unresolve Comment
|
||||
|
||||
**PATCH** `/api/files/{file_id}/comments/{comment_id}/resolve`
|
||||
|
||||
Mark a comment thread as resolved or unresolved.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"is_resolved": true
|
||||
}
|
||||
```
|
||||
|
||||
### List Annotations
|
||||
|
||||
**GET** `/api/files/{file_id}/annotations`
|
||||
|
||||
Returns all PDF page annotations for a document, ordered by page then creation time.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"file_id": 1,
|
||||
"annotations": [
|
||||
{
|
||||
"id": 1,
|
||||
"file_id": 1,
|
||||
"user_id": "alice",
|
||||
"page": 1,
|
||||
"x": 100.0,
|
||||
"y": 200.0,
|
||||
"width": 150.0,
|
||||
"height": 20.0,
|
||||
"content": "Important paragraph",
|
||||
"annotation_type": "highlight",
|
||||
"color": "#ffff00",
|
||||
"created_at": "2026-03-21T12:00:00+00:00",
|
||||
"updated_at": "2026-03-21T12:00:00+00:00"
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
|
||||
### Create Annotation
|
||||
|
||||
**POST** `/api/files/{file_id}/annotations`
|
||||
|
||||
Create a new annotation on a PDF page.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"page": 1,
|
||||
"x": 100.0,
|
||||
"y": 200.0,
|
||||
"width": 150.0,
|
||||
"height": 20.0,
|
||||
"content": "Important paragraph",
|
||||
"annotation_type": "highlight",
|
||||
"color": "#ffff00"
|
||||
}
|
||||
```
|
||||
|
||||
Allowed `annotation_type` values: `note`, `highlight`, `underline`, `strikethrough`.
|
||||
|
||||
### Update Annotation
|
||||
|
||||
**PUT** `/api/files/{file_id}/annotations/{annotation_id}`
|
||||
|
||||
Update an existing annotation. Only the annotation author may update it.
|
||||
|
||||
**Request** (all fields optional):
|
||||
```json
|
||||
{
|
||||
"content": "Updated note",
|
||||
"color": "#00ff00"
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Annotation
|
||||
|
||||
**DELETE** `/api/files/{file_id}/annotations/{annotation_id}`
|
||||
|
||||
Delete an annotation. Only the annotation author may delete it.
|
||||
|
||||
**Response:** `204 No Content`
|
||||
|
||||
### List Mentionable Users
|
||||
|
||||
**GET** `/api/users/mentionable`
|
||||
|
||||
Returns all non-blocked user profiles for the @mention autocomplete.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
[
|
||||
{ "user_id": "alice", "display_name": "Alice Anderson" },
|
||||
{ "user_id": "bob", "display_name": "Bob Baker" }
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Sharing & Permissions
|
||||
|
||||
DocuElevate supports per-user document sharing with role-based access control.
|
||||
|
||||
### Roles
|
||||
|
||||
| Role | View | Comment / Annotate | Edit metadata | Delete | Share |
|
||||
|----------|------|--------------------|---------------|--------|-------|
|
||||
| `owner` | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| `editor` | ✓ | ✓ | ✓ | ✗ | ✗ |
|
||||
| `viewer` | ✓ | ✓ | ✗ | ✗ | ✗ |
|
||||
|
||||
- Only the **file owner** can share a document, change roles, or delete the document.
|
||||
- When a user is **@mentioned** in a comment they are automatically granted `viewer` access to the document (multi-user mode only).
|
||||
|
||||
---
|
||||
|
||||
### List Shares
|
||||
|
||||
**GET** `/api/files/{file_id}/shares`
|
||||
|
||||
Returns all active shares for a document. Only the file owner (or an admin) may call this endpoint.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"file_id": 42,
|
||||
"owner_id": "alice",
|
||||
"shared_with_user_id": "bob",
|
||||
"role": "viewer",
|
||||
"created_at": "2026-03-22T10:00:00+00:00",
|
||||
"updated_at": "2026-03-22T10:00:00+00:00"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `403`: Not the file owner
|
||||
- `404`: File not found
|
||||
|
||||
---
|
||||
|
||||
### Create Share
|
||||
|
||||
**POST** `/api/files/{file_id}/shares`
|
||||
|
||||
Share a document with another user. Only the file owner may call this endpoint. If the user already has a share, their role is updated.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"shared_with_user_id": "bob",
|
||||
"role": "viewer"
|
||||
}
|
||||
```
|
||||
|
||||
`role` must be `"viewer"` (default) or `"editor"`.
|
||||
|
||||
**Response (201):**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"file_id": 42,
|
||||
"owner_id": "alice",
|
||||
"shared_with_user_id": "bob",
|
||||
"role": "viewer",
|
||||
"created_at": "2026-03-22T10:00:00+00:00",
|
||||
"updated_at": "2026-03-22T10:00:00+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `403`: Not the file owner
|
||||
- `422`: Invalid role, empty user ID, or sharing with self
|
||||
|
||||
---
|
||||
|
||||
### Update Share Role
|
||||
|
||||
**PUT** `/api/files/{file_id}/shares/{share_id}`
|
||||
|
||||
Change the role of an existing share. Only the file owner may call this endpoint.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"role": "editor"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200):** Updated share object.
|
||||
|
||||
**Error Responses:**
|
||||
- `403`: Not the file owner
|
||||
- `404`: Share not found
|
||||
- `422`: Invalid role
|
||||
|
||||
---
|
||||
|
||||
### Revoke Share
|
||||
|
||||
**DELETE** `/api/files/{file_id}/shares/{share_id}`
|
||||
|
||||
Remove a user's access to a document. Only the file owner may revoke shares.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{ "status": "success", "message": "Share revoked successfully" }
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `403`: Not the file owner
|
||||
- `404`: Share not found
|
||||
|
||||
---
|
||||
|
||||
### List Shared With
|
||||
|
||||
**GET** `/api/files/{file_id}/shared-with`
|
||||
|
||||
Returns who a document is shared with. Accessible to any user with at least `viewer` access (owner, editors, and viewers can all call this).
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"share_id": 1,
|
||||
"user_id": "bob",
|
||||
"display_name": "Bob Baker",
|
||||
"role": "viewer"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
# Apple App Store Compliance Audit Report
|
||||
|
||||
This document details the findings from a comprehensive audit of the DocuElevate mobile app against Apple's App Store Review Guidelines, Human Interface Guidelines (HIG), and privacy requirements. It covers all areas of compliance, risks for rejection, and recommendations.
|
||||
|
||||
> **Last Audited:** March 2026
|
||||
> **App Version:** 1.0.0
|
||||
> **Expo SDK:** 54.0.0
|
||||
> **Bundle ID:** `org.docuelevate.mobile`
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The DocuElevate mobile app is broadly compliant with Apple's App Store requirements. The following issues were identified and resolved as part of this audit:
|
||||
|
||||
| Issue | Severity | Status |
|
||||
|-------|----------|--------|
|
||||
| Unused `fetch` background mode declared | High | ✅ Fixed |
|
||||
| Missing privacy manifest for required reason APIs | High | ✅ Fixed |
|
||||
| No account deletion option (Guideline 5.1.1(v)) | Critical | ✅ Fixed |
|
||||
| No Privacy Policy / Terms of Service links in-app | High | ✅ Fixed |
|
||||
| Emoji used as UI icons instead of platform-native icons | Medium | ✅ Fixed |
|
||||
| Missing app version display | Low | ✅ Fixed |
|
||||
| Unused `Switch` import in ProfileScreen | Low | ✅ Fixed |
|
||||
|
||||
---
|
||||
|
||||
## 1. Human Interface Guidelines (HIG)
|
||||
|
||||
### 1.1 Navigation & Tab Bar ✅
|
||||
|
||||
- The app uses a standard bottom tab bar with three tabs: Upload, Files, and Profile.
|
||||
- Tab icons use **Ionicons** (an icon set that closely maps to Apple's SF Symbols).
|
||||
- Active/inactive tab colors follow iOS conventions (`#1e40af` active, `#9ca3af` inactive).
|
||||
- Header styling uses a solid color background with white text, consistent with iOS navigation bar patterns.
|
||||
|
||||
### 1.2 Icons & Visual Assets ✅
|
||||
|
||||
- **App icon:** Custom `icon.png` provided at root level; Expo handles generating all required sizes.
|
||||
- **Splash screen:** Uses branded splash with `contain` resize mode and matching background color.
|
||||
- **Adaptive icon (Android):** Properly configured with foreground image and background color.
|
||||
- **Action buttons:** Previously used emoji characters (📷, 🖼️, 📄) which render inconsistently across iOS versions. **Fixed:** Now using Ionicons (`camera-outline`, `images-outline`, `document-outline`).
|
||||
- **Status indicators:** Previously used emoji (✅, ❌, ⏳, ⚙️). **Fixed:** Now using Ionicons with semantic colors.
|
||||
|
||||
### 1.3 Typography & Colors ✅
|
||||
|
||||
- Uses system fonts (default React Native text rendering uses San Francisco on iOS).
|
||||
- Color palette (`#1e40af` primary blue, semantic reds/greens/grays) provides sufficient contrast ratios.
|
||||
- Text sizes follow iOS recommended minimums (body text ≥ 13pt).
|
||||
|
||||
### 1.4 Touch Targets ✅
|
||||
|
||||
- All interactive elements have `minHeight: 44` or `minHeight: 48` (meets Apple's 44×44pt minimum).
|
||||
- Back links, cancel buttons, and retry buttons all meet minimum touch target requirements.
|
||||
|
||||
### 1.5 Safe Areas ✅
|
||||
|
||||
- The app uses `react-native-safe-area-context` (`SafeAreaProvider`) to respect device notches, Dynamic Island, and home indicator.
|
||||
|
||||
### 1.6 Dark Mode ✅
|
||||
|
||||
- `userInterfaceStyle: "automatic"` is set in `app.json`, enabling automatic dark mode support.
|
||||
|
||||
---
|
||||
|
||||
## 2. Privacy & Data Usage
|
||||
|
||||
### 2.1 Permission Descriptions ✅
|
||||
|
||||
All iOS permission strings (Info.plist keys) are present and provide clear, specific descriptions of why each permission is needed:
|
||||
|
||||
| Permission | Key | Description |
|
||||
|-----------|-----|-------------|
|
||||
| Camera | `NSCameraUsageDescription` | "DocuElevate uses the camera to scan QR codes for login and to capture documents for upload." |
|
||||
| Photo Library (Read) | `NSPhotoLibraryUsageDescription` | "DocuElevate accesses your photo library to select documents for upload." |
|
||||
| Photo Library (Write) | `NSPhotoLibraryAddUsageDescription` | "DocuElevate saves scanned documents to your photo library." |
|
||||
|
||||
**Assessment:** All descriptions clearly explain the purpose, which is a requirement for App Review approval.
|
||||
|
||||
### 2.2 Push Notifications ✅
|
||||
|
||||
- Push notification permission is requested at runtime (not at launch) when the user enters the authenticated area.
|
||||
- The app works gracefully without push notifications if permission is denied.
|
||||
- Device tokens are registered via a dedicated backend endpoint.
|
||||
|
||||
### 2.3 Background Modes ✅ (Fixed)
|
||||
|
||||
- **Previous state:** `UIBackgroundModes` included `["fetch", "remote-notification"]`.
|
||||
- **Issue:** The app does not implement background fetch (`application:performFetchWithCompletionHandler:`). Apple may reject apps that declare background modes they don't actively use (Guideline 2.5.4).
|
||||
- **Fix:** Removed `fetch` from `UIBackgroundModes`. Only `remote-notification` remains, which is required for push notification delivery.
|
||||
|
||||
### 2.4 Privacy Manifest ✅ (Fixed)
|
||||
|
||||
Starting in Spring 2024, Apple requires a privacy manifest (`PrivacyInfo.xcprivacy`) for apps using specific APIs. The following required reason APIs are used by the app's dependencies:
|
||||
|
||||
| API Category | Reason Code | Justification |
|
||||
|-------------|-------------|---------------|
|
||||
| `NSPrivacyAccessedAPICategoryUserDefaults` | `CA92.1` | Used by `@react-native-async-storage/async-storage` for user preferences |
|
||||
| `NSPrivacyAccessedAPICategoryFileTimestamp` | `C617.1` | Used by `expo-file-system` to read file metadata |
|
||||
| `NSPrivacyAccessedAPICategoryDiskSpace` | `E174.1` | Used by Expo runtime for storage space checks |
|
||||
| `NSPrivacyAccessedAPICategorySystemBootTime` | `35F9.1` | Used by React Native's timing APIs |
|
||||
|
||||
The privacy manifest is configured via `expo-build-properties` plugin in `app.json`, which ensures it is included in the generated Xcode project during EAS Build.
|
||||
|
||||
### 2.5 Tracking & Analytics ✅
|
||||
|
||||
- `NSPrivacyTracking: false` — the app does **not** track users.
|
||||
- `NSPrivacyCollectedDataTypes: []` — no data types are collected for tracking.
|
||||
- No analytics SDKs (Firebase Analytics, Amplitude, Mixpanel, etc.) are included.
|
||||
- No App Tracking Transparency (ATT) prompt is needed.
|
||||
|
||||
### 2.6 Encryption Declaration ✅
|
||||
|
||||
- `ITSAppUsesNonExemptEncryption: false` — the app uses only standard HTTPS/TLS for network communication, which is exempt from export compliance requirements.
|
||||
|
||||
### 2.7 Data Storage Security ✅
|
||||
|
||||
- API tokens are stored in the device keychain via `expo-secure-store` (uses iOS Keychain Services).
|
||||
- No sensitive data is stored in `AsyncStorage` or `UserDefaults`.
|
||||
- Server URL is stored in secure storage, not in plain text files.
|
||||
|
||||
---
|
||||
|
||||
## 3. App Store Review Guidelines Compliance
|
||||
|
||||
### 3.1 Functionality (Guideline 2.x) ✅
|
||||
|
||||
- **2.1 App Completeness:** The app provides a complete, functional experience. All advertised features (camera capture, file upload, document list, push notifications) work as described.
|
||||
- **2.3 Accurate Metadata:** App name ("DocuElevate"), description, and screenshots should accurately reflect the app's functionality.
|
||||
- **2.5.4 Background Modes:** Only `remote-notification` is declared, which is actively used. ✅ Fixed.
|
||||
|
||||
### 3.2 Content & Intellectual Property (Guideline 3.x) ✅
|
||||
|
||||
- No third-party trademarked content is used.
|
||||
- The app does not display user-generated content publicly (documents are private to each user).
|
||||
- No copyrighted content is bundled with the app.
|
||||
|
||||
### 3.3 Business (Guideline 3.1.x) ✅
|
||||
|
||||
- The app does not include in-app purchases, subscriptions, or payment processing.
|
||||
- No physical goods or services are sold through the app.
|
||||
- Authentication is handled via self-hosted or enterprise SSO — no Apple Sign-In requirement applies (Apple Sign-In is required only when third-party social login options like Google/Facebook are offered as the primary login method; enterprise SSO to a self-hosted server is exempt).
|
||||
|
||||
### 3.4 Safety & Privacy (Guideline 5.x) ✅
|
||||
|
||||
- **5.1.1 Data Collection and Storage:** The app collects only what is necessary for its functionality (server URL, auth token, push token).
|
||||
- **5.1.1(v) Account Deletion:** ✅ Fixed. Users can now initiate account deletion from the Profile screen, which opens the server's account deletion page in the browser.
|
||||
- **5.1.2 Data Use and Sharing:** No data is shared with third parties or used for advertising.
|
||||
|
||||
### 3.5 Privacy Policy ✅ (Fixed)
|
||||
|
||||
- **Requirement:** Apple requires all apps to have an accessible privacy policy.
|
||||
- **Fix:** Privacy Policy and Terms of Service links are now accessible from the Profile screen, opening the server's hosted policy pages.
|
||||
- **App Store Connect:** The privacy policy URL must also be provided in App Store Connect during submission.
|
||||
|
||||
### 3.6 Login & Authentication ✅
|
||||
|
||||
- Two login methods are available: SSO (browser-based OAuth) and QR code scanning.
|
||||
- Both methods provide clear error messages on failure.
|
||||
- The app correctly handles authentication cancellation.
|
||||
- Session restoration on app launch is implemented.
|
||||
- **Demo Account:** For App Review, a demo account may need to be provided in App Store Connect's review notes. Ensure the review team can access a test server.
|
||||
|
||||
---
|
||||
|
||||
## 4. Technical Compliance
|
||||
|
||||
### 4.1 API Usage ✅
|
||||
|
||||
- No private APIs are used (all functionality comes from Expo SDK and React Native public APIs).
|
||||
- No deprecated APIs are used that would trigger rejection.
|
||||
|
||||
### 4.2 Network Security ✅
|
||||
|
||||
- The app validates server URLs require `http://` or `https://` scheme.
|
||||
- All API calls use Bearer token authentication over HTTPS.
|
||||
- App Transport Security (ATS) is not explicitly disabled — default iOS ATS rules apply.
|
||||
|
||||
### 4.3 Deep Linking ✅
|
||||
|
||||
- Custom URL scheme `docuelevate://` is properly registered.
|
||||
- Deep link handling for QR login (`docuelevate://qr-login`) and file sharing is implemented correctly.
|
||||
- `WebBrowser.openAuthSessionAsync` is used for OAuth, which properly handles the authentication session lifecycle.
|
||||
|
||||
### 4.4 Document Handling ✅
|
||||
|
||||
- `CFBundleDocumentTypes` properly declares supported file types.
|
||||
- `LSSupportsOpeningDocumentsInPlace: false` ensures iOS copies shared files to the app's accessible Inbox directory, avoiding security-scoped URL issues.
|
||||
- The `+not-found.tsx` handler correctly intercepts iOS "Open In…" file paths.
|
||||
- `UploadScreen` uses `expo-file-system` to copy external files to cache before uploading for reliable file access.
|
||||
|
||||
### 4.5 Crash Resistance ✅
|
||||
|
||||
- All network calls are wrapped in try/catch blocks.
|
||||
- Error states are displayed to users with actionable recovery options (retry buttons).
|
||||
- Permission denials are handled gracefully with explanatory messages.
|
||||
|
||||
---
|
||||
|
||||
## 5. Onboarding & First-Run Experience
|
||||
|
||||
### 5.1 Welcome Screen ✅
|
||||
|
||||
- Clean, informative welcome screen with app branding and feature highlights.
|
||||
- Clear "Get Started" call-to-action leading to the login screen.
|
||||
- No misleading claims or functionality promises.
|
||||
|
||||
### 5.2 Login Flow ✅
|
||||
|
||||
- Server URL entry with input validation.
|
||||
- Two clear authentication options (SSO and QR code).
|
||||
- Error handling with user-friendly alert dialogs.
|
||||
- Back navigation available from all auth screens.
|
||||
|
||||
### 5.3 First-Run Permissions ✅
|
||||
|
||||
- Camera permission is requested at the point of use (when tapping Camera button), not at launch.
|
||||
- Photo library permission is requested at the point of use.
|
||||
- Push notification permission is requested after authentication, not before.
|
||||
- All permission requests include clear usage descriptions.
|
||||
|
||||
---
|
||||
|
||||
## 6. Remaining Recommendations
|
||||
|
||||
### 6.1 App Store Connect Preparation
|
||||
|
||||
Before submission, ensure the following are configured in App Store Connect:
|
||||
|
||||
- [ ] **Privacy Policy URL** — must point to the server's `/privacy` endpoint
|
||||
- [ ] **App Store description** — accurate description of features
|
||||
- [ ] **Screenshots** — for iPhone and iPad (since `supportsTablet: true`)
|
||||
- [ ] **App category** — "Business" or "Productivity"
|
||||
- [ ] **Age rating** — complete the questionnaire (likely 4+)
|
||||
- [ ] **Review notes** — provide demo server URL and test credentials for the Apple review team
|
||||
- [ ] **Privacy Nutrition Labels** — declare data types collected (device ID for push notifications, authentication tokens)
|
||||
|
||||
### 6.2 Accessibility Enhancements (Recommended)
|
||||
|
||||
While the app includes `accessibilityRole` and `accessibilityLabel` on interactive elements, consider:
|
||||
|
||||
- Adding `accessibilityHint` to buttons where the action isn't immediately obvious.
|
||||
- Testing with VoiceOver to ensure all screens are fully navigable.
|
||||
- Ensuring all status changes are announced to screen readers.
|
||||
|
||||
### 6.3 iPad Support
|
||||
|
||||
The app declares `supportsTablet: true`. Ensure:
|
||||
|
||||
- UI scales appropriately on iPad screen sizes.
|
||||
- Split View and Slide Over multitasking work correctly.
|
||||
- Touch targets remain accessible on larger screens.
|
||||
|
||||
### 6.4 Localization (Future Enhancement)
|
||||
|
||||
- The app currently uses English-only strings.
|
||||
- For broader App Store reach, consider localizing the app name, description, and in-app strings.
|
||||
|
||||
---
|
||||
|
||||
## 7. Compliance Checklist Summary
|
||||
|
||||
| Area | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| Human Interface Guidelines | ✅ Pass | Ionicons used for platform-consistent iconography |
|
||||
| App Icons & Visual Assets | ✅ Pass | All required assets provided |
|
||||
| Device Data Usage | ✅ Pass | Camera, photos, notifications properly handled |
|
||||
| Privacy Disclosures | ✅ Pass | Info.plist keys and privacy manifest configured |
|
||||
| Background Modes | ✅ Pass | Only `remote-notification` declared |
|
||||
| Restricted APIs | ✅ Pass | No private or deprecated APIs used |
|
||||
| Content Standards | ✅ Pass | No misleading or inappropriate content |
|
||||
| Functionality | ✅ Pass | Complete, functional app experience |
|
||||
| Business Model | ✅ Pass | No IAP conflicts |
|
||||
| Safety & Privacy | ✅ Pass | Account deletion available, privacy policy linked |
|
||||
| Onboarding | ✅ Pass | Clear, permission-respectful first-run experience |
|
||||
| Privacy Manifest | ✅ Pass | Required reason APIs declared |
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Apple App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/)
|
||||
- [Apple Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/)
|
||||
- [Apple Privacy Manifest Requirements](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files)
|
||||
- [App Store Connect Help](https://developer.apple.com/help/app-store-connect/)
|
||||
@@ -15,6 +15,7 @@ This guide explains how to configure authentication for DocuElevate to secure yo
|
||||
| `AUTHENTIK_CLIENT_SECRET` | Client secret for OpenID Connect authentication |
|
||||
| `AUTHENTIK_CONFIG_URL` | OpenID Connect discovery URL |
|
||||
| `OAUTH_PROVIDER_NAME` | Display name for the OAuth provider button |
|
||||
| `SSO_AUTO_LOGIN` | Auto-redirect to SSO login (skips the login page) |
|
||||
|
||||
For a complete list of configuration options, see the [Configuration Guide](ConfigurationGuide.md).
|
||||
|
||||
@@ -24,7 +25,12 @@ DocuElevate supports multiple authentication methods that can be used independen
|
||||
|
||||
1. **Simple Authentication** - Basic username/password authentication managed by DocuElevate
|
||||
2. **OpenID Connect** - Integration with identity providers like Authentik, Keycloak, or Auth0
|
||||
3. **Social Login** - Sign in with Google, Microsoft, Apple, or Dropbox accounts (see [Social Login Setup Guide](SocialLoginSetup.md))
|
||||
3. **Social Login** - Sign in with Google, Microsoft, Apple, Dropbox, or GitHub accounts (see [Social Login Setup Guide](SocialLoginSetup.md))
|
||||
4. **Keycloak SSO** - Self-hosted identity management via Keycloak
|
||||
5. **Generic OAuth2** - Any OAuth2-compatible identity provider
|
||||
6. **SAML2** - Enterprise SSO via SAML 2.0
|
||||
|
||||
> **Admin Connections Page:** You can configure all authentication providers through the admin **Connections** page at `/admin/connections`.
|
||||
|
||||
## Session Security
|
||||
|
||||
@@ -154,6 +160,62 @@ DocuElevate can work with any OpenID Connect-compliant provider, not just Authen
|
||||
OAUTH_PROVIDER_NAME=Auth0
|
||||
```
|
||||
|
||||
## Server-Side Session Management
|
||||
|
||||
DocuElevate supports server-side session tracking. Every login creates a `UserSession` record that can be listed and revoked individually or all at once ("log off everywhere").
|
||||
|
||||
### Configuration
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `SESSION_LIFETIME_DAYS` | Number of days before a session expires | `30` |
|
||||
| `SESSION_LIFETIME_CUSTOM_DAYS` | Override for `SESSION_LIFETIME_DAYS` when set | — |
|
||||
|
||||
### Managing Sessions
|
||||
|
||||
Users can manage their active sessions from the **Profile → Security** section:
|
||||
|
||||
- **View active sessions** — see browser, device, IP address, and last activity for each session.
|
||||
- **Revoke a single session** — immediately invalidate one session.
|
||||
- **Log off everywhere** — revoke all sessions (optionally keeping the current one) and all API tokens at once.
|
||||
|
||||
Expired sessions are automatically cleaned up by a periodic background task.
|
||||
|
||||
### API Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/api/sessions` | List the current user's active sessions |
|
||||
| `DELETE` | `/api/sessions/{id}` | Revoke a single session |
|
||||
| `POST` | `/api/sessions/revoke-all` | Revoke all sessions for the current user |
|
||||
|
||||
## QR Code Login
|
||||
|
||||
QR code login allows users to authenticate a mobile device by scanning a QR code displayed in the web UI, without manually entering credentials on the phone.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. The authenticated web user opens the **QR Login** page and a challenge QR code is displayed.
|
||||
2. The user opens the DocuElevate mobile app and taps **Scan QR Code to Login**, which opens the device camera.
|
||||
3. The mobile app scans the QR code. The QR code contains both the challenge token and the server URL (`docuelevate://qr-login?token=...&server=...`), so there is no need to enter the server URL manually.
|
||||
4. An API token is issued for the mobile device and the web UI is notified via polling.
|
||||
|
||||
> **Note:** The countdown timer on the web page uses server-relative time (TTL in seconds) rather than absolute timestamps, so it works correctly even when the client's clock is not in sync with the server.
|
||||
|
||||
### Configuration
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `QR_LOGIN_CHALLENGE_TTL_SECONDS` | How long a QR challenge is valid (seconds) | `120` |
|
||||
|
||||
### API Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `POST` | `/api/qr-auth/challenge` | Create a new QR login challenge (returns `ttl_seconds` for client countdown) |
|
||||
| `GET` | `/api/qr-auth/challenge/{id}/status` | Poll the status of a challenge |
|
||||
| `POST` | `/api/qr-auth/claim` | Claim a challenge from a mobile device |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Always use HTTPS** in production to protect authentication tokens and passwords
|
||||
|
||||
+315
-7
@@ -11,12 +11,19 @@ Configuration is primarily done through environment variables specified in a `.e
|
||||
| **Variable** | **Description** | **Example** |
|
||||
|------------------------|----------------------------------------------------------|--------------------------------|
|
||||
| `DATABASE_URL` | Path/URL to the SQLite database (or other SQL backend). Use the [Database Wizard](/database-wizard) for guided setup. See [Database Configuration](DatabaseConfiguration.md). | `sqlite:///./app/database.db` |
|
||||
| `DB_POOL_SIZE` | Number of persistent connections in the pool per worker (PostgreSQL/MySQL only; ignored for SQLite). | `10` |
|
||||
| `DB_MAX_OVERFLOW` | Additional connections beyond `DB_POOL_SIZE` under burst load (PostgreSQL/MySQL only). | `20` |
|
||||
| `DB_POOL_TIMEOUT` | Seconds to wait for a pool connection before raising `TimeoutError` (PostgreSQL/MySQL only). | `30` |
|
||||
| `DB_POOL_RECYCLE` | Recycle connections after this many seconds to avoid stale connections (PostgreSQL/MySQL only). | `1800` |
|
||||
| `REDIS_URL` | URL for Redis, used by Celery for broker & result store. | `redis://redis:6379/0` |
|
||||
| `WORKDIR` | Working directory for the application. | `/workdir` |
|
||||
| `GOTENBERG_URL` | Gotenberg PDF processing URL. | `http://gotenberg:3000` |
|
||||
| `EXTERNAL_HOSTNAME` | The external hostname for the application. | `docuelevate.example.com` |
|
||||
| `PUBLIC_BASE_URL` | Full public base URL including scheme (e.g., `https://docuelevate.example.com`). When set, overrides auto-detected URLs used for OAuth redirect URIs. **Required when your reverse proxy does not forward `X-Forwarded-Proto` headers.** | *(not set)* |
|
||||
| `ALLOW_FILE_DELETE` | Enable file deletion in the web interface (`true`/`false`). | `true` |
|
||||
| `COMPLIANCE_ENABLED` | Enable the compliance templates dashboard (GDPR, HIPAA, SOC 2). | `true` |
|
||||
| `FACTORY_RESET_ON_STARTUP` | Wipe all user data on every startup (demo/testing). | `false` |
|
||||
| `ENABLE_FACTORY_RESET` | Show the System Reset page in the admin UI. | `false` |
|
||||
|
||||
### Batch Processing Settings
|
||||
|
||||
@@ -79,6 +86,28 @@ Control how the web UI queues and paces file uploads to avoid overwhelming the b
|
||||
|
||||
**Example**: With `UPLOAD_CONCURRENCY=3` and `UPLOAD_QUEUE_DELAY_MS=500`, a directory of 5,000 files is uploaded ≈ 3 at a time with 500 ms pacing – the backend processes files at its own rate while the queue drains in the background without triggering API rate limits.
|
||||
|
||||
### Per-User Upload Rate Limiting
|
||||
|
||||
Server-side rate limiting that prevents any single user from overwhelming the system with bulk uploads. The limiter uses a Redis-backed sliding window and dynamically adjusts limits based on system health.
|
||||
|
||||
| **Variable** | **Description** | **Default** |
|
||||
|--------------------------------|------------------------------------------------------------------------------------------------------------------------------|-------------|
|
||||
| `UPLOAD_RATE_LIMIT_PER_USER` | Maximum uploads allowed per user within the sliding window. Effective limit may be reduced under load. | `20` |
|
||||
| `UPLOAD_RATE_LIMIT_WINDOW` | Sliding window size in seconds. | `60` |
|
||||
|
||||
**Health-aware dynamic limiting**: The effective per-user limit is automatically reduced when the system is under heavy load:
|
||||
|
||||
| **System condition** | **Effective limit** | **Trigger** |
|
||||
|--------------------------------|---------------------|--------------------------------|
|
||||
| Normal | 100 % of base | Queue < 50, CPU load normal |
|
||||
| Moderate load | 50 % of base | Queue 50–100 or CPU > 1.5× |
|
||||
| High load | 25 % of base | Queue 100–200 or CPU > 2× |
|
||||
| Critical load | 10 % of base | Queue > 200 or CPU > 3× |
|
||||
|
||||
When a user exceeds the limit, the server returns **HTTP 429 Too Many Requests** with a `Retry-After` header. The browser client (see *Client-Side Upload Throttling* above) automatically pauses and retries.
|
||||
|
||||
> **Note**: The limiter fails open — if Redis is unavailable, all uploads are allowed through so that a monitoring outage never blocks document processing.
|
||||
|
||||
### File Upload Size Limits
|
||||
|
||||
**Security Feature**: Control file upload sizes to prevent resource exhaustion attacks. See [SECURITY_AUDIT.md](../SECURITY_AUDIT.md#5-file-upload-size-limits) for security details.
|
||||
@@ -364,6 +393,9 @@ Credentials are encrypted at rest using Fernet encryption.
|
||||
|-------------------------|---------------------------------------------------------------|
|
||||
| `AUTH_ENABLED` | Enable or disable authentication (`true`/`false`). |
|
||||
| `SESSION_SECRET` | Secret key used to encrypt sessions and cookies (at least 32 chars). |
|
||||
| `SESSION_LIFETIME_DAYS` | Number of days before a server-side session expires. Default: `30`. |
|
||||
| `SESSION_LIFETIME_CUSTOM_DAYS` | Override for `SESSION_LIFETIME_DAYS` when set. |
|
||||
| `QR_LOGIN_CHALLENGE_TTL_SECONDS` | How long a QR login challenge is valid (seconds). Default: `120`. |
|
||||
| `ADMIN_USERNAME` | Username for basic authentication (when not using OIDC). |
|
||||
| `ADMIN_PASSWORD` | Password for basic authentication (when not using OIDC). |
|
||||
| `ADMIN_GROUP_NAME` | Group name in OIDC claims that grants admin access. Default: `admin`. |
|
||||
@@ -374,7 +406,7 @@ Credentials are encrypted at rest using Fernet encryption.
|
||||
|
||||
### Social Login Providers
|
||||
|
||||
Social login lets users sign in with their existing Google, Microsoft, Apple, or Dropbox accounts. Each provider is independently enabled and configured. For detailed setup instructions see the [Social Login Setup Guide](SocialLoginSetup.md).
|
||||
Social login lets users sign in with their existing Google, Microsoft, Apple, Dropbox, or GitHub accounts. Each provider is independently enabled and configured. For detailed setup instructions see the [Social Login Setup Guide](SocialLoginSetup.md).
|
||||
|
||||
| **Variable** | **Description** | **Default** |
|
||||
|---|---|---|
|
||||
@@ -393,6 +425,33 @@ Social login lets users sign in with their existing Google, Microsoft, Apple, or
|
||||
| `SOCIAL_AUTH_DROPBOX_ENABLED` | Enable Dropbox Sign-In. | `false` |
|
||||
| `SOCIAL_AUTH_DROPBOX_CLIENT_ID` | Dropbox OAuth2 App Key. | *(empty)* |
|
||||
| `SOCIAL_AUTH_DROPBOX_CLIENT_SECRET` | Dropbox OAuth2 App Secret. | *(empty)* |
|
||||
| `SOCIAL_AUTH_GITHUB_ENABLED` | Enable GitHub Sign-In. | `false` |
|
||||
| `SOCIAL_AUTH_GITHUB_CLIENT_ID` | GitHub OAuth2 client ID from GitHub Developer Settings. | *(empty)* |
|
||||
| `SOCIAL_AUTH_GITHUB_CLIENT_SECRET` | GitHub OAuth2 client secret. | *(empty)* |
|
||||
| `SSO_AUTO_LOGIN` | Automatically redirect to SSO login when authentication is required. | `false` |
|
||||
|
||||
### SSO Providers
|
||||
|
||||
| **Variable** | **Description** | **Default** |
|
||||
|---|---|---|
|
||||
| `SOCIAL_AUTH_KEYCLOAK_ENABLED` | Enable Keycloak SSO. | `false` |
|
||||
| `SOCIAL_AUTH_KEYCLOAK_CLIENT_ID` | Keycloak OAuth2 client ID. | *(empty)* |
|
||||
| `SOCIAL_AUTH_KEYCLOAK_CLIENT_SECRET` | Keycloak OAuth2 client secret. | *(empty)* |
|
||||
| `SOCIAL_AUTH_KEYCLOAK_SERVER_URL` | Keycloak server base URL (e.g. `https://keycloak.example.com`). | *(empty)* |
|
||||
| `SOCIAL_AUTH_KEYCLOAK_REALM` | Keycloak realm name. | *(empty)* |
|
||||
| `SOCIAL_AUTH_GENERIC_OAUTH2_ENABLED` | Enable a generic OAuth2 SSO provider. | `false` |
|
||||
| `SOCIAL_AUTH_GENERIC_OAUTH2_CLIENT_ID` | Generic OAuth2 client ID. | *(empty)* |
|
||||
| `SOCIAL_AUTH_GENERIC_OAUTH2_CLIENT_SECRET` | Generic OAuth2 client secret. | *(empty)* |
|
||||
| `SOCIAL_AUTH_GENERIC_OAUTH2_AUTHORIZE_URL` | Generic OAuth2 authorization URL. | *(empty)* |
|
||||
| `SOCIAL_AUTH_GENERIC_OAUTH2_TOKEN_URL` | Generic OAuth2 token endpoint URL. | *(empty)* |
|
||||
| `SOCIAL_AUTH_GENERIC_OAUTH2_USERINFO_URL` | Generic OAuth2 userinfo endpoint URL. | *(empty)* |
|
||||
| `SOCIAL_AUTH_GENERIC_OAUTH2_SCOPE` | Space-separated list of OAuth2 scopes. | `openid profile email` |
|
||||
| `SOCIAL_AUTH_GENERIC_OAUTH2_NAME` | Display name for the provider button. | `OAuth2` |
|
||||
| `SOCIAL_AUTH_SAML2_ENABLED` | Enable SAML2 SSO authentication. | `false` |
|
||||
| `SOCIAL_AUTH_SAML2_ENTITY_ID` | SAML2 Identity Provider Entity ID. | *(empty)* |
|
||||
| `SOCIAL_AUTH_SAML2_SSO_URL` | SAML2 Identity Provider SSO URL. | *(empty)* |
|
||||
| `SOCIAL_AUTH_SAML2_CERTIFICATE` | SAML2 Identity Provider X.509 certificate (PEM format). | *(empty)* |
|
||||
| `SOCIAL_AUTH_SAML2_NAME` | Display name for the SAML2 provider. | `SAML2` |
|
||||
|
||||
### Multi-User Mode
|
||||
|
||||
@@ -457,6 +516,87 @@ default overage buffer applied across all plans.
|
||||
|
||||
DocuElevate supports HTTP security headers to improve browser-side security. **These headers are disabled by default** since most deployments use a reverse proxy (Traefik, Nginx, etc.) that already adds them. Enable only if deploying directly without a reverse proxy. See [Deployment Guide - Security Headers](DeploymentGuide.md#security-headers) for detailed configuration examples.
|
||||
|
||||
### Application Logging
|
||||
|
||||
DocuElevate uses Python's standard `logging` module. Two environment variables control log verbosity:
|
||||
|
||||
| **Variable** | **Description** | **Default** |
|
||||
|-------------|----------------|-------------|
|
||||
| `LOG_LEVEL` | Root logger level. Accepts standard Python level names: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`. | `INFO` |
|
||||
| `DEBUG` | Enable debug mode. When `true` **and** `LOG_LEVEL` is **not** explicitly set, the effective log level is automatically lowered to `DEBUG`. | `false` |
|
||||
|
||||
**Precedence rules (standard behaviour):**
|
||||
|
||||
1. If `LOG_LEVEL` is explicitly set, it always wins — regardless of `DEBUG`.
|
||||
2. If only `DEBUG=true` is set (no `LOG_LEVEL`), the effective level becomes `DEBUG`.
|
||||
3. If neither is set, the default level is `INFO`.
|
||||
|
||||
```bash
|
||||
# Typical production (default)
|
||||
# LOG_LEVEL=INFO
|
||||
|
||||
# Quick debug mode — sets level to DEBUG automatically
|
||||
DEBUG=true
|
||||
|
||||
# Explicit level override (DEBUG flag is ignored for level selection)
|
||||
LOG_LEVEL=WARNING
|
||||
```
|
||||
|
||||
> **Tip:** At `DEBUG` level, noisy third-party libraries (httpx, authlib, urllib3, etc.) are automatically pinned to `WARNING` so that application debug output remains readable.
|
||||
|
||||
#### Structured JSON Logging
|
||||
|
||||
Set `LOG_FORMAT=json` to emit structured JSON lines on stdout — one JSON object per log message. This is the standard format for log collectors and SIEM tools:
|
||||
|
||||
| **Variable** | **Description** | **Default** |
|
||||
|-------------|----------------|-------------|
|
||||
| `LOG_FORMAT` | Log output format: `text` (human-readable) or `json` (structured JSON lines). | `text` |
|
||||
|
||||
Each JSON log line contains: `timestamp` (ISO 8601), `level`, `logger`, `message`, `module`, `funcName`, `lineno`, and `exc_info` (when an exception is logged).
|
||||
|
||||
```bash
|
||||
# Enable JSON logging for SIEM / log aggregation
|
||||
LOG_FORMAT=json
|
||||
```
|
||||
|
||||
**Example JSON output:**
|
||||
```json
|
||||
{"timestamp": "2025-03-16T09:18:05.192000+00:00", "level": "INFO", "logger": "app.auth", "message": "[SECURITY] OAUTH_LOGIN_SUCCESS user=alice@example.com admin=False", "module": "auth", "funcName": "oauth_callback", "lineno": 654}
|
||||
```
|
||||
|
||||
**Compatible with:**
|
||||
- **Grafana Loki** — Promtail scrapes JSON from Docker stdout
|
||||
- **Splunk** — Universal Forwarder or HEC with JSON sourcetype
|
||||
- **ELK / OpenSearch** — Filebeat with JSON codec
|
||||
- **Datadog** — Agent auto-parses JSON logs
|
||||
- **Fluentd / Vector** — JSON input plugin
|
||||
- **Docker log drivers** — `--log-driver=json-file` (default) preserves structure
|
||||
|
||||
#### Syslog Forwarding (Application Logs)
|
||||
|
||||
For traditional (non-container) deployments, application logs can be forwarded directly to a syslog receiver. This is **separate** from audit-log SIEM forwarding (see below) — it sends _every_ Python log message, not just audit events.
|
||||
|
||||
| **Variable** | **Description** | **Default** |
|
||||
|-------------|----------------|-------------|
|
||||
| `LOG_SYSLOG_ENABLED` | Forward application logs to a syslog receiver in addition to stdout. | `false` |
|
||||
| `LOG_SYSLOG_HOST` | Hostname or IP of the syslog receiver. | `localhost` |
|
||||
| `LOG_SYSLOG_PORT` | Port of the syslog receiver. | `514` |
|
||||
| `LOG_SYSLOG_PROTOCOL` | Protocol: `udp` or `tcp`. | `udp` |
|
||||
|
||||
```bash
|
||||
# Forward all application logs to syslog
|
||||
LOG_SYSLOG_ENABLED=true
|
||||
LOG_SYSLOG_HOST=syslog.internal.example.com
|
||||
LOG_SYSLOG_PORT=514
|
||||
LOG_SYSLOG_PROTOCOL=udp
|
||||
|
||||
# Combine with JSON format for structured syslog messages
|
||||
LOG_FORMAT=json
|
||||
LOG_SYSLOG_ENABLED=true
|
||||
```
|
||||
|
||||
> **Note:** When `LOG_FORMAT=json`, syslog messages are also sent as JSON. When `LOG_FORMAT=text`, syslog messages use the standard `name - level - message` format.
|
||||
|
||||
### Audit Logging
|
||||
|
||||
DocuElevate provides comprehensive audit logging that records significant actions (logins, document CRUD, settings changes) to an append-only database table. Every entry captures the timestamp, user, action, resource, client IP, and optional JSON details.
|
||||
@@ -652,7 +792,7 @@ SECURITY_HEADER_CSP_VALUE="default-src 'self'; script-src 'self'; style-src 'sel
|
||||
SECURITY_HEADER_CSP_VALUE="default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline';"
|
||||
```
|
||||
|
||||
**Note:** The default policy includes `'unsafe-inline'` for compatibility with Tailwind CSS and inline JavaScript. For stricter security, use nonces or hashes.
|
||||
**Note:** The default policy includes `'unsafe-inline'` for compatibility with inline JavaScript. Tailwind CSS v3 is compiled at build time into a static file served from `'self'`, so no external style CDN is needed.
|
||||
|
||||
#### X-Frame-Options
|
||||
|
||||
@@ -853,6 +993,48 @@ OPENAI_API_KEY=sk-ant-... # passed as the api_key to LiteLLM
|
||||
|
||||
---
|
||||
|
||||
### Document Translation
|
||||
|
||||
After processing, DocuElevate can automatically translate a document's extracted text into a configurable *default language* (e.g. English). This reference translation is stored alongside the original text so users always have a version in a language they understand.
|
||||
|
||||
Other languages are translated **on the fly** via the AI provider and are not persisted.
|
||||
|
||||
#### Settings
|
||||
|
||||
| **Variable** | **Description** | **Default** |
|
||||
|------------------------------|-----------------------------------------------------------------------------------------------------------|-------------|
|
||||
| `DEFAULT_DOCUMENT_LANGUAGE` | ISO 639-1 code for the default translation target (e.g. `en`, `de`, `fr`). Documents whose detected language differs are automatically translated into this language after processing. | `en` |
|
||||
|
||||
Each user can override this global default in their profile (`UserProfile.default_document_language`).
|
||||
|
||||
#### How It Works
|
||||
|
||||
1. During metadata extraction the AI detects the document language (stored as `detected_language` on the file record).
|
||||
2. If the detected language differs from the default target language, a background Celery task (`translate_to_default_language`) translates the extracted text.
|
||||
3. The translated text is persisted in `default_language_text` and the target code in `default_language_code`.
|
||||
4. The file detail view shows both the original text and the default-language version.
|
||||
5. Users can also request on-the-fly translations to any language via the **Translate** dropdown.
|
||||
|
||||
#### API Endpoints
|
||||
|
||||
| **Endpoint** | **Method** | **Description** |
|
||||
|-----------------------------------------------|------------|------------------------------------------------------------------------|
|
||||
| `/api/files/{id}/translation/default` | GET | Returns the persisted default-language translation (404 if unavailable)|
|
||||
| `/api/files/{id}/translate?lang=xx` | GET | On-the-fly translation to any ISO 639-1 language code |
|
||||
| `/files/{id}/text/default-language` | GET | View endpoint returning the default-language text as JSON |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
# Get the stored English translation of a German document
|
||||
curl http://localhost:8000/api/files/42/translation/default
|
||||
|
||||
# Translate on the fly to French
|
||||
curl "http://localhost:8000/api/files/42/translate?lang=fr"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### OCR Providers
|
||||
|
||||
DocuElevate supports multiple OCR engines that can be used individually or in combination. Configure the list of active providers with `OCR_PROVIDERS` and tune each provider with the settings below.
|
||||
@@ -1161,6 +1343,20 @@ For detailed setup instructions, see the [Google Drive Setup Guide](GoogleDriveS
|
||||
|
||||
For detailed setup instructions, see the [OneDrive Setup Guide](OneDriveSetup.md).
|
||||
|
||||
### SharePoint Online
|
||||
|
||||
| **Variable** | **Description** |
|
||||
|---------------------------------|-------------------------------------------------------|
|
||||
| `SHAREPOINT_CLIENT_ID` | Azure AD application client ID |
|
||||
| `SHAREPOINT_CLIENT_SECRET` | Azure AD application client secret |
|
||||
| `SHAREPOINT_TENANT_ID` | Azure AD tenant ID (use "common" for multi-tenant apps) |
|
||||
| `SHAREPOINT_REFRESH_TOKEN` | OAuth 2.0 refresh token |
|
||||
| `SHAREPOINT_SITE_URL` | SharePoint site URL (e.g. `https://tenant.sharepoint.com/sites/sitename`) |
|
||||
| `SHAREPOINT_DOCUMENT_LIBRARY` | Document library name (default: `Documents`) |
|
||||
| `SHAREPOINT_FOLDER_PATH` | Subfolder path inside the document library |
|
||||
|
||||
SharePoint uses the same Microsoft Graph API as OneDrive. See the [OneDrive Setup Guide](OneDriveSetup.md) for Azure AD app registration instructions — the same app registration can be reused for SharePoint with the `Sites.ReadWrite.All` permission.
|
||||
|
||||
### Amazon S3
|
||||
|
||||
| **Variable** | **Description** |
|
||||
@@ -1206,6 +1402,9 @@ For detailed setup instructions, see the [Amazon S3 Setup Guide](AmazonS3Setup.m
|
||||
| `NOTIFY_ON_USER_SIGNUP` | Send admin notification when a new user signs up (`True`/`False`, default `True`) |
|
||||
| `NOTIFY_ON_PLAN_CHANGE` | Send admin notification when a user changes their subscription plan (`True`/`False`, default `True`) |
|
||||
| `NOTIFY_ON_PAYMENT_ISSUE` | Send admin notification when a payment issue is reported for a user (`True`/`False`, default `True`) |
|
||||
| `TELEGRAM_ENABLED` | Enable Telegram bot notifications. | `false` |
|
||||
| `TELEGRAM_BOT_TOKEN` | Telegram Bot API token from @BotFather. | *(empty)* |
|
||||
| `TELEGRAM_CHAT_ID` | Telegram chat ID to send notifications to. | *(empty)* |
|
||||
|
||||
#### User-Event Notifications
|
||||
|
||||
@@ -1285,6 +1484,26 @@ Configurations are stored in the database and managed through the API (see [API
|
||||
|
||||
Webhook URLs, secrets, and subscribed events are configured per-webhook via the `/api/webhooks/` endpoints (admin access required). Each delivery includes an optional HMAC-SHA256 signature for verification and is retried with exponential backoff on failure.
|
||||
|
||||
### Automation Hooks (Zapier / Make.com)
|
||||
|
||||
Automation hooks enable integration with external automation platforms such as
|
||||
[Zapier](https://zapier.com) and [Make.com](https://make.com) (formerly Integromat).
|
||||
|
||||
| **Variable** | **Description** | **Default** |
|
||||
|----------------------------|------------------------------------------------------------------------------------------------|-------------|
|
||||
| `AUTOMATION_HOOKS_ENABLED` | Enable or disable Zapier / Make.com automation hook subscriptions and delivery (`True`/`False`) | `True` |
|
||||
|
||||
When enabled, external platforms can:
|
||||
|
||||
- **Subscribe** to DocuElevate events via `POST /api/automation/hooks/subscribe` (outgoing triggers)
|
||||
- **Send documents** to DocuElevate via `POST /api/automation/actions/upload` (incoming actions)
|
||||
- **Discover fields** via `GET /api/automation/triggers/sample/{event}` (Zapier field mapping)
|
||||
|
||||
Automation hooks share the same event types as webhooks (`document.uploaded`, `document.processed`,
|
||||
`document.failed`, `user.signup`, `user.plan_changed`, `user.payment_issue`) and use a flat
|
||||
Zapier-compatible JSON payload format. See the [API docs](API.md#automation-zapier--makecom) for
|
||||
endpoint details and payload examples.
|
||||
|
||||
### Backup & Restore
|
||||
|
||||
DocuElevate automatically backs up the database on a scheduled basis.
|
||||
@@ -1386,6 +1605,8 @@ No additional configuration is required — the auto-fill uses the authenticated
|
||||
|
||||
DocuElevate integrates with [Sentry](https://sentry.io) for real-time error tracking and performance monitoring. See [SentrySetup.md](./SentrySetup.md) for a full setup guide.
|
||||
|
||||
### Server-side (Python SDK)
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `SENTRY_DSN` | Sentry DSN URL. When set, error reporting and performance tracing are enabled automatically. Leave blank to disable. | *(unset)* |
|
||||
@@ -1394,18 +1615,33 @@ DocuElevate integrates with [Sentry](https://sentry.io) for real-time error trac
|
||||
| `SENTRY_PROFILES_SAMPLE_RATE` | Fraction of profiled transactions sent to Sentry (0.0 – 1.0). Only active when traces > 0. | `0.0` |
|
||||
| `SENTRY_SEND_DEFAULT_PII` | Attach PII (IP addresses, user agents) to Sentry events. Disabled by default for GDPR/CCPA compliance. | `false` |
|
||||
|
||||
### Browser SDK (JavaScript)
|
||||
|
||||
The Sentry Browser SDK is loaded automatically on every rendered page when `SENTRY_DSN` is set. The same DSN is used for both server and browser — the DSN is a *public* key in Sentry's security model and is intentionally embedded in client-side code.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `SENTRY_JS_TRACES_SAMPLE_RATE` | Fraction of browser page-loads captured for client-side performance tracing (0.0 – 1.0). | `0.0` |
|
||||
| `SENTRY_JS_REPLAY_SESSION_SAMPLE_RATE` | Fraction of sessions recorded by [Sentry Session Replay](https://docs.sentry.io/product/session-replay/) (0.0 – 1.0). | `0.0` |
|
||||
| `SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE` | Fraction of error sessions captured with session replay context (0.0 – 1.0). | `0.1` |
|
||||
|
||||
```bash
|
||||
# Minimal example
|
||||
# Minimal example (server + browser)
|
||||
SENTRY_DSN=https://<key>@o<org>.ingest.sentry.io/<project>
|
||||
SENTRY_ENVIRONMENT=production
|
||||
|
||||
# Optional tuning
|
||||
# Optional server-side tuning
|
||||
SENTRY_TRACES_SAMPLE_RATE=0.1
|
||||
SENTRY_PROFILES_SAMPLE_RATE=0.0
|
||||
SENTRY_SEND_DEFAULT_PII=false
|
||||
|
||||
# Optional browser-side tuning
|
||||
SENTRY_JS_TRACES_SAMPLE_RATE=0.1
|
||||
SENTRY_JS_REPLAY_SESSION_SAMPLE_RATE=0.0
|
||||
SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE=0.1
|
||||
```
|
||||
|
||||
> **Note:** Sentry is completely opt-in — if `SENTRY_DSN` is not set, the SDK is never initialised and no data leaves your infrastructure.
|
||||
> **Note:** Sentry is completely opt-in — if `SENTRY_DSN` is not set, neither SDK is initialised and no data leaves your infrastructure.
|
||||
|
||||
## Duplicate Document Detection
|
||||
|
||||
@@ -1413,14 +1649,30 @@ DocuElevate detects and flags documents that share the same content, even if the
|
||||
|
||||
### Exact Duplicate Detection (SHA-256)
|
||||
|
||||
When `ENABLE_DEDUPLICATION=True` (the default), each new document is hashed with SHA-256 before processing begins. If the hash matches an existing file record the new document is stored as a duplicate (`is_duplicate=True`, `duplicate_of_id=<original_id>`) and no further processing is performed.
|
||||
When `ENABLE_DEDUPLICATION=True` (the default), each new document is hashed with SHA-256 before processing begins. If the hash matches an existing file record the upload is rejected immediately — no processing task is created, and the temporary file is removed from disk. The `/api/ui-upload` response returns `"status": "duplicate"` together with a `duplicate_of` object that identifies the original file.
|
||||
|
||||
If the same file somehow reaches the Celery worker (e.g. via a watch-folder ingest) it is still caught there and stored as a duplicate (`is_duplicate=True`, `duplicate_of_id=<original_id>`) with no further processing.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `ENABLE_DEDUPLICATION` | Hash-based exact duplicate detection on ingest. | `True` |
|
||||
| `SHOW_DEDUPLICATION_STEP` | Show the "Check for Duplicates" step in the processing timeline UI. | `True` |
|
||||
|
||||
An immediate duplicate warning is also included in the `/api/ui-upload` JSON response so the frontend can alert the user before the pipeline completes.
|
||||
When the upload is an exact duplicate the `/api/ui-upload` response looks like:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "duplicate",
|
||||
"original_filename": "invoice.pdf",
|
||||
"stored_filename": "abc-123.pdf",
|
||||
"duplicate_of": {
|
||||
"duplicate_type": "exact",
|
||||
"original_file_id": 42,
|
||||
"original_filename": "invoice.pdf",
|
||||
"message": "This file is an exact duplicate of an already-processed document. It has not been queued for processing again."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Near-Duplicate Detection (Content Similarity)
|
||||
|
||||
@@ -1500,6 +1752,7 @@ For example:
|
||||
| S3 | `docs/uploads/` | `docs/uploads/pdfa/` |
|
||||
| Nextcloud | `/Files` | `/Files/pdfa` |
|
||||
| OneDrive | `Documents/Uploads` | `Documents/Uploads/pdfa` |
|
||||
| SharePoint | `Uploads` | `Uploads/pdfa` |
|
||||
| Google Drive | *(folder ID)* | `GOOGLE_DRIVE_PDFA_FOLDER_ID` |
|
||||
|
||||
Set `PDFA_UPLOAD_FOLDER` to an empty string to upload PDF/A files into the
|
||||
@@ -1706,6 +1959,15 @@ ONEDRIVE_TENANT_ID=common
|
||||
ONEDRIVE_REFRESH_TOKEN=your_refresh_token
|
||||
ONEDRIVE_FOLDER_PATH=Documents/Uploads
|
||||
|
||||
# SharePoint Online
|
||||
SHAREPOINT_CLIENT_ID=12345678-1234-1234-1234-123456789012
|
||||
SHAREPOINT_CLIENT_SECRET=your_client_secret
|
||||
SHAREPOINT_TENANT_ID=your-tenant-id
|
||||
SHAREPOINT_REFRESH_TOKEN=your_refresh_token
|
||||
SHAREPOINT_SITE_URL=https://tenant.sharepoint.com/sites/sitename
|
||||
SHAREPOINT_DOCUMENT_LIBRARY=Documents
|
||||
SHAREPOINT_FOLDER_PATH=Uploads
|
||||
|
||||
# Amazon S3
|
||||
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
|
||||
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
||||
@@ -1733,6 +1995,52 @@ BACKUP_RETAIN_WEEKLY=13
|
||||
|
||||
You can choose which document storage services to use by only including the relevant environment variables. For example, if you only want to use Dropbox, include only the Dropbox variables and omit the Paperless NGX and Nextcloud variables.
|
||||
|
||||
## System Reset / Factory Reset
|
||||
|
||||
DocuElevate provides two mechanisms for resetting the system to a clean state. Both are **disabled by default** and must be explicitly enabled.
|
||||
|
||||
### Automatic Reset on Startup
|
||||
|
||||
Set `FACTORY_RESET_ON_STARTUP=true` to wipe all user data (database rows and work-files) every time the application starts. This is useful for demo, testing, or ephemeral environments where you always want a fresh instance.
|
||||
|
||||
```dotenv
|
||||
FACTORY_RESET_ON_STARTUP=true
|
||||
```
|
||||
|
||||
> **Warning:** This destroys all documents, processing history, audit logs, and backups on every restart. Application settings and configuration are preserved.
|
||||
|
||||
### Admin UI Reset Page
|
||||
|
||||
Set `ENABLE_FACTORY_RESET=true` to display the **System Reset** page in the admin navigation menu. From this page, administrators can:
|
||||
|
||||
| Action | Confirmation | Description |
|
||||
|--------|-------------|-------------|
|
||||
| **Full Reset** | Type `DELETE` | Wipes all database rows and work-files. The system returns to its initial state. |
|
||||
| **Reset & Re-import** | Type `REIMPORT` | Copies original files to a `reimport/` folder inside the workdir, wipes everything, then configures the reimport folder as a watch folder so files are automatically re-ingested with the same processing pipeline, rate limits, and backoff strategy as regular uploads. |
|
||||
|
||||
```dotenv
|
||||
ENABLE_FACTORY_RESET=true
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
|
||||
When `ENABLE_FACTORY_RESET=true`, two admin-only API endpoints are available:
|
||||
|
||||
- `POST /api/admin/system-reset/full` — body: `{"confirmation": "DELETE"}`
|
||||
- `POST /api/admin/system-reset/reimport` — body: `{"confirmation": "REIMPORT"}`
|
||||
- `GET /api/admin/system-reset/status` — returns current feature-flag state
|
||||
|
||||
### What Gets Deleted
|
||||
|
||||
| Deleted | Preserved |
|
||||
|---------|-----------|
|
||||
| All document records (`files` table) | Application settings (`application_settings` table) |
|
||||
| Processing logs and steps | User accounts and profiles |
|
||||
| Audit logs | Subscription plans |
|
||||
| Backup records | Pipelines and scheduled jobs |
|
||||
| Original, processed, and temporary files | The workdir directory itself |
|
||||
| Watch-folder caches and ingestion state | OAuth and integration configuration |
|
||||
|
||||
## Configuration File Location
|
||||
|
||||
The `.env` file should be placed at the root of the project directory. When using Docker Compose, you can reference it with the `env_file` directive in your `docker-compose.yml`.
|
||||
|
||||
@@ -11,7 +11,7 @@ Credentials fall into two categories:
|
||||
| Category | Examples |
|
||||
|---|---|
|
||||
| **API keys** | OpenAI API key, Azure AI key, Paperless-ngx API token, AWS access keys |
|
||||
| **OAuth tokens / secrets** | Dropbox, Google Drive, OneDrive, Authentik client secrets and refresh tokens |
|
||||
| **OAuth tokens / secrets** | Dropbox, Google Drive, OneDrive, SharePoint, Authentik client secrets and refresh tokens |
|
||||
| **Passwords** | Admin password, Nextcloud, Email (SMTP), IMAP, FTP, SFTP, WebDAV |
|
||||
| **Private keys** | SFTP private key and passphrase |
|
||||
|
||||
@@ -119,6 +119,15 @@ For service-account credentials (`google_drive_credentials_json`):
|
||||
4. Re-authorize via the OAuth flow to get a fresh `onedrive_refresh_token`.
|
||||
5. Delete the old client secret in Azure.
|
||||
|
||||
### SharePoint (Microsoft OAuth)
|
||||
|
||||
1. SharePoint uses the same Azure AD app registration as OneDrive.
|
||||
2. In **Azure App Registrations**, navigate to **Certificates & secrets** for your app.
|
||||
3. Add a new client secret.
|
||||
4. Update `sharepoint_client_secret` in DocuElevate.
|
||||
5. Re-authorize via the OAuth flow to get a fresh `sharepoint_refresh_token`.
|
||||
6. Delete the old client secret in Azure.
|
||||
|
||||
### Authentik (OIDC)
|
||||
|
||||
1. In your Authentik admin panel, navigate to the DocuElevate application and regenerate the client secret.
|
||||
|
||||
@@ -287,6 +287,27 @@ alembic revision --autogenerate -m "describe your change"
|
||||
|
||||
Review the generated file in `migrations/versions/` before applying it.
|
||||
|
||||
> **Tip:** For detailed guidance on naming conventions, idempotent patterns, parallel-branch workflows, and resolving merge conflicts, see the [Migration Workflow Guide](MigrationWorkflow.md).
|
||||
|
||||
### Validating the Migration Chain
|
||||
|
||||
A CI check and pre-commit hook validate that the migration chain has no broken
|
||||
references, duplicate revisions, or diverged heads. Run the check locally:
|
||||
|
||||
```bash
|
||||
python scripts/check_alembic_migrations.py
|
||||
python scripts/check_alembic_migrations.py --verbose # extra detail
|
||||
```
|
||||
|
||||
If you see **"Multiple migration heads detected"**, two branches added
|
||||
migrations from the same parent. Create a merge migration:
|
||||
|
||||
```bash
|
||||
alembic merge heads -m "merge_parallel_branches"
|
||||
```
|
||||
|
||||
For a complete walk-through, see the [Migration Workflow Guide](MigrationWorkflow.md).
|
||||
|
||||
### Automating Migrations in Docker Compose
|
||||
|
||||
Add a short-lived `migrate` service that runs before the API and Worker:
|
||||
@@ -316,18 +337,26 @@ The Helm chart includes a pre-install and pre-upgrade Job hook that runs `alembi
|
||||
|
||||
## Connection Pooling
|
||||
|
||||
SQLAlchemy manages a connection pool automatically. The defaults are suitable for most deployments. For high-concurrency or Kubernetes deployments you may want to tune:
|
||||
SQLAlchemy manages a connection pool automatically. DocuElevate selects the pool
|
||||
strategy based on the database backend:
|
||||
|
||||
- **SQLite** — uses `NullPool` (a fresh connection per request, closed immediately).
|
||||
This avoids the `QueuePool limit reached` `TimeoutError` that can occur under
|
||||
concurrent load because SQLite does not benefit from persistent connection pooling.
|
||||
- **PostgreSQL / MySQL** — uses a bounded `QueuePool` whose size is configurable
|
||||
via environment variables.
|
||||
|
||||
```bash
|
||||
# Optional — these are set via environment variables if you extend app/database.py
|
||||
# Typical production values:
|
||||
DB_POOL_SIZE=10 # Number of persistent connections per worker
|
||||
DB_MAX_OVERFLOW=20 # Additional connections allowed beyond pool_size
|
||||
DB_POOL_TIMEOUT=30 # Seconds to wait for a connection from the pool
|
||||
DB_POOL_RECYCLE=1800 # Recycle connections after 30 minutes (avoids stale connections)
|
||||
# Tune these for PostgreSQL / MySQL (ignored when using SQLite):
|
||||
DB_POOL_SIZE=10 # Number of persistent connections per worker (default: 10)
|
||||
DB_MAX_OVERFLOW=20 # Additional connections allowed beyond pool_size (default: 20)
|
||||
DB_POOL_TIMEOUT=30 # Seconds to wait for a connection from the pool (default: 30)
|
||||
DB_POOL_RECYCLE=1800 # Recycle connections after 30 minutes (default: 1800)
|
||||
```
|
||||
|
||||
> **Note:** These environment variables are not exposed in the default `app/config.py`. If you need to tune them, extend the database engine creation in `app/database.py`.
|
||||
All backends also enable `pool_pre_ping`, which sends a lightweight health-check
|
||||
before each connection is handed out. This detects stale or dropped connections
|
||||
and transparently reconnects.
|
||||
|
||||
For **PgBouncer** (external connection pooling), point `DATABASE_URL` at your PgBouncer instance and use transaction-mode pooling:
|
||||
|
||||
@@ -491,4 +520,13 @@ Then retry `alembic upgrade head`.
|
||||
|
||||
Either increase `max_connections` in `postgresql.conf` or add PgBouncer in front of PostgreSQL. The default PostgreSQL `max_connections` is `100`; reduce `DB_POOL_SIZE` per worker to stay within this limit.
|
||||
|
||||
### "QueuePool limit reached" TimeoutError (SQLite)
|
||||
|
||||
If you see `TimeoutError: QueuePool limit of size 5 overflow 10 reached`, your
|
||||
deployment is still running an older version of DocuElevate that used a bounded
|
||||
connection pool for SQLite. Upgrade to the latest release — SQLite now uses
|
||||
`NullPool`, which eliminates this error entirely. If you are already on the
|
||||
latest version and are still seeing pool exhaustion, ensure you are not
|
||||
overriding the engine creation manually.
|
||||
|
||||
For more help, see the [Troubleshooting Guide](Troubleshooting.md).
|
||||
|
||||
+11
-7
@@ -19,7 +19,7 @@ This guide covers all supported deployment methods for DocuElevate.
|
||||
- Access to required external services (if configured):
|
||||
- AI provider API key (OpenAI, Anthropic, Gemini, or other configured provider)
|
||||
- Azure Document Intelligence
|
||||
- Dropbox, Google Drive, OneDrive, S3, or other storage APIs
|
||||
- Dropbox, Google Drive, OneDrive, SharePoint, S3, or other storage APIs
|
||||
- SMTP / IMAP server (for email processing)
|
||||
- Notification services (Discord, Telegram, etc.)
|
||||
|
||||
@@ -349,16 +349,18 @@ workdir:
|
||||
|
||||
## Scaling
|
||||
|
||||
DocuElevate is designed for horizontal scaling. Both API and worker pods are stateless and can be scaled independently.
|
||||
|
||||
### Docker Compose
|
||||
|
||||
Add more worker containers:
|
||||
Scale workers (task processing) and API pods (request handling) independently:
|
||||
|
||||
```yaml
|
||||
worker:
|
||||
deploy:
|
||||
replicas: 3
|
||||
```bash
|
||||
docker compose up -d --scale worker=3 --scale api=2
|
||||
```
|
||||
|
||||
> **Note:** The `beat` service (Celery Beat scheduler) must always run as exactly **one** instance. Do not scale it. It publishes periodic tasks to the Redis broker; workers pick them up.
|
||||
|
||||
### Kubernetes / Helm
|
||||
|
||||
Enable HPA:
|
||||
@@ -377,13 +379,15 @@ worker:
|
||||
maxReplicas: 10
|
||||
```
|
||||
|
||||
The Helm chart deploys a separate **beat** pod (always 1 replica, `Recreate` strategy) so that scheduled tasks are never duplicated when workers scale.
|
||||
|
||||
---
|
||||
|
||||
## Monitoring
|
||||
|
||||
- **Docker Compose**: `docker-compose logs -f`, `docker stats`
|
||||
- **Kubernetes**: `kubectl logs -l app.kubernetes.io/component=api -f`
|
||||
- **Prometheus / Grafana**: Scrape the `/api/health` endpoint for readiness; add custom metrics as needed.
|
||||
- **Prometheus / Grafana**: Scrape the `/api/diagnostic/healthz/ready` endpoint for readiness; add custom metrics as needed.
|
||||
- **Uptime Kuma**: Set `UPTIME_KUMA_URL` to your push URL for heartbeat monitoring.
|
||||
|
||||
---
|
||||
|
||||
+28
-3
@@ -28,9 +28,10 @@ End users authorize their own Dropbox integration from the **Integrations** dash
|
||||
1. Navigate to `/integrations` and click **+ Add Destination** (or **+ Add Source** for Watch Folder).
|
||||
2. Create a Dropbox destination integration (or a Watch Folder with `source_type = dropbox`).
|
||||
3. Click the **Authorize** button next to the integration — it links directly to the OAuth wizard pre-loaded with your integration's configuration.
|
||||
4. Enter your Dropbox App Key and App Secret in the wizard (or use the global admin credentials if pre-configured).
|
||||
4. If the administrator has configured system-wide Dropbox app credentials (`DROPBOX_APP_KEY` / `DROPBOX_APP_SECRET`), the wizard defaults to using them — no need to register your own Dropbox app. Uncheck the toggle to use custom credentials if needed.
|
||||
5. Click **Start Authentication Flow**, authorize access in Dropbox, and the refresh token is automatically saved to your personal integration record.
|
||||
6. The page redirects back to `/integrations` on success. Re-authorization is available at any time via the **Re-Authorize** button.
|
||||
6. After authorization, an interactive **folder browser** lets you select the target folder directly from your Dropbox — no need to manually type folder paths.
|
||||
7. The page redirects to `/integrations` on success. Re-authorization is available at any time via the **Re-Authorize** button.
|
||||
|
||||
> **Note:** Your credentials are stored encrypted per-integration and are never mixed with other users' data. Each user can have multiple Dropbox integrations with independent tokens.
|
||||
|
||||
@@ -128,7 +129,31 @@ If you encounter issues with Dropbox integration:
|
||||
1. **Authentication Errors**: Make sure your App Key and App Secret are correct
|
||||
2. **Token Expired**: Click "Refresh Token" button on the setup page to obtain a new token
|
||||
3. **Folder Permissions**: Ensure your app has the correct permissions enabled for file operations
|
||||
4. **Invalid Redirect URI**: Verify that the redirect URI in your app settings matches the one used in the authentication flow
|
||||
4. **Invalid Redirect URI**: See section below for the most common cause and fix.
|
||||
5. **Rate Limiting**: Dropbox API has rate limits; if exceeded, wait and try again
|
||||
|
||||
### Fixing "Invalid redirect_uri" Error
|
||||
|
||||
This error appears on the Dropbox authorization page when the redirect URI in the OAuth request does not match any URI registered in your Dropbox app console.
|
||||
|
||||
**Most common cause**: The application is deployed behind a reverse proxy (Traefik, Nginx, Caddy) that does **not** forward the `X-Forwarded-Proto: https` header to DocuElevate. Without this header, the server cannot determine that it is being accessed over HTTPS and may construct an `http://` redirect URI, while the registered URI in Dropbox is `https://`.
|
||||
|
||||
**Fix**:
|
||||
|
||||
Option 1 – Configure your proxy to forward `X-Forwarded-Proto`:
|
||||
|
||||
```nginx
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
```
|
||||
|
||||
Option 2 – Set `PUBLIC_BASE_URL` in your environment (recommended for most deployments):
|
||||
|
||||
```bash
|
||||
PUBLIC_BASE_URL=https://docuelevate.example.com
|
||||
```
|
||||
|
||||
When `PUBLIC_BASE_URL` is set, DocuElevate uses it directly for all OAuth redirect URIs instead of trying to infer the scheme from request headers. This is the most reliable option.
|
||||
|
||||
After setting `PUBLIC_BASE_URL`, ensure the Dropbox app console redirect URI matches exactly (e.g., `https://docuelevate.example.com/dropbox-callback`). The setup wizard at `/dropbox-setup` will show you the exact URI to register.
|
||||
|
||||
For more general configuration issues, see the [Configuration Troubleshooting Guide](ConfigurationTroubleshooting.md).
|
||||
|
||||
@@ -30,7 +30,7 @@ End users can authorize their own Google Drive integration directly from the **I
|
||||
1. Navigate to `/integrations` and click **+ Add Destination** (or **+ Add Source** for Watch Folder).
|
||||
2. Create a Google Drive destination integration (or a Watch Folder with `source_type = google_drive`).
|
||||
3. Click the **Authorize** button — it opens the OAuth wizard pre-loaded with your integration's configuration.
|
||||
4. Enter your Google OAuth Client ID and Client Secret in the wizard.
|
||||
4. If the administrator has configured system-wide Google Drive app credentials (`GOOGLE_DRIVE_CLIENT_ID` / `GOOGLE_DRIVE_CLIENT_SECRET`), the wizard defaults to using them — no need to register your own Google Cloud app. Uncheck the toggle to use custom credentials if needed.
|
||||
5. Click **Start Authentication Flow** and authorize access in Google.
|
||||
6. Credentials are saved automatically to your personal integration record; the page redirects back to `/integrations`.
|
||||
7. Re-authorization is available at any time via the **Re-Authorize** button.
|
||||
|
||||
@@ -373,6 +373,8 @@ worker:
|
||||
replicaCount: 4
|
||||
```
|
||||
|
||||
> **Beat scheduler:** The Helm chart deploys a dedicated `beat` pod (always exactly 1 replica with `Recreate` strategy) that publishes periodic tasks to the Redis broker. Workers consume these tasks — scaling workers does **not** duplicate scheduled jobs.
|
||||
|
||||
### Horizontal Pod Autoscaler
|
||||
|
||||
```yaml
|
||||
@@ -433,24 +435,30 @@ externalRedis:
|
||||
|
||||
### Kubernetes Probes
|
||||
|
||||
The Helm chart configures liveness and readiness probes on the API pods via `/api/health`. Default settings:
|
||||
The Helm chart configures **unauthenticated** liveness and readiness probes on the API pods so kubelet can reach them without credentials. Default settings:
|
||||
|
||||
```yaml
|
||||
api:
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
path: /api/diagnostic/healthz/live
|
||||
port: 8000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
periodSeconds: 20
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
path: /api/diagnostic/healthz/ready
|
||||
port: 8000
|
||||
initialDelaySeconds: 10
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
```
|
||||
|
||||
| Endpoint | Auth | Purpose |
|
||||
|----------|------|---------|
|
||||
| `/api/diagnostic/healthz/live` | None | Lightweight liveness check — returns 200 if the process is running |
|
||||
| `/api/diagnostic/healthz/ready` | None | Readiness check — verifies database and Redis connectivity (503 when DB is down) |
|
||||
| `/api/diagnostic/health` | Required | Full health status for monitoring dashboards (Grafana, Uptime Kuma) |
|
||||
|
||||
### Prometheus Scraping
|
||||
|
||||
Add annotations to expose metrics (if using a Prometheus-compatible exporter):
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
# Migration Workflow
|
||||
|
||||
This guide explains how to create, test, and merge Alembic database migrations in DocuElevate — especially when **multiple feature branches** add migrations in parallel.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Reference](#quick-reference)
|
||||
- [Creating a New Migration](#creating-a-new-migration)
|
||||
- [Migration Naming Convention](#migration-naming-convention)
|
||||
- [Idempotent Migration Patterns](#idempotent-migration-patterns)
|
||||
- [Parallel Branch Development](#parallel-branch-development)
|
||||
- [Resolving Migration Conflicts](#resolving-migration-conflicts)
|
||||
- [CI Validation](#ci-validation)
|
||||
- [Pre-commit Hook](#pre-commit-hook)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
# Create a new migration after editing app/models.py
|
||||
alembic revision --autogenerate -m "add_foobar_column"
|
||||
|
||||
# Apply all pending migrations
|
||||
alembic upgrade head
|
||||
|
||||
# Check current database version
|
||||
alembic current
|
||||
|
||||
# View migration history
|
||||
alembic history --verbose
|
||||
|
||||
# Detect multiple heads (diverged branches)
|
||||
alembic heads
|
||||
|
||||
# Create a merge migration to resolve multiple heads
|
||||
alembic merge heads -m "merge_parallel_branches"
|
||||
|
||||
# Validate migration chain integrity (CI script)
|
||||
python scripts/check_alembic_migrations.py
|
||||
python scripts/check_alembic_migrations.py --verbose
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Creating a New Migration
|
||||
|
||||
1. **Edit `app/models.py`** — add or modify SQLAlchemy model classes.
|
||||
|
||||
2. **Generate the migration** from the repo root. Use `--rev-id` to set the
|
||||
revision identifier directly (avoids renaming afterwards):
|
||||
|
||||
```bash
|
||||
alembic revision --autogenerate --rev-id 037_add_my_new_table -m "add my new table"
|
||||
```
|
||||
|
||||
This creates `migrations/versions/037_add_my_new_table_add_my_new_table.py`
|
||||
with `revision = "037_add_my_new_table"`. Rename the file to match:
|
||||
|
||||
```bash
|
||||
mv migrations/versions/037_add_my_new_table_add_my_new_table.py \
|
||||
migrations/versions/037_add_my_new_table.py
|
||||
```
|
||||
|
||||
Alternatively, generate with the default hash and then rename:
|
||||
|
||||
```bash
|
||||
alembic revision --autogenerate -m "add_my_new_table"
|
||||
# Rename: mv migrations/versions/<hash>_add_my_new_table.py migrations/versions/037_add_my_new_table.py
|
||||
# Update revision inside the file to match the filename stem.
|
||||
```
|
||||
|
||||
Alembic uses the `migrations/script.py.mako` template to generate the file. The template includes inline comments about idempotent patterns — read them.
|
||||
|
||||
3. **Review the generated code** — autogenerate is helpful but not perfect. Check:
|
||||
- Are new tables and columns detected correctly?
|
||||
- Does the `downgrade()` reverse all changes?
|
||||
- Are SQLite-incompatible operations wrapped in `batch_alter_table()`?
|
||||
|
||||
4. **Test the migration** against a fresh database:
|
||||
|
||||
```bash
|
||||
# Apply
|
||||
alembic upgrade head
|
||||
|
||||
# Rollback
|
||||
alembic downgrade -1
|
||||
|
||||
# Re-apply
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
5. **Run the chain validation**:
|
||||
|
||||
```bash
|
||||
python scripts/check_alembic_migrations.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Naming Convention
|
||||
|
||||
All migration files follow a **sequential numeric prefix** scheme:
|
||||
|
||||
```
|
||||
NNN_short_description.py
|
||||
```
|
||||
|
||||
| Component | Rule |
|
||||
|-----------|------|
|
||||
| `NNN` | Three-digit zero-padded number, incrementing from the previous migration |
|
||||
| `short_description` | Lowercase snake_case summary of the change |
|
||||
|
||||
The **`revision`** variable inside the file **must match the filename stem** exactly:
|
||||
|
||||
```python
|
||||
# File: migrations/versions/037_add_classification_rules.py
|
||||
revision: str = "037_add_classification_rules"
|
||||
down_revision: Union[str, None] = "036_add_document_translation_fields"
|
||||
```
|
||||
|
||||
The CI check (`scripts/check_alembic_migrations.py`) enforces this consistency.
|
||||
|
||||
---
|
||||
|
||||
## Idempotent Migration Patterns
|
||||
|
||||
Migrations should be **idempotent** — safe to run even if the change already exists. This is critical for SQLite compatibility and for recovering from partial failures.
|
||||
|
||||
### Add a Column (only if missing)
|
||||
|
||||
```python
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
|
||||
if "my_table" in inspector.get_table_names():
|
||||
existing = {c["name"] for c in inspector.get_columns("my_table")}
|
||||
if "new_col" not in existing:
|
||||
with op.batch_alter_table("my_table") as batch_op:
|
||||
batch_op.add_column(sa.Column("new_col", sa.String(128), nullable=True))
|
||||
```
|
||||
|
||||
### Create a Table (only if missing)
|
||||
|
||||
```python
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
|
||||
if "new_table" not in inspector.get_table_names():
|
||||
op.create_table(
|
||||
"new_table",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("name", sa.String(255), nullable=False),
|
||||
)
|
||||
```
|
||||
|
||||
### Drop a Column (only if present)
|
||||
|
||||
```python
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
|
||||
if "my_table" in inspector.get_table_names():
|
||||
existing = {c["name"] for c in inspector.get_columns("my_table")}
|
||||
if "new_col" in existing:
|
||||
with op.batch_alter_table("my_table") as batch_op:
|
||||
batch_op.drop_column("new_col")
|
||||
```
|
||||
|
||||
### Use `batch_alter_table` for SQLite
|
||||
|
||||
SQLite does not support `ALTER TABLE DROP COLUMN` or `ALTER TABLE RENAME COLUMN` natively. Alembic's `batch_alter_table` context manager works around this by recreating the table:
|
||||
|
||||
```python
|
||||
with op.batch_alter_table("users") as batch_op:
|
||||
batch_op.add_column(sa.Column("phone", sa.String(20), nullable=True))
|
||||
batch_op.drop_column("fax")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Parallel Branch Development
|
||||
|
||||
When two feature branches both add migrations from the same parent, the migration chain **diverges** into multiple heads. This is normal and expected — Alembic supports it — but the heads must be merged before the code reaches `main`.
|
||||
|
||||
### Example
|
||||
|
||||
```
|
||||
main: 001 → 002 → 003
|
||||
↘ Branch A: 004_add_widgets
|
||||
↘ Branch B: 004_add_gadgets ← two heads!
|
||||
```
|
||||
|
||||
### How to Avoid Conflicts
|
||||
|
||||
1. **Coordinate** — if two developers are both adding migrations, assign different sequence numbers (e.g., `037_` and `038_`). Even if both depend on `036_`, different numbers prevent filename collisions.
|
||||
|
||||
2. **Rebase early** — before opening a PR, rebase your branch onto the latest `main`:
|
||||
|
||||
```bash
|
||||
git fetch origin main
|
||||
git rebase origin/main
|
||||
```
|
||||
|
||||
If `main` now has a new migration `037_*`, renumber yours to `038_*` and update `down_revision` to point at `037_*`.
|
||||
|
||||
3. **Check for multiple heads** locally:
|
||||
|
||||
```bash
|
||||
python scripts/check_alembic_migrations.py
|
||||
# or
|
||||
alembic heads
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resolving Migration Conflicts
|
||||
|
||||
If your PR's CI check reports **"Multiple migration heads detected"**, follow these steps:
|
||||
|
||||
### Step 1 — Update Your Branch
|
||||
|
||||
```bash
|
||||
git fetch origin main
|
||||
git merge origin/main
|
||||
# or
|
||||
git rebase origin/main
|
||||
```
|
||||
|
||||
### Step 2 — Check Heads
|
||||
|
||||
```bash
|
||||
python scripts/check_alembic_migrations.py --verbose
|
||||
```
|
||||
|
||||
The output lists the conflicting heads.
|
||||
|
||||
### Step 3 — Create a Merge Migration
|
||||
|
||||
```bash
|
||||
alembic merge heads -m "merge_parallel_branches"
|
||||
```
|
||||
|
||||
This generates a new migration with **two parents** (a merge point):
|
||||
|
||||
```python
|
||||
down_revision = ("037_add_widgets", "037_add_gadgets")
|
||||
```
|
||||
|
||||
### Step 4 — Rename and Validate
|
||||
|
||||
Rename the merge migration to the next sequence number:
|
||||
|
||||
```bash
|
||||
mv migrations/versions/<hash>_merge_parallel_branches.py \
|
||||
migrations/versions/038_merge_parallel_branches.py
|
||||
```
|
||||
|
||||
Update the `revision` inside to match, then validate:
|
||||
|
||||
```bash
|
||||
python scripts/check_alembic_migrations.py
|
||||
```
|
||||
|
||||
### Step 5 — Test
|
||||
|
||||
```bash
|
||||
alembic upgrade head
|
||||
alembic downgrade -1
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CI Validation
|
||||
|
||||
The CI pipeline (`.github/workflows/ci.yml`) includes a **migration-chain** job that runs:
|
||||
|
||||
```bash
|
||||
python scripts/check_alembic_migrations.py
|
||||
```
|
||||
|
||||
This script checks for:
|
||||
|
||||
| Check | Description |
|
||||
|-------|-------------|
|
||||
| Multiple heads | Diverged migration chains that need a merge migration |
|
||||
| Broken references | A `down_revision` that points to a non-existent revision |
|
||||
| Duplicate revisions | Two files declaring the same `revision` identifier |
|
||||
| Filename mismatches | The `revision` variable doesn't match the filename stem |
|
||||
|
||||
The job runs in Stage 1 (fast-fail gates) alongside lint checks. If it fails, the build is blocked until the migration chain is fixed.
|
||||
|
||||
---
|
||||
|
||||
## Pre-commit Hook
|
||||
|
||||
A local pre-commit hook is configured in `.pre-commit-config.yaml` that runs the same check whenever you commit a change to `migrations/versions/`:
|
||||
|
||||
```yaml
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: check-alembic-migrations
|
||||
name: Check Alembic migration chain
|
||||
entry: python scripts/check_alembic_migrations.py
|
||||
language: python
|
||||
pass_filenames: false
|
||||
files: ^migrations/versions/.*\.py$
|
||||
```
|
||||
|
||||
Install the hook:
|
||||
|
||||
```bash
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Multiple migration heads detected"
|
||||
|
||||
See [Resolving Migration Conflicts](#resolving-migration-conflicts) above.
|
||||
|
||||
### "Broken chain: revision X references down_revision Y which does not exist"
|
||||
|
||||
You removed or renamed a migration that another migration depends on. Either restore the missing file or update the dependent migration's `down_revision`.
|
||||
|
||||
### "Filename mismatch: file declares revision=X but filename stem is Y"
|
||||
|
||||
The `revision` string inside the Python file must match the filename (without `.py`). Rename the file or update the variable.
|
||||
|
||||
### "relation already exists" when running `alembic upgrade head`
|
||||
|
||||
The database has a table that a pending migration tries to create. Stamp the current state:
|
||||
|
||||
```bash
|
||||
alembic stamp head
|
||||
```
|
||||
|
||||
### Autogenerate doesn't detect my changes
|
||||
|
||||
Ensure all models are imported in `migrations/env.py`. The `from app.models import ...` block at the top must include your new model class.
|
||||
|
||||
### SQLite "no such column" after downgrade
|
||||
|
||||
SQLite has limited `ALTER TABLE` support. Always use `op.batch_alter_table()` for column operations on existing tables.
|
||||
|
||||
---
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Alembic Tutorial](https://alembic.sqlalchemy.org/en/latest/tutorial.html)
|
||||
- [Alembic Branch / Merge](https://alembic.sqlalchemy.org/en/latest/branches.html)
|
||||
- [Database Configuration Guide](DatabaseConfiguration.md)
|
||||
+236
-12
@@ -8,12 +8,18 @@ DocuElevate includes a native mobile application for iOS and Android built with
|
||||
|---------|-----|---------|
|
||||
| SSO login (OAuth2) | ✅ | ✅ |
|
||||
| Local / basic auth login | ✅ | ✅ |
|
||||
| QR code login (scan from web) | ✅ | ✅ |
|
||||
| Auto-generated API token | ✅ | ✅ |
|
||||
| Camera capture → upload | ✅ | ✅ |
|
||||
| File picker upload | ✅ | ✅ |
|
||||
| Multi-image selection from library | ✅ | ✅ |
|
||||
| Share Sheet / Share Intent | ✅ | ✅ |
|
||||
| Push notifications | ✅ | ✅ |
|
||||
| Document list | ✅ | ✅ |
|
||||
| Document list with search | ✅ | ✅ |
|
||||
| File detail view with processing logs | ✅ | ✅ |
|
||||
| Pre-login legal pages (GDPR) | ✅ | ✅ |
|
||||
| Localization (EN, DE, ES, FR, IT) | ✅ | ✅ |
|
||||
| Language selection | ✅ | ✅ |
|
||||
| Dark mode | ✅ | ✅ |
|
||||
|
||||
## Getting Started (Development)
|
||||
@@ -57,6 +63,34 @@ eas build --platform android
|
||||
|
||||
See the [EAS Build documentation](https://docs.expo.dev/build/introduction/) for full setup instructions.
|
||||
|
||||
## Automated CI/CD
|
||||
|
||||
An **EAS Cloud Workflow** (`mobile/.eas/workflows/create-builds.yml`) automates production builds and iOS submission:
|
||||
|
||||
- **Path filtering:** The workflow only triggers on pushes to `main` that include changes inside the `mobile/` directory. Backend-only or documentation-only changes do not trigger a mobile build.
|
||||
- **Build:** Both iOS and Android production builds run in parallel on EAS Build.
|
||||
- **Auto-submit (iOS):** After a successful iOS build, the workflow automatically submits the binary to **App Store Connect** using the credentials configured in `eas.json` (`submit.production.ios`). The build then appears in **TestFlight** for internal testing and can be promoted to the App Store from App Store Connect.
|
||||
|
||||
> **Prerequisite:** An App Store Connect API Key must be configured in EAS for non-interactive submission. See [Troubleshooting → "Session expired"](#session-expired-local-session-during-ios-build) below for setup instructions.
|
||||
|
||||
### Version Management
|
||||
|
||||
Build numbers (iOS `buildNumber` / Android `versionCode`) are managed **remotely** by EAS. The `eas.json` configuration uses:
|
||||
|
||||
```json
|
||||
{
|
||||
"cli": { "appVersionSource": "remote" },
|
||||
"build": { "production": { "autoIncrement": true } }
|
||||
}
|
||||
```
|
||||
|
||||
- **`appVersionSource: "remote"`** — EAS stores the current build number on its servers instead of reading it from `app.json`. This ensures every CI build gets a unique, ever-increasing number without needing to commit version bumps back to the repository.
|
||||
- **`autoIncrement: true`** — EAS automatically increments the build number before each production build.
|
||||
|
||||
The `ios.buildNumber` and `android.versionCode` values in `app.json` serve as the **initial seed** when the remote version is first created; after that they are informational only. Do not rely on them for the actual version submitted to the stores.
|
||||
|
||||
> **Tip:** To check or manually set the remote version, use `eas build:version:get` and `eas build:version:set`.
|
||||
|
||||
## Authentication
|
||||
|
||||
### SSO Login Flow
|
||||
@@ -84,6 +118,18 @@ When developing with **Expo Go** the app does not have the `docuelevate://` cust
|
||||
|
||||
No extra configuration is needed — just run `npx expo start` and scan the QR code with the **Expo Go** app.
|
||||
|
||||
### QR Code Login Flow
|
||||
|
||||
As an alternative to SSO, users can log in by scanning a QR code displayed in the web UI:
|
||||
|
||||
1. The authenticated web user navigates to **Profile → Security & Sessions → Log in on mobile via QR code**.
|
||||
2. A QR code is displayed containing a deep link: `docuelevate://qr-login?token=<challenge_token>&server=<server_url>`.
|
||||
3. In the mobile app, the user taps **Scan QR Code to Login**, which opens the device camera.
|
||||
4. The app scans the QR code, extracts both the server URL and the challenge token, and calls `POST /api/qr-auth/claim`.
|
||||
5. An API token is issued and stored securely — no need to enter the server URL manually.
|
||||
|
||||
> **Note:** The QR code already contains the server URL, so users do not need to type it in when using QR login.
|
||||
|
||||
### Auto-generated Mobile Token
|
||||
|
||||
When the mobile app completes login it automatically creates a named API token (`"Mobile App – <device name>"`) via `POST /api/mobile/generate-token`. This token:
|
||||
@@ -126,10 +172,17 @@ curl -X DELETE -H "Authorization: Bearer <token>" https://your-server/api/mobile
|
||||
3. Point the camera at the document and take a photo.
|
||||
4. The image is immediately uploaded and queued for processing.
|
||||
|
||||
### Photo Library
|
||||
|
||||
1. Open the **Upload** tab.
|
||||
2. Tap **Photos**.
|
||||
3. Select one or more photos from the device's photo library (multi-selection is supported).
|
||||
4. All selected images are uploaded and queued for processing.
|
||||
|
||||
### File Picker
|
||||
|
||||
1. Open the **Upload** tab.
|
||||
2. Tap **File Picker**.
|
||||
2. Tap **Files**.
|
||||
3. Browse to and select one or more files (PDF, DOCX, images, etc.).
|
||||
4. Files are uploaded and queued for processing.
|
||||
|
||||
@@ -140,10 +193,128 @@ The app registers itself as a share target so any file can be sent directly to D
|
||||
1. Open a file in Files, Mail, Safari, or any other app.
|
||||
2. Tap the **Share** button (iOS) or **Share** (Android).
|
||||
3. Find and tap **DocuElevate** in the share sheet.
|
||||
4. The file is immediately uploaded.
|
||||
4. The file is immediately uploaded and queued for processing.
|
||||
|
||||
> **Note:** The app must be installed on the device for it to appear in the share sheet.
|
||||
|
||||
#### iOS implementation
|
||||
|
||||
`app.json` declares `CFBundleDocumentTypes` (with `LSHandlerRank: Alternate`) inside the iOS `infoPlist`. This tells iOS that DocuElevate can open common document types, making it visible in the share sheet without overriding system defaults. When the user selects DocuElevate, iOS opens the app with a URL via `application:openURL:options:`.
|
||||
|
||||
The URL may arrive as a standard `file://` path **or** under the app's custom `docuelevate://` scheme (e.g. `docuelevate://private/var/mobile/Library/…/file.pdf`). The root layout detects the custom-scheme form and rewrites it to a `file://` URL before forwarding it to the Upload screen through `ShareContext`.
|
||||
|
||||
##### Handling "unmatched route" errors from "Open In…"
|
||||
|
||||
iOS sometimes delivers the file path under the `docuelevate://` scheme, e.g.:
|
||||
|
||||
```
|
||||
docuelevate://private/var/mobile/Library/Mobile Documents/…/Invoice.pdf
|
||||
```
|
||||
|
||||
expo-router strips the scheme and tries to match `/private/var/mobile/…` as an in-app route. Because no such route exists, it previously threw an **"unmatched route docuelevate://"** error and the upload never completed.
|
||||
|
||||
The fix is a catch-all `+not-found.tsx` route (see `mobile/app/+not-found.tsx`). When expo-router cannot match the path, it renders this screen instead. The screen detects that the path is a filesystem path rather than a real in-app route, adds the file directly to `ShareContext`, and redirects to the Upload tab. `UploadScreen` picks up the pending file and begins uploading automatically. The `Linking` listener in the root layout may also fire for the same URL; `ShareContext.addPendingFile` deduplicates by URI so the file is only uploaded once.
|
||||
|
||||
##### File accessibility and local caching
|
||||
|
||||
Shared files may reference paths outside the app's sandbox or use security-scoped URLs that React Native's `fetch` cannot read directly. To guarantee reliable uploads:
|
||||
|
||||
- **`LSSupportsOpeningDocumentsInPlace`** is set to `false` in `app.json`, which tells iOS to copy shared files into the app's `Documents/Inbox` directory before handing them to the app.
|
||||
- **`UploadScreen`** uses `expo-file-system` (`FileSystem.copyAsync`) to copy any `file://` URI that is outside the app's cache/documents directory to a local cache path before uploading. This ensures the file is readable regardless of its origin.
|
||||
- **MIME type inference**: Both `+not-found.tsx` and the `Linking` handler in `_layout.tsx` infer the MIME type from the file extension (e.g. `.pdf` → `application/pdf`) so the server receives a correct `Content-Type` instead of `application/octet-stream`.
|
||||
|
||||
##### iOS Action / Share Extension (future enhancement)
|
||||
|
||||
Apps like DeepL ("Translate in DeepL") and Microsoft Word ("Convert to Word") appear as **Action Extensions** in the iOS share sheet — a system-level feature that requires a separate Xcode target built with Swift or Objective-C. A proper Action Extension runs in its own process and must share authentication credentials with the main app via an iOS **App Group** (shared keychain / shared container).
|
||||
|
||||
This level of iOS-native integration is a planned future enhancement. Until it is available, the recommended workflow is the current one: tap **Share → DocuElevate** (the app appears in the "Open With" row of the share sheet via `CFBundleDocumentTypes`).
|
||||
|
||||
#### Android implementation
|
||||
|
||||
`app.json` declares `ACTION_SEND` and `ACTION_SEND_MULTIPLE` intent filters for `mimeType: "*/*"` in the `android.intentFilters` section. Incoming content URIs are received the same way as on iOS.
|
||||
|
||||
#### Upload status polling
|
||||
|
||||
After a file is uploaded the app polls `/api/files?search=<filename>` every 5 seconds to find the corresponding `FileRecord`, then polls `/api/files/{id}` to track the processing status in real time. Polling stops automatically once the status reaches a terminal state (`completed`, `failed`, or `duplicate`).
|
||||
|
||||
#### Retrying failed uploads
|
||||
|
||||
If a file upload fails (e.g. due to network issues or a server error), the failed item stays visible in the upload list with an error message and a **"Tap to retry"** hint. Users can retry the upload in two ways:
|
||||
|
||||
- **Tap** the failed item to immediately retry the upload.
|
||||
- **Long-press** the failed item to see a confirmation dialog with a **Retry** option.
|
||||
|
||||
The retry re-uses the original file URI so no re-selection is needed.
|
||||
|
||||
## Document Search
|
||||
|
||||
The **Files** tab includes a search bar at the top that lets users search through their processed documents by filename. Searches are debounced (400ms) to avoid excessive API calls. Clear the search with the ✕ button to return to the full list.
|
||||
|
||||
## File Detail View
|
||||
|
||||
Tapping any document in the **Files** tab opens a detail view showing:
|
||||
|
||||
- **File metadata**: filename, file size, MIME type, upload date, and file hash
|
||||
- **Processing status**: current status with a colour-coded icon
|
||||
- **Processing log**: chronological list of processing steps with individual status indicators and timestamps
|
||||
|
||||
Pull-to-refresh updates the detail view. This replicates the web interface at `/files/{id}` and `/files/{id}/detail` in a mobile-friendly layout.
|
||||
|
||||
## Legal & Compliance
|
||||
|
||||
### GDPR & Apple App Store Compliance
|
||||
|
||||
Privacy Policy, Terms of Service, and Imprint links are accessible **before login** from both the **Welcome Screen** and the **Login Screen**. This ensures compliance with:
|
||||
|
||||
- **GDPR** (General Data Protection Regulation) – users must be able to review the privacy policy before providing personal data
|
||||
- **Apple App Store Review Guidelines** – apps must provide accessible privacy information before account creation
|
||||
|
||||
Post-login, the same links are available in the **Profile** tab under the "Legal" section.
|
||||
|
||||
## Localization (i18n)
|
||||
|
||||
The mobile app supports five languages with automatic device-locale detection:
|
||||
|
||||
| Language | Code | Status |
|
||||
|----------|------|--------|
|
||||
| English | `en` | ✅ Complete |
|
||||
| German (Deutsch) | `de` | ✅ Complete |
|
||||
| Spanish (Español) | `es` | ✅ Complete |
|
||||
| French (Français) | `fr` | ✅ Complete |
|
||||
| Italian (Italiano) | `it` | ✅ Complete |
|
||||
|
||||
### How it works
|
||||
|
||||
Language priority (highest to lowest):
|
||||
|
||||
1. **Server preference** — `preferred_language` returned by `GET /api/mobile/whoami` on login or app resume. Allows a language set on the desktop web interface to propagate to mobile automatically.
|
||||
2. **AsyncStorage** — the last language explicitly selected on the device, used as an offline fallback when the server is unreachable.
|
||||
3. **Device locale** — detected via `expo-localization` on first launch.
|
||||
4. **English** — final fallback when none of the above match a supported locale.
|
||||
|
||||
When a user selects a language on mobile the choice is:
|
||||
- Applied immediately to all screens (via `LocaleContext`)
|
||||
- Persisted locally to AsyncStorage
|
||||
- Synced to the server via `POST /api/i18n/language` (fire-and-forget), so the next desktop login reflects the same preference.
|
||||
|
||||
> **Note**: If the server's preferred language is not supported by the mobile app (e.g. a locale added to the web frontend but not yet translated for mobile), the mobile app falls back to the next priority in the list above.
|
||||
|
||||
### Adding a new language
|
||||
|
||||
1. Create a new translation file in `mobile/src/i18n/` (e.g. `pt.json` for Portuguese)
|
||||
2. Copy the structure from `en.json` and translate all values
|
||||
3. Import the new file in `mobile/src/i18n/index.ts`
|
||||
4. Add it to the `translations` object and `getSupportedLanguages()` array
|
||||
|
||||
## User Settings
|
||||
|
||||
The **Profile** tab includes a **Settings** section where users can:
|
||||
|
||||
- **Change language**: Select from the supported languages (English, German, Spanish, French, Italian)
|
||||
- View server connection details
|
||||
- Access legal documents (Privacy Policy, Terms of Service, Imprint)
|
||||
- Sign out or delete their account
|
||||
|
||||
## Mobile API Endpoints
|
||||
|
||||
The backend exposes a dedicated `/api/mobile/` namespace:
|
||||
@@ -154,7 +325,8 @@ The backend exposes a dedicated `/api/mobile/` namespace:
|
||||
| `POST` | `/api/mobile/register-device` | Bearer | Register Expo push token |
|
||||
| `GET` | `/api/mobile/devices` | Bearer | List registered devices |
|
||||
| `DELETE` | `/api/mobile/devices/{id}` | Bearer | Deactivate a device |
|
||||
| `GET` | `/api/mobile/whoami` | Bearer | Get current user profile |
|
||||
| `GET` | `/api/mobile/whoami` | Bearer | Get current user profile (includes `preferred_language`) |
|
||||
| `POST` | `/api/i18n/language` | Bearer | Sync language preference to server |
|
||||
|
||||
All other API endpoints (file upload, file listing, etc.) work with Bearer token authentication.
|
||||
|
||||
@@ -198,7 +370,7 @@ Re-registering the same token is safe (idempotent).
|
||||
|
||||
### GET /api/mobile/whoami
|
||||
|
||||
Returns the current user's profile.
|
||||
Returns the current user's profile, including the server-stored language preference.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
@@ -207,10 +379,15 @@ Returns the current user's profile.
|
||||
"display_name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"avatar_url": "https://www.gravatar.com/avatar/...",
|
||||
"is_admin": false
|
||||
"is_admin": false,
|
||||
"preferred_language": "de"
|
||||
}
|
||||
```
|
||||
|
||||
`preferred_language` is `null` when no preference has been saved. The mobile
|
||||
app applies this value on login / app resume, falling back to AsyncStorage and
|
||||
then the device locale when it is `null` or unsupported.
|
||||
|
||||
## Configuration
|
||||
|
||||
No server-side configuration is required to enable the mobile app. The Expo push notification routing does not need FCM or APNs credentials on the server.
|
||||
@@ -221,27 +398,73 @@ If you wish to use **direct FCM/APNs** without Expo's relay, replace the `send_e
|
||||
|
||||
```
|
||||
mobile/
|
||||
├── App.tsx # Root component
|
||||
├── App.tsx # Root component (legacy, not used at runtime)
|
||||
├── app/ # Expo Router file-based routes
|
||||
│ ├── _layout.tsx # Root layout (AuthGuard + providers)
|
||||
│ ├── index.tsx # Root redirect → /(auth)/
|
||||
│ ├── (auth)/ # Unauthenticated route group
|
||||
│ │ ├── _layout.tsx # Stack navigator (headerless)
|
||||
│ │ ├── index.tsx # Welcome screen
|
||||
│ │ ├── login.tsx # Login screen
|
||||
│ │ └── qr-scanner.tsx # QR code scanner screen
|
||||
│ └── (tabs)/ # Authenticated route group
|
||||
│ ├── _layout.tsx # Tab navigator
|
||||
│ ├── index.tsx # Upload screen (default tab)
|
||||
│ ├── files.tsx # Files screen
|
||||
│ └── profile.tsx # Profile screen
|
||||
├── app.json # Expo/EAS configuration
|
||||
├── eas.json # EAS Build profiles
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── src/
|
||||
├── context/
|
||||
│ └── AuthContext.tsx # Auth state + SSO login flow
|
||||
│ ├── AuthContext.tsx # Auth state + SSO login flow
|
||||
│ └── ShareContext.tsx # Shared-file queue (iOS Share Sheet / Android Intent)
|
||||
├── hooks/
|
||||
│ └── usePushNotifications.ts # Push token registration
|
||||
├── screens/
|
||||
│ ├── LoginScreen.tsx # Server URL + SSO button
|
||||
│ ├── UploadScreen.tsx # Camera capture + file picker
|
||||
│ ├── FilesScreen.tsx # Processed document list
|
||||
│ └── ProfileScreen.tsx # User profile + sign out
|
||||
│ ├── LoginScreen.tsx # Server URL + SSO button + QR code scanner
|
||||
│ ├── QRScannerScreen.tsx # Camera-based QR code scanner for login
|
||||
│ ├── UploadScreen.tsx # Camera capture + photo library + file picker
|
||||
│ ├── FilesScreen.tsx # Processed document list with search
|
||||
│ ├── FileDetailScreen.tsx # File detail view with processing logs
|
||||
│ ├── ProfileScreen.tsx # User profile + settings + sign out
|
||||
│ └── WelcomeScreen.tsx # Pre-login welcome with legal links
|
||||
├── i18n/ # Localization (i18n)
|
||||
│ ├── index.ts # i18n module (locale detection, t() function)
|
||||
│ ├── en.json # English translations
|
||||
│ ├── de.json # German translations
|
||||
│ ├── es.json # Spanish translations
|
||||
│ ├── fr.json # French translations
|
||||
│ └── it.json # Italian translations
|
||||
├── utils/
|
||||
│ ├── mimeTypes.ts # MIME type mapping for file extensions
|
||||
│ └── normalizeUri.ts # URI normalization for deduplication
|
||||
└── services/
|
||||
└── api.ts # DocuElevate REST API client
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### App shows "Hello World" / default Expo page after update
|
||||
|
||||
If the iOS or Android app shows a generic "Hello World – This is the first page of your app" screen instead of the DocuElevate UI, it means a stale default `index.tsx` file (generated by Expo CLI scaffolding) is being picked up in the `mobile/app/` directory.
|
||||
|
||||
**To fix:**
|
||||
|
||||
1. Delete any leftover default `mobile/app/index.tsx` that is **not** the repository version (the repo version contains a `<Redirect>` to `/(auth)/`).
|
||||
2. Clear the Metro bundler cache and rebuild:
|
||||
```bash
|
||||
cd mobile
|
||||
npx expo start --clear
|
||||
```
|
||||
3. For production builds, run a clean EAS build:
|
||||
```bash
|
||||
eas build --platform ios --clear-cache
|
||||
```
|
||||
|
||||
The repository includes a root `app/index.tsx` that immediately redirects to the authentication flow, so this issue should not recur once the correct file is present.
|
||||
|
||||
### "Session expired Local session" during iOS build
|
||||
|
||||
EAS stores an Apple ID session locally (in `~/.expo/`) to manage code-signing certificates and provisioning profiles. This session expires after a few weeks.
|
||||
@@ -303,3 +526,4 @@ eas build --platform ios
|
||||
- [API Documentation](./API.md)
|
||||
- [Configuration Guide](./ConfigurationGuide.md)
|
||||
- [Deployment Guide](./DeploymentGuide.md)
|
||||
- [Apple App Store Compliance Audit](./AppleAppStoreCompliance.md)
|
||||
|
||||
@@ -29,9 +29,10 @@ End users authorize their own OneDrive integration from the **Integrations** das
|
||||
1. Navigate to `/integrations` and click **+ Add Destination** (or **+ Add Source** for Watch Folder).
|
||||
2. Create a OneDrive destination integration (or a Watch Folder with `source_type = onedrive`).
|
||||
3. Click the **Authorize** button next to the integration — it links directly to the OAuth wizard pre-loaded with your integration's configuration.
|
||||
4. Enter your Azure AD Client ID and Client Secret in the wizard.
|
||||
4. If the administrator has configured system-wide OneDrive app credentials (`ONEDRIVE_CLIENT_ID` / `ONEDRIVE_CLIENT_SECRET`), the wizard defaults to using them — no need to register your own Azure AD app. Uncheck the toggle to use custom credentials if needed.
|
||||
5. Click **Start Authentication Flow**, authorize access via Microsoft, and the refresh token is automatically saved to your personal integration record.
|
||||
6. The page redirects back to `/integrations` on success. Re-authorization is available at any time via the **Re-Authorize** button.
|
||||
6. After authorization, an interactive **folder browser** lets you select the target folder directly from your OneDrive — no need to manually type folder paths.
|
||||
7. The page redirects to `/integrations` on success. Re-authorization is available at any time via the **Re-Authorize** button.
|
||||
|
||||
> **Note:** Your credentials are stored encrypted per-integration and are never mixed with other users' data. Each user can have multiple OneDrive integrations with independent tokens.
|
||||
|
||||
|
||||
+39
-16
@@ -34,7 +34,7 @@ Use this checklist to track readiness before going live.
|
||||
- [ ] **Redis** — Running and accessible only from internal network
|
||||
- [ ] **Meilisearch** — Running and accessible only from internal network
|
||||
- [ ] **Worker replicas** — At least 2 workers configured for redundancy
|
||||
- [ ] **Monitoring** — `/api/health` polled by uptime checker
|
||||
- [ ] **Monitoring** — `/api/diagnostic/health` polled by uptime checker
|
||||
- [ ] **Backups** — Automated backup of database, workdir, and Meilisearch data
|
||||
- [ ] **Log retention** — Logs shipped to a persistent store or aggregator
|
||||
- [ ] **Secrets management** — API keys not committed to source control
|
||||
@@ -157,7 +157,7 @@ Recommended headers to configure at the proxy level:
|
||||
|
||||
#### Content-Security-Policy Notes
|
||||
|
||||
DocuElevate's frontend uses Tailwind CSS loaded from CDN in development mode. In production, ensure your CSP allows loading scripts and styles from your configured static file origin. A starting point:
|
||||
DocuElevate's frontend uses Tailwind CSS v3 compiled at Docker build time. No external CDN requests are needed for CSS. In production, your CSP does not need to allow any external style sources beyond your own static file origin. A starting point:
|
||||
|
||||
```
|
||||
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
|
||||
@@ -285,22 +285,24 @@ For SSO/OIDC (Authentik, Keycloak, Auth0, etc.) see the [Authentication Setup Gu
|
||||
|
||||
### Docker Compose
|
||||
|
||||
Use the `deploy.replicas` setting (requires Docker Swarm mode) or simply run multiple workers:
|
||||
|
||||
```yaml
|
||||
worker:
|
||||
deploy:
|
||||
replicas: 3
|
||||
```
|
||||
|
||||
Or scale after deployment:
|
||||
Scale workers independently:
|
||||
|
||||
```bash
|
||||
docker-compose up -d --scale worker=3
|
||||
docker compose up -d --scale worker=3
|
||||
```
|
||||
|
||||
Each worker processes tasks from the Celery queue independently. Ensure the shared `workdir` volume is accessible from all worker containers.
|
||||
|
||||
> **Important:** The `beat` service (Celery Beat scheduler) must always run as exactly **one** instance. It is defined as a dedicated service in `docker-compose.yaml` with a fixed `container_name`. Do not scale it.
|
||||
|
||||
### Scaling the API
|
||||
|
||||
API pods are fully stateless (sessions use encrypted cookies, not server-side state) and can be scaled behind a load balancer:
|
||||
|
||||
```bash
|
||||
docker compose up -d --scale api=3
|
||||
```
|
||||
|
||||
### Kubernetes (Helm)
|
||||
|
||||
```yaml
|
||||
@@ -339,11 +341,32 @@ celery -A app.celery_worker worker -Q default,celery --concurrency=2
|
||||
|
||||
### Health Check Endpoint
|
||||
|
||||
DocuElevate exposes `/api/health` for readiness probing. Configure your uptime monitor to poll this endpoint:
|
||||
DocuElevate exposes three health-related endpoints:
|
||||
|
||||
| Endpoint | Auth | Purpose |
|
||||
|----------|------|---------|
|
||||
| `GET /api/diagnostic/healthz/live` | None | Lightweight liveness probe — returns 200 if the process is running |
|
||||
| `GET /api/diagnostic/healthz/ready` | None | Readiness probe — checks database and Redis (503 when DB is down) |
|
||||
| `GET /api/diagnostic/health` | Required | Full status for monitoring dashboards (Grafana, Uptime Kuma) |
|
||||
|
||||
For **Kubernetes probes**, use the unauthenticated endpoints:
|
||||
|
||||
```yaml
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/diagnostic/healthz/live
|
||||
port: 8000
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/diagnostic/healthz/ready
|
||||
port: 8000
|
||||
```
|
||||
|
||||
For **uptime monitors** (Uptime Kuma, Grafana, etc.), use the authenticated endpoint:
|
||||
|
||||
```bash
|
||||
curl http://docuelevate.example.com/api/health
|
||||
# Expected: {"status": "ok", ...}
|
||||
curl http://docuelevate.example.com/api/diagnostic/health
|
||||
# Expected: {"status": "healthy", ...}
|
||||
```
|
||||
|
||||
Set `UPTIME_KUMA_URL` to your Uptime Kuma push URL for heartbeat monitoring:
|
||||
@@ -502,4 +525,4 @@ For a dedicated Kubernetes deployment guide, including architecture diagrams, PV
|
||||
|
||||
- **Image Pull Policy**: Use `IfNotPresent` in production with pinned image tags (not `latest`) for reproducible deployments.
|
||||
|
||||
- **Liveness & Readiness Probes**: Already configured in the Helm chart via `/api/health`. Verify they are tuned to your startup time.
|
||||
- **Liveness & Readiness Probes**: Already configured in the Helm chart via unauthenticated endpoints (`/api/diagnostic/healthz/live` and `/api/diagnostic/healthz/ready`). Verify they are tuned to your startup time.
|
||||
|
||||
@@ -22,6 +22,7 @@ Welcome to the DocuElevate documentation. This directory contains comprehensive
|
||||
- [Google Drive Setup](GoogleDriveSetup.md) - How to set up Google Drive integration
|
||||
- [Dropbox Setup](DropboxSetup.md) - How to set up Dropbox integration
|
||||
- [OneDrive Setup](OneDriveSetup.md) - How to set up Microsoft OneDrive/Graph integration
|
||||
- [SharePoint Setup](SharePointSetup.md) - How to set up Microsoft SharePoint Online integration
|
||||
- [Amazon S3 Setup](AmazonS3Setup.md) - How to set up Amazon S3 integration
|
||||
- [Authentication Setup](AuthenticationSetup.md) - How to set up user authentication
|
||||
- [Notifications Setup](NotificationsSetup.md) - How to set up system notifications
|
||||
|
||||
+60
-11
@@ -2,13 +2,13 @@
|
||||
|
||||
DocuElevate ships with first-class support for [Sentry](https://sentry.io) — an open-source observability platform that provides real-time error tracking and performance monitoring.
|
||||
|
||||
When a **Sentry DSN** is configured, every unhandled exception in the FastAPI web process and Celery worker is automatically captured and sent to your Sentry project. Performance transactions (request traces, database queries, background task durations) are also recorded, giving you end-to-end visibility into your deployment.
|
||||
When a **Sentry DSN** is configured, every unhandled exception in the FastAPI web process and Celery worker is automatically captured and sent to your Sentry project. The **Sentry Browser SDK** is also injected into every rendered page, capturing client-side JavaScript errors, browser performance transactions, and (optionally) session replays. Together these give you full-stack, end-to-end visibility into your deployment.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Create a Sentry project** at <https://sentry.io> (or your self-hosted Sentry instance). Choose the **Python** platform.
|
||||
1. **Create a Sentry project** at <https://sentry.io> (or your self-hosted Sentry instance). Choose the **Python** platform (the same project and DSN are used for both the server and browser SDKs).
|
||||
2. Copy the **DSN** from *Project → Settings → Client Keys (DSN)*. It looks like:
|
||||
```
|
||||
https://<public_key>@o<org_id>.ingest.sentry.io/<project_id>
|
||||
@@ -17,12 +17,14 @@ When a **Sentry DSN** is configured, every unhandled exception in the FastAPI we
|
||||
```bash
|
||||
SENTRY_DSN=https://<public_key>@o<org_id>.ingest.sentry.io/<project_id>
|
||||
```
|
||||
4. Restart DocuElevate. Sentry initialises automatically on startup — you will see a log line confirming activation.
|
||||
4. Restart DocuElevate. Sentry initialises automatically on startup — you will see a log line confirming activation. The Sentry Browser SDK `<script>` tag is automatically injected into every rendered page.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Server-side (Python SDK)
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `SENTRY_DSN` | *(empty)* | Sentry DSN URL. **Required** to enable Sentry. Leave unset to disable. |
|
||||
@@ -31,6 +33,16 @@ When a **Sentry DSN** is configured, every unhandled exception in the FastAPI we
|
||||
| `SENTRY_PROFILES_SAMPLE_RATE` | `0.0` | Fraction of profiled transactions sent to Sentry (0.0 – 1.0). Only active when traces > 0. |
|
||||
| `SENTRY_SEND_DEFAULT_PII` | `false` | Attach PII (IP addresses, user agents) to events. Disable for GDPR / CCPA compliance. |
|
||||
|
||||
### Browser SDK (JavaScript)
|
||||
|
||||
The Sentry Browser SDK is loaded automatically on every rendered page when `SENTRY_DSN` is set. The DSN is a *public* key in Sentry's security model and is intentionally embedded in client-side code.
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `SENTRY_JS_TRACES_SAMPLE_RATE` | `0.0` | Fraction of browser page-loads captured for client-side performance tracing (0.0 – 1.0). |
|
||||
| `SENTRY_JS_REPLAY_SESSION_SAMPLE_RATE` | `0.0` | Fraction of sessions recorded by [Sentry Session Replay](https://docs.sentry.io/product/session-replay/) (0.0 – 1.0). |
|
||||
| `SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE` | `0.1` | Fraction of error sessions captured with session replay context (0.0 – 1.0). |
|
||||
|
||||
All variables can alternatively be managed through the **Settings → Observability** section of the DocuElevate admin UI. Settings stored in the database are applied before Sentry initialises on every startup, so changes made via the UI take effect after a restart without requiring any changes to environment variables or `.env` files.
|
||||
|
||||
---
|
||||
@@ -42,9 +54,13 @@ All variables can alternatively be managed through the **Settings → Observabil
|
||||
```bash
|
||||
SENTRY_DSN=https://<key>@o<org>.ingest.sentry.io/<project>
|
||||
SENTRY_ENVIRONMENT=development
|
||||
SENTRY_TRACES_SAMPLE_RATE=1.0 # Capture every request during development
|
||||
SENTRY_TRACES_SAMPLE_RATE=1.0 # Capture every request during development
|
||||
SENTRY_PROFILES_SAMPLE_RATE=1.0
|
||||
SENTRY_SEND_DEFAULT_PII=true # OK in dev; disable in production
|
||||
SENTRY_SEND_DEFAULT_PII=true # OK in dev; disable in production
|
||||
|
||||
SENTRY_JS_TRACES_SAMPLE_RATE=1.0 # Capture every browser navigation
|
||||
SENTRY_JS_REPLAY_SESSION_SAMPLE_RATE=1.0 # Record every session
|
||||
SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE=1.0
|
||||
```
|
||||
|
||||
### Staging
|
||||
@@ -55,6 +71,10 @@ SENTRY_ENVIRONMENT=staging
|
||||
SENTRY_TRACES_SAMPLE_RATE=0.5
|
||||
SENTRY_PROFILES_SAMPLE_RATE=0.0
|
||||
SENTRY_SEND_DEFAULT_PII=false
|
||||
|
||||
SENTRY_JS_TRACES_SAMPLE_RATE=0.5
|
||||
SENTRY_JS_REPLAY_SESSION_SAMPLE_RATE=0.1
|
||||
SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE=1.0
|
||||
```
|
||||
|
||||
### Production
|
||||
@@ -62,9 +82,13 @@ SENTRY_SEND_DEFAULT_PII=false
|
||||
```bash
|
||||
SENTRY_DSN=https://<key>@o<org>.ingest.sentry.io/<project>
|
||||
SENTRY_ENVIRONMENT=production
|
||||
SENTRY_TRACES_SAMPLE_RATE=0.1 # 10 % sampling keeps quota low
|
||||
SENTRY_TRACES_SAMPLE_RATE=0.1 # 10 % sampling keeps quota low
|
||||
SENTRY_PROFILES_SAMPLE_RATE=0.0
|
||||
SENTRY_SEND_DEFAULT_PII=false # Default — required for GDPR compliance
|
||||
SENTRY_SEND_DEFAULT_PII=false # Default — required for GDPR compliance
|
||||
|
||||
SENTRY_JS_TRACES_SAMPLE_RATE=0.1
|
||||
SENTRY_JS_REPLAY_SESSION_SAMPLE_RATE=0.0 # Disabled; rely on error-triggered replay
|
||||
SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE=0.1
|
||||
```
|
||||
|
||||
---
|
||||
@@ -80,6 +104,8 @@ services:
|
||||
SENTRY_DSN: "https://<key>@o<org>.ingest.sentry.io/<project>"
|
||||
SENTRY_ENVIRONMENT: "production"
|
||||
SENTRY_TRACES_SAMPLE_RATE: "0.1"
|
||||
SENTRY_JS_TRACES_SAMPLE_RATE: "0.1"
|
||||
SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE: "0.1"
|
||||
|
||||
worker:
|
||||
environment:
|
||||
@@ -88,6 +114,8 @@ services:
|
||||
SENTRY_TRACES_SAMPLE_RATE: "0.1"
|
||||
```
|
||||
|
||||
> The `worker` service only needs server-side variables — the Browser SDK runs in the user's browser and is configured via the `api` service.
|
||||
|
||||
---
|
||||
|
||||
## Kubernetes
|
||||
@@ -110,6 +138,10 @@ env:
|
||||
value: production
|
||||
- name: SENTRY_TRACES_SAMPLE_RATE
|
||||
value: "0.1"
|
||||
- name: SENTRY_JS_TRACES_SAMPLE_RATE
|
||||
value: "0.1"
|
||||
- name: SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE
|
||||
value: "0.1"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -132,17 +164,24 @@ env:
|
||||
|
||||
Log messages at `ERROR` level and above are automatically forwarded to Sentry as events. Messages at `INFO` level are recorded as breadcrumbs (contextual trail leading up to an error).
|
||||
|
||||
### Browser (JavaScript SDK)
|
||||
|
||||
- **Client-side errors** — unhandled JavaScript exceptions and Promise rejections are captured automatically with browser context (URL, user agent, breadcrumbs).
|
||||
- **Browser performance** — page load, navigation, and resource timing data is captured as Sentry transactions when `SENTRY_JS_TRACES_SAMPLE_RATE > 0`.
|
||||
- **Session Replay** — screen recordings of user sessions (or just sessions containing errors) can be captured when the relevant replay sample rates are set above `0.0`. Replay data helps reproduce and diagnose hard-to-find UI bugs.
|
||||
|
||||
---
|
||||
|
||||
## Disabling Sentry
|
||||
|
||||
Simply leave `SENTRY_DSN` unset (or set it to an empty string). The SDK is never initialised and no data is sent.
|
||||
Simply leave `SENTRY_DSN` unset (or set it to an empty string). Neither the Python SDK nor the Browser SDK `<script>` tag is loaded, and no data is sent.
|
||||
|
||||
---
|
||||
|
||||
## SDK Version
|
||||
|
||||
DocuElevate uses [`sentry-sdk`](https://pypi.org/project/sentry-sdk/) `>=2.20.0,<3.0.0` with the `fastapi`, `celery`, and `sqlalchemy` extras.
|
||||
- **Server:** DocuElevate uses [`sentry-sdk`](https://pypi.org/project/sentry-sdk/) `>=2.20.0,<3.0.0` with the `fastapi`, `celery`, and `sqlalchemy` extras.
|
||||
- **Browser:** The `bundle.tracing.replay.min.js` bundle from the Sentry Browser SDK **v10** is loaded from the official Sentry CDN (`browser.sentry-cdn.com`). The version pin is in `frontend/templates/base.html`.
|
||||
|
||||
---
|
||||
|
||||
@@ -154,6 +193,13 @@ DocuElevate uses [`sentry-sdk`](https://pypi.org/project/sentry-sdk/) `>=2.20.0,
|
||||
2. Check the application logs for the `Sentry initialised` message at startup. If it is absent, the DSN is not being read — confirm the environment variable name is `SENTRY_DSN`, or check the **Settings → Observability** section of the admin UI if you configured it there.
|
||||
3. Test with `SENTRY_TRACES_SAMPLE_RATE=1.0` so that every request is sent.
|
||||
|
||||
### No browser errors in Sentry
|
||||
|
||||
1. Confirm `SENTRY_DSN` is set — the Browser SDK `<script>` tag is only injected when the DSN is non-empty.
|
||||
2. Open browser DevTools → Network and verify that the Sentry CDN bundle (`bundle.tracing.replay.min.js`) loads successfully (HTTP 200).
|
||||
3. Open DevTools → Console and run `window.Sentry` — it should be an object if the SDK loaded correctly.
|
||||
4. Check that `SENTRY_JS_TRACES_SAMPLE_RATE` and replay rates are set to values > 0 if you expect performance / replay data (they default to `0.0`).
|
||||
|
||||
### `sentry-sdk` import error
|
||||
|
||||
```
|
||||
@@ -164,11 +210,13 @@ Run `pip install 'sentry-sdk[fastapi,celery,sqlalchemy]'` inside your container,
|
||||
|
||||
### PII / GDPR concerns
|
||||
|
||||
By default `SENTRY_SEND_DEFAULT_PII=false`, which prevents IP addresses and user agents from being attached to events. Review Sentry's [data management documentation](https://docs.sentry.io/product/data-management-settings/) and your organisation's privacy policy before enabling PII.
|
||||
By default `SENTRY_SEND_DEFAULT_PII=false`, which prevents IP addresses and user agents from being attached to server-side events. Session Replay data may capture user interactions — review [Sentry's privacy documentation](https://docs.sentry.io/product/session-replay/privacy/) and your organisation's privacy policy before enabling replay.
|
||||
|
||||
### High Sentry quota usage
|
||||
|
||||
Lower `SENTRY_TRACES_SAMPLE_RATE` (e.g. `0.05` for 5 %) or set it to `0.0` to disable performance tracing entirely.
|
||||
- Lower `SENTRY_TRACES_SAMPLE_RATE` (e.g. `0.05` for 5 %) or set it to `0.0` to disable server-side performance tracing.
|
||||
- Set `SENTRY_JS_TRACES_SAMPLE_RATE=0.0` to disable browser performance tracing.
|
||||
- Set `SENTRY_JS_REPLAY_SESSION_SAMPLE_RATE=0.0` and `SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE=0.0` to disable Session Replay entirely.
|
||||
|
||||
---
|
||||
|
||||
@@ -179,6 +227,7 @@ Lower `SENTRY_TRACES_SAMPLE_RATE` (e.g. `0.05` for 5 %) or set it to `0.0` to di
|
||||
- Use **release tracking**: DocuElevate automatically sets `release` to the current `VERSION` string, enabling you to correlate errors with specific releases.
|
||||
- Set up **performance baselines** using Sentry's Performance dashboard so that you can detect regressions after deployments.
|
||||
- Review the [Sentry Python documentation](https://docs.sentry.io/platforms/python/) for advanced configuration options such as custom tags, user context, and scrubbing sensitive data.
|
||||
- Review the [Sentry Browser SDK documentation](https://docs.sentry.io/platforms/javascript/) for advanced browser SDK configuration.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ Settings are organized into logical categories for easy navigation:
|
||||
- **Authentication**: Login settings, session secrets, OAuth configuration, admin group
|
||||
- **AI Services**: AI provider selection, model configuration, embeddings, and credentials (OpenAI, Azure, Anthropic, Gemini, Ollama, OpenRouter, Portkey, LiteLLM)
|
||||
- **OCR Engines**: OCR provider selection and configuration (Tesseract, EasyOCR, Mistral, Google DocAI, AWS Textract)
|
||||
- **Storage Providers**: Dropbox, Google Drive, OneDrive, S3, FTP, SFTP, WebDAV, Nextcloud, Paperless
|
||||
- **Storage Providers**: Dropbox, Google Drive, OneDrive, SharePoint, S3, FTP, SFTP, WebDAV, Nextcloud, Paperless
|
||||
- **Email**: SMTP configuration for sending emails
|
||||
- **IMAP**: Email ingestion configuration (supports two mailbox accounts)
|
||||
- **Monitoring**: Uptime Kuma integration
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# Setting up SharePoint Integration
|
||||
|
||||
This guide explains how to set up the Microsoft SharePoint Online integration for DocuElevate.
|
||||
|
||||
## Required Configuration Parameters
|
||||
|
||||
| **Variable** | **Description** |
|
||||
|---------------------------------|-------------------------------------------------------|
|
||||
| `SHAREPOINT_CLIENT_ID` | Azure AD application client ID |
|
||||
| `SHAREPOINT_CLIENT_SECRET` | Azure AD application client secret |
|
||||
| `SHAREPOINT_TENANT_ID` | Azure AD tenant ID (use "common" for multi-tenant apps) |
|
||||
| `SHAREPOINT_REFRESH_TOKEN` | OAuth 2.0 refresh token |
|
||||
| `SHAREPOINT_SITE_URL` | SharePoint site URL (e.g. `https://tenant.sharepoint.com/sites/sitename`) |
|
||||
| `SHAREPOINT_DOCUMENT_LIBRARY` | Document library name (default: `Documents`) |
|
||||
| `SHAREPOINT_FOLDER_PATH` | Subfolder path inside the document library |
|
||||
|
||||
For a complete list of configuration options, see the [Configuration Guide](ConfigurationGuide.md).
|
||||
|
||||
## Overview
|
||||
|
||||
SharePoint Online integration uses the same Microsoft Graph API as OneDrive. The key difference is that SharePoint targets a **site-specific document library** rather than a personal OneDrive. Documents are uploaded via chunked upload sessions for reliability with large files.
|
||||
|
||||
> **Tip:** If you already have an Azure AD app registration for OneDrive, you can reuse it for SharePoint — just add the `Sites.ReadWrite.All` permission.
|
||||
|
||||
## Setup Steps
|
||||
|
||||
### 1. Register an application in Azure Active Directory
|
||||
|
||||
If you don't already have an app registration (e.g. from OneDrive setup):
|
||||
|
||||
1. Go to the [Azure Portal](https://portal.azure.com/)
|
||||
2. Navigate to **Azure Active Directory** > **App registrations**
|
||||
3. Click **New registration**
|
||||
4. Enter a name for your application (e.g., "DocuElevate")
|
||||
5. For **Supported account types**, select:
|
||||
- **Single tenant**: "Accounts in this organizational directory only"
|
||||
- **Multi-tenant**: "Accounts in any organizational directory"
|
||||
6. For **Redirect URI**, select "Web" and enter your callback URL (e.g., `https://your-domain.com/onedrive-callback`)
|
||||
7. Click **Register**
|
||||
|
||||
### 2. Get Application (client) ID
|
||||
|
||||
1. After registration, note the **Application (client) ID** from the overview page
|
||||
2. Set this value as `SHAREPOINT_CLIENT_ID`
|
||||
|
||||
### 3. Create a client secret
|
||||
|
||||
1. In your application page, go to **Certificates & secrets**
|
||||
2. Under **Client secrets**, click **New client secret**
|
||||
3. Add a description and select an expiration period
|
||||
4. Click **Add** and immediately copy the secret value (it will only be shown once)
|
||||
5. Set this value as `SHAREPOINT_CLIENT_SECRET`
|
||||
|
||||
### 4. Configure API permissions
|
||||
|
||||
1. In your application page, go to **API permissions**
|
||||
2. Click **Add a permission**
|
||||
3. Select **Microsoft Graph**
|
||||
4. For **delegated permissions** (user-context access), add:
|
||||
- `Sites.ReadWrite.All` — Read and write items in all site collections
|
||||
- `offline_access` — Required for refresh tokens
|
||||
5. For **application permissions** (app-only access without a user), add:
|
||||
- `Sites.ReadWrite.All` — Read and write items in all site collections
|
||||
6. Click **Add permissions**
|
||||
7. Click **Grant admin consent** (requires admin privileges)
|
||||
|
||||
> **Important:** SharePoint access requires `Sites.ReadWrite.All` rather than the `Files.ReadWrite` permission used by OneDrive.
|
||||
|
||||
### 5. Get your Tenant ID
|
||||
|
||||
1. In the Azure Portal, find your **Tenant ID** (also called "Directory ID")
|
||||
2. It is on the **Azure Active Directory** overview page
|
||||
3. Set this value as `SHAREPOINT_TENANT_ID`
|
||||
|
||||
### 6. Generate a Refresh Token
|
||||
|
||||
#### Using the OneDrive Auth Wizard
|
||||
|
||||
The SharePoint integration reuses the same MSAL token flow as OneDrive:
|
||||
|
||||
1. Navigate to `/onedrive-setup`
|
||||
2. Enter your SharePoint Client ID and Tenant ID
|
||||
3. Click **Start Authentication Flow** and follow the prompts
|
||||
4. Copy the generated refresh token and set it as `SHAREPOINT_REFRESH_TOKEN`
|
||||
|
||||
#### Manual Method
|
||||
|
||||
1. Open the following URL in your browser (replace placeholders):
|
||||
```
|
||||
https://login.microsoftonline.com/YOUR_TENANT_ID/oauth2/v2.0/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REDIRECT_URI&response_mode=query&scope=https://graph.microsoft.com/.default offline_access&prompt=consent
|
||||
```
|
||||
2. Sign in with your Microsoft work account
|
||||
3. After authentication, copy the `code` parameter from the redirect URL
|
||||
4. Exchange the code for tokens:
|
||||
```bash
|
||||
curl -X POST https://login.microsoftonline.com/YOUR_TENANT_ID/oauth2/v2.0/token \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "client_id=YOUR_CLIENT_ID&scope=https://graph.microsoft.com/.default offline_access&code=YOUR_AUTH_CODE&redirect_uri=YOUR_REDIRECT_URI&grant_type=authorization_code&client_secret=YOUR_CLIENT_SECRET"
|
||||
```
|
||||
5. From the response JSON, copy the `refresh_token` value
|
||||
6. Set this as `SHAREPOINT_REFRESH_TOKEN`
|
||||
|
||||
### 7. Find your SharePoint Site URL
|
||||
|
||||
Your SharePoint site URL follows the pattern:
|
||||
```
|
||||
https://YOUR-TENANT.sharepoint.com/sites/SITE-NAME
|
||||
```
|
||||
|
||||
For example:
|
||||
- `https://contoso.sharepoint.com/sites/documents`
|
||||
- `https://contoso.sharepoint.com/sites/engineering-team`
|
||||
|
||||
Set this as `SHAREPOINT_SITE_URL`.
|
||||
|
||||
### 8. Choose your Document Library
|
||||
|
||||
Each SharePoint site has one or more document libraries. The default library is usually called `Documents` (or `Shared Documents`). You can find your library names by navigating to your SharePoint site in a browser and looking at the left sidebar.
|
||||
|
||||
Set the library name as `SHAREPOINT_DOCUMENT_LIBRARY` (default: `Documents`).
|
||||
|
||||
### 9. Set the Upload Folder (Optional)
|
||||
|
||||
If you want documents to be uploaded into a subfolder inside the library, set `SHAREPOINT_FOLDER_PATH`. For example, `Uploads` or `DocuElevate/Processed`.
|
||||
|
||||
## App-Only Access (No User Token)
|
||||
|
||||
For fully automated scenarios without user interaction:
|
||||
|
||||
1. Add **Application permissions** (not Delegated) for `Sites.ReadWrite.All`
|
||||
2. Grant admin consent
|
||||
3. Set `SHAREPOINT_TENANT_ID` to your organization's tenant ID
|
||||
4. Leave `SHAREPOINT_REFRESH_TOKEN` empty — the app will use the client credentials flow
|
||||
|
||||
> **Note:** Client credentials flow requires a specific tenant ID (not "common").
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
**With Refresh Token (Delegated Permissions):**
|
||||
```dotenv
|
||||
SHAREPOINT_CLIENT_ID=12345678-1234-1234-1234-123456789012
|
||||
SHAREPOINT_CLIENT_SECRET=your_client_secret
|
||||
SHAREPOINT_TENANT_ID=87654321-4321-4321-4321-210987654321
|
||||
SHAREPOINT_REFRESH_TOKEN=your_refresh_token
|
||||
SHAREPOINT_SITE_URL=https://contoso.sharepoint.com/sites/documents
|
||||
SHAREPOINT_DOCUMENT_LIBRARY=Documents
|
||||
SHAREPOINT_FOLDER_PATH=Uploads
|
||||
```
|
||||
|
||||
**App-Only Access (Application Permissions):**
|
||||
```dotenv
|
||||
SHAREPOINT_CLIENT_ID=12345678-1234-1234-1234-123456789012
|
||||
SHAREPOINT_CLIENT_SECRET=your_client_secret
|
||||
SHAREPOINT_TENANT_ID=87654321-4321-4321-4321-210987654321
|
||||
# No refresh token needed for app-only access
|
||||
SHAREPOINT_SITE_URL=https://contoso.sharepoint.com/sites/documents
|
||||
SHAREPOINT_DOCUMENT_LIBRARY=Shared Documents
|
||||
SHAREPOINT_FOLDER_PATH=DocuElevate/Processed
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Failed to resolve SharePoint site"
|
||||
|
||||
- Verify `SHAREPOINT_SITE_URL` is correct and accessible
|
||||
- Ensure your app has `Sites.ReadWrite.All` permission with admin consent
|
||||
- Check that the site exists and your account has access to it
|
||||
|
||||
### "Document library not found"
|
||||
|
||||
- Verify the library name in `SHAREPOINT_DOCUMENT_LIBRARY` matches exactly (case-insensitive)
|
||||
- Navigate to your SharePoint site in a browser to confirm the library name
|
||||
- Common names: `Documents`, `Shared Documents`
|
||||
|
||||
### Token errors
|
||||
|
||||
- If using a refresh token, try re-authorizing via the OAuth flow
|
||||
- Ensure `offline_access` scope is included in your permissions
|
||||
- For app-only access, verify the tenant ID is not set to "common"
|
||||
|
||||
### Permission errors
|
||||
|
||||
- Ensure an admin has granted consent for `Sites.ReadWrite.All`
|
||||
- Verify the app registration has the correct permissions
|
||||
- Check that the site's sharing settings allow API access
|
||||
@@ -1,10 +1,10 @@
|
||||
# Social Login Setup Guide
|
||||
|
||||
This guide explains how to configure social login providers (Google, Microsoft, Apple, Dropbox) for DocuElevate. Social login lets your users sign in with their existing accounts, reducing friction and eliminating the need for separate passwords.
|
||||
This guide explains how to configure social login providers (Google, Microsoft, Apple, Dropbox, GitHub) for DocuElevate. Social login lets your users sign in with their existing accounts, reducing friction and eliminating the need for separate passwords.
|
||||
|
||||
## Overview
|
||||
|
||||
DocuElevate supports four social login providers:
|
||||
DocuElevate supports five social login providers, plus additional SSO options:
|
||||
|
||||
| Provider | Protocol | Best For |
|
||||
|----------|----------|----------|
|
||||
@@ -12,9 +12,15 @@ DocuElevate supports four social login providers:
|
||||
| **Microsoft** | OAuth2 / OpenID Connect | Microsoft 365 / Azure AD organizations and personal Microsoft accounts |
|
||||
| **Apple** | OAuth2 / OpenID Connect | iOS/macOS users, privacy-focused users |
|
||||
| **Dropbox** | OAuth2 | Teams already using Dropbox as a storage destination |
|
||||
| **GitHub** | OAuth2 | Developer teams and open-source organizations |
|
||||
| **Keycloak** | OpenID Connect | Self-hosted identity management |
|
||||
| **Generic OAuth2** | OAuth2 | Any OAuth2-compatible identity provider |
|
||||
| **SAML2** | SAML 2.0 | Enterprise identity providers (Okta, ADFS, etc.) |
|
||||
|
||||
Each provider is **independently enabled** — you can use one, several, or all of them at the same time. Social login works alongside any other DocuElevate authentication method (simple auth, OIDC/Authentik, local signup).
|
||||
|
||||
> **Tip:** You can also configure providers through the admin **Connections** page at `/admin/connections`, which provides a wizard-like interface for setting up authentication services.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before configuring any social login provider, ensure:
|
||||
@@ -42,6 +48,7 @@ For example, if your DocuElevate instance is at `https://docuelevate.example.com
|
||||
| Microsoft | `https://docuelevate.example.com/social-callback/microsoft` |
|
||||
| Apple | `https://docuelevate.example.com/social-callback/apple` |
|
||||
| Dropbox | `https://docuelevate.example.com/social-callback/dropbox` |
|
||||
| GitHub | `https://docuelevate.example.com/social-callback/github` |
|
||||
|
||||
---
|
||||
|
||||
@@ -257,6 +264,86 @@ docker compose restart api worker
|
||||
|
||||
---
|
||||
|
||||
## GitHub Sign-In
|
||||
|
||||
### 1. Create an OAuth App in GitHub
|
||||
|
||||
1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
|
||||
2. Click **OAuth Apps** → **New OAuth App**
|
||||
3. Fill in the required fields:
|
||||
- **Application name**: `DocuElevate` (or your preferred name)
|
||||
- **Homepage URL**: `https://docuelevate.example.com`
|
||||
- **Authorization callback URL**: `https://docuelevate.example.com/social-callback/github`
|
||||
4. Click **Register application**
|
||||
5. Copy the **Client ID**
|
||||
6. Click **Generate a new client secret** and copy the secret immediately (it won't be shown again)
|
||||
|
||||
### 2. Configure DocuElevate
|
||||
|
||||
Add these environment variables to your `.env` file:
|
||||
|
||||
```bash
|
||||
SOCIAL_AUTH_GITHUB_ENABLED=true
|
||||
SOCIAL_AUTH_GITHUB_CLIENT_ID=your-github-client-id
|
||||
SOCIAL_AUTH_GITHUB_CLIENT_SECRET=your-github-client-secret
|
||||
```
|
||||
|
||||
### 3. Restart DocuElevate
|
||||
|
||||
```bash
|
||||
docker compose restart api worker
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- **Scopes requested**: `read:user` and `user:email` — DocuElevate only reads the user's public profile and primary email address
|
||||
- **Organization restrictions**: If your GitHub organization restricts OAuth app access, an organization owner must approve the DocuElevate OAuth app
|
||||
- **Private email**: If a user's email is private on GitHub, DocuElevate will request it via the `user:email` scope
|
||||
|
||||
---
|
||||
|
||||
## Keycloak SSO
|
||||
|
||||
### 1. Create a Client in Keycloak
|
||||
|
||||
1. Log in to your Keycloak admin console
|
||||
2. Select (or create) a realm
|
||||
3. Go to **Clients** → **Create client**
|
||||
4. Set **Client type** to `OpenID Connect`
|
||||
5. Set **Client ID** (e.g., `docuelevate`)
|
||||
6. Enable **Client authentication** (confidential)
|
||||
7. Add `https://docuelevate.example.com/social-callback/keycloak` to **Valid redirect URIs**
|
||||
8. Save and copy the **Client secret** from the **Credentials** tab
|
||||
|
||||
### 2. Configure DocuElevate
|
||||
|
||||
```bash
|
||||
SOCIAL_AUTH_KEYCLOAK_ENABLED=true
|
||||
SOCIAL_AUTH_KEYCLOAK_CLIENT_ID=docuelevate
|
||||
SOCIAL_AUTH_KEYCLOAK_CLIENT_SECRET=your-keycloak-client-secret
|
||||
SOCIAL_AUTH_KEYCLOAK_SERVER_URL=https://keycloak.example.com
|
||||
SOCIAL_AUTH_KEYCLOAK_REALM=your-realm-name
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Generic OAuth2 SSO
|
||||
|
||||
For any OAuth2-compatible identity provider not listed above:
|
||||
|
||||
```bash
|
||||
SOCIAL_AUTH_GENERIC_OAUTH2_ENABLED=true
|
||||
SOCIAL_AUTH_GENERIC_OAUTH2_CLIENT_ID=your-client-id
|
||||
SOCIAL_AUTH_GENERIC_OAUTH2_CLIENT_SECRET=your-client-secret
|
||||
SOCIAL_AUTH_GENERIC_OAUTH2_AUTHORIZE_URL=https://idp.example.com/oauth/authorize
|
||||
SOCIAL_AUTH_GENERIC_OAUTH2_TOKEN_URL=https://idp.example.com/oauth/token
|
||||
SOCIAL_AUTH_GENERIC_OAUTH2_USERINFO_URL=https://idp.example.com/oauth/userinfo
|
||||
SOCIAL_AUTH_GENERIC_OAUTH2_SCOPE=openid profile email
|
||||
SOCIAL_AUTH_GENERIC_OAUTH2_NAME=My Identity Provider
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Unified Authentication and Storage
|
||||
|
||||
One of the key advantages of social login in DocuElevate is the potential for **unified authentication** — using the same identity for both signing in and accessing cloud storage destinations:
|
||||
@@ -266,6 +353,7 @@ One of the key advantages of social login in DocuElevate is the potential for **
|
||||
| Google | Google Drive | User already has a Google identity for Drive integration |
|
||||
| Microsoft | OneDrive | User already has a Microsoft identity for OneDrive integration |
|
||||
| Dropbox | Dropbox | User already has a Dropbox identity for Dropbox integration |
|
||||
| GitHub | *(none)* | Developer-friendly authentication for technical teams |
|
||||
| Apple | *(none)* | Provides a familiar, privacy-respecting login option |
|
||||
|
||||
When a user signs in with a social provider that matches a configured storage destination, the administrator can leverage the same OAuth credentials or simplify the integration setup. Note that the storage integration credentials are configured separately in the admin settings — social login establishes the user's identity, not their storage permissions.
|
||||
|
||||
@@ -341,6 +341,7 @@ in task messages or logs.
|
||||
| `S3` | boto3 `upload_file`, per-user access key |
|
||||
| `GOOGLE_DRIVE` | Google Drive API v3, OAuth or service account |
|
||||
| `ONEDRIVE` | Microsoft Graph API, MSAL confidential-client |
|
||||
| `SHAREPOINT` | Microsoft Graph API, site/drive resolution + chunked upload |
|
||||
| `WEBDAV` | HTTP PUT request, Basic Auth |
|
||||
| `NEXTCLOUD` | WebDAV (same as WEBDAV, Nextcloud-compatible path) |
|
||||
| `FTP` | ftplib FTPS (TLS preferred, plaintext configurable) |
|
||||
|
||||
+79
-3
@@ -87,7 +87,7 @@ DocuElevate provides multiple convenient ways to upload documents to the system.
|
||||
|
||||
#### Supported File Types
|
||||
- **Documents**: PDF, Word (.doc, .docx), Excel (.xls, .xlsx), PowerPoint (.ppt, .pptx)
|
||||
- **Images**: JPEG, PNG, GIF, BMP, TIFF, WebP, SVG
|
||||
- **Images**: JPEG, PNG, GIF, BMP, TIFF, WebP, SVG, HEIC, HEIF
|
||||
- **Text**: Plain text (.txt), CSV, RTF, HTML, XML, Markdown
|
||||
- **Maximum file size**: 500MB per file
|
||||
|
||||
@@ -170,7 +170,7 @@ The **Integrations** page (`/integrations`) provides a unified view of all your
|
||||
- **S3** — bucket, region, access key, secret key
|
||||
- **WebDAV / Nextcloud** — URL, folder, username, password
|
||||
- **FTP / SFTP** — host, port, remote path, username, password
|
||||
- **Dropbox / Google Drive / OneDrive** — folder path, with a link to the OAuth setup page
|
||||
- **Dropbox / Google Drive / OneDrive / SharePoint** — folder path, with a link to the OAuth setup page
|
||||
- **Email Forward** — recipient email address
|
||||
- **Watch Folder** — source type (Local, S3, Dropbox, Google Drive, OneDrive, Nextcloud, WebDAV), per-type config fields, delete after processing toggle
|
||||
- **Paperless NGX** — URL and API token
|
||||
@@ -582,7 +582,25 @@ Processing pipelines let you define exactly what happens to your documents when
|
||||
| `embed_metadata` | Write extracted metadata into the PDF document properties |
|
||||
| `compute_embedding` | Compute semantic embeddings for similarity search |
|
||||
| `send_to_destinations` | Upload the processed document to all configured storage destinations |
|
||||
| `classify` | Classify the document type with AI |
|
||||
| `classify` | Classify the document type using rules (filename patterns, content keywords, metadata) |
|
||||
|
||||
#### Classify step – rule-based document classification
|
||||
|
||||
The `classify` step assigns a category to each document by evaluating **built-in** and **custom** classification rules. Rules are matched against three signals:
|
||||
|
||||
- **Filename patterns** — regex matched against the original filename (e.g. `(?i)invoice` matches filenames containing "invoice").
|
||||
- **Content keywords** — pipe-separated keywords matched against the OCR text (e.g. `invoice number|amount due`).
|
||||
- **Metadata match** — `field=value` matched against existing AI metadata (e.g. `document_type=Invoice`).
|
||||
|
||||
**Pre-built categories** include: Invoice, Contract, Receipt, Letter, Report, Bank Statement, Tax Document, Insurance, and Payslip. You can also define your own custom categories.
|
||||
|
||||
The classification result is stored in the document's `ai_metadata` under the `classification` key with the matched category, confidence score, and list of matched rules. If no `document_type` was previously set by AI metadata extraction, the classify step will also populate it.
|
||||
|
||||
> **Tip:** Manage custom classification rules via **Settings → Classification Rules** or the `/api/classification-rules/` API. See the [API Documentation](./API.md#classification-rules) for details.
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `use_builtin_rules` | boolean | `true` | Include the pre-built classification rules |
|
||||
|
||||
#### OCR step options
|
||||
|
||||
@@ -687,6 +705,64 @@ You can test your rules without actually routing a document using the
|
||||
**evaluate** endpoint (`POST /api/routing-rules/evaluate`). For the full
|
||||
API reference, see [API Documentation](API.md#routing-rules).
|
||||
|
||||
## Comments & Annotations
|
||||
|
||||
The file detail page includes a **collaboration panel** for threaded
|
||||
comments and PDF annotations, allowing team members to discuss documents
|
||||
directly within DocuElevate.
|
||||
|
||||
### Comments
|
||||
|
||||
The **Comments** panel is on the left side of the collaboration section at
|
||||
the bottom of the file detail page.
|
||||
|
||||
#### Viewing Comments
|
||||
Open any file's detail page (`/files/{id}/detail`). Existing comments load
|
||||
automatically, displayed in a threaded tree — replies are nested under their
|
||||
parent.
|
||||
|
||||
#### Adding a Comment
|
||||
1. Type your comment in the text area at the bottom of the Comments panel.
|
||||
2. Use `@username` to mention another user — an autocomplete dropdown
|
||||
appears as you type after the `@` symbol. Use arrow keys and Enter to
|
||||
select a user.
|
||||
3. Click **Add comment** to post.
|
||||
|
||||
#### Replying to a Comment
|
||||
Click the **Reply** button on any top-level comment. A reply text area
|
||||
appears inline; type your response and click **Reply** to post.
|
||||
|
||||
#### Editing & Deleting
|
||||
You can edit or delete your own comments using the **Edit** and trash
|
||||
buttons. Edits re-extract @mentions automatically.
|
||||
|
||||
#### Resolving Threads
|
||||
Click **Resolve** on a top-level comment to mark the thread as resolved
|
||||
(shown with a green badge). Click **Reopen** to re-open it.
|
||||
|
||||
### Annotations
|
||||
|
||||
The **Annotations** panel is on the right side of the collaboration
|
||||
section.
|
||||
|
||||
#### Adding an Annotation
|
||||
1. Type the annotation content in the text area.
|
||||
2. Set the **Page** number the annotation refers to.
|
||||
3. Choose a **Type**: Note, Highlight, Underline, or Strikethrough.
|
||||
4. Pick a **Color** using the color picker.
|
||||
5. Click **Add annotation** to save.
|
||||
|
||||
#### Editing & Deleting
|
||||
You can edit or delete your own annotations using the pencil and trash
|
||||
buttons. When editing, you can also change the annotation type.
|
||||
|
||||
### @Mention Autocomplete
|
||||
|
||||
When typing `@` followed by characters in the comment input, an
|
||||
autocomplete dropdown shows matching users (sourced from the
|
||||
`/api/users/mentionable` endpoint). Navigate with arrow keys and press
|
||||
Enter or click to insert the mention.
|
||||
|
||||
## API Access
|
||||
|
||||
For programmatic access, DocuElevate provides a comprehensive REST API:
|
||||
|
||||
Reference in New Issue
Block a user