Merge remote-tracking branch 'origin/main' into copilot/fix-watch-folder-settings

# Conflicts:
#	app/views/dropbox.py
#	frontend/templates/dropbox.html
#	frontend/templates/integrations_dashboard.html
#	tests/test_api_dropbox.py
#	tests/test_views_dropbox.py
This commit is contained in:
copilot-swe-agent[bot]
2026-03-20 23:33:58 +00:00
385 changed files with 330000 additions and 3150 deletions
+502 -15
View File
@@ -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/
@@ -1620,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.
@@ -2009,6 +2073,160 @@ Pass no `pipeline_id` query parameter (or omit it) to clear the assignment.
```
## Routing Rules
Routing rules let you conditionally assign documents to different pipelines
based on file properties such as type, size, filename, or AI-extracted
metadata. Rules are evaluated in **position order** (lowest first); the first
rule that matches wins. If no rule matches, the system falls back to the
owner's (or global) default pipeline.
### Supported operators and fields
```bash
GET /api/routing-rules/operators
```
Returns the catalogue of valid operators and built-in fields so UIs can
populate dropdowns without hard-coding values.
**Response (200):**
```json
{
"operators": ["contains", "equals", "gt", "gte", "lt", "lte", "not_contains", "not_equals", "regex"],
"builtin_fields": ["category", "document_type", "file_type", "filename", "size"],
"metadata_prefix": "metadata."
}
```
> **Tip:** For AI metadata fields use the `metadata.` prefix, e.g.
> `metadata.sender`, `metadata.amount`.
### List routing rules
```bash
GET /api/routing-rules
```
Returns the current user's rules **plus** any system-wide rules
(`owner_id = null`), ordered by position.
**Response (200):**
```json
[
{
"id": 1,
"owner_id": "alice",
"name": "Route invoices",
"position": 0,
"field": "document_type",
"operator": "equals",
"value": "Invoice",
"target_pipeline_id": 3,
"is_active": true,
"created_at": "2026-03-09T12:00:00+00:00",
"updated_at": "2026-03-09T12:00:00+00:00"
}
]
```
### Create routing rule
```bash
POST /api/routing-rules
Content-Type: application/json
{
"name": "Route invoices",
"field": "document_type",
"operator": "equals",
"value": "Invoice",
"target_pipeline_id": 3
}
```
Optional fields: `position` (auto-assigned if omitted), `is_active` (default `true`).
**Response (201 Created):** The created rule object.
### Get routing rule
```bash
GET /api/routing-rules/{rule_id}
```
**Response (200):** A single rule object.
### Update routing rule
```bash
PUT /api/routing-rules/{rule_id}
Content-Type: application/json
{ "name": "Renamed rule", "operator": "contains", "is_active": false }
```
Only the supplied fields are updated.
**Response (200):** The updated rule object.
### Delete routing rule
```bash
DELETE /api/routing-rules/{rule_id}
```
Returns **204 No Content**.
### Reorder routing rules
```bash
PUT /api/routing-rules/reorder
Content-Type: application/json
{ "rule_ids": [3, 1, 2] }
```
Provide the complete ordered list of your rule IDs. Positions are reassigned
0, 1, 2, … in the given order.
### Evaluate rules (dry run)
```bash
POST /api/routing-rules/evaluate
Content-Type: application/json
{
"file_type": "application/pdf",
"filename": "invoice_2024.pdf",
"size": 204800,
"document_type": "Invoice",
"metadata": { "sender": "Acme Corp" }
}
```
Tests which rule (if any) would match the given properties **without**
actually routing a document.
**Response (200) match found:**
```json
{
"matched": true,
"rule": { "id": 1, "name": "Route invoices", "..." : "..." },
"target_pipeline": { "id": 3, "name": "Invoice Pipeline", "is_active": true }
}
```
**Response (200) no match:**
```json
{
"matched": false,
"rule": null,
"target_pipeline": null
}
```
## API Tokens
Personal API tokens allow programmatic access to the DocuElevate API without
@@ -2022,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` (13650). If omitted the token never expires.
**Request:**
```json
{
"name": "CI Pipeline"
"name": "CI Pipeline",
"expires_in_days": 90
}
```
@@ -2042,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"
}
```
@@ -2064,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
@@ -2081,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:
@@ -2112,3 +2345,257 @@ print(response.json())
## Further Assistance
For additional help with the API, please contact our support team or refer to the [Development Guide](../CONTRIBUTING.md).
## Mobile App API
The mobile API provides endpoints used by the native iOS and Android app. All endpoints require authentication (Bearer token or active session cookie).
For full mobile app documentation see [MobileApp.md](./MobileApp.md).
### POST /api/mobile/generate-token
Exchange an active web session for a long-lived API token scoped to the mobile app.
**Request:**
```json
{ "device_name": "John's iPhone" }
```
**Response (201 Created):**
```json
{
"token": "de_AbCdEfGhIjKl...",
"token_id": 42,
"name": "Mobile App John's iPhone",
"created_at": "2026-03-10T09:30:00Z"
}
```
> The `token` is shown **once only**.
### POST /api/mobile/register-device
Register an Expo push token to receive push notifications.
**Request:**
```json
{
"push_token": "ExponentPushToken[xxxxxx]",
"device_name": "John's iPhone",
"platform": "ios"
}
```
**Response (201 Created):** Device record with `id`, `platform`, `is_active`, `created_at`.
### GET /api/mobile/devices
List all registered push-notification devices for the current user.
**Response (200 OK):** Array of device records.
### DELETE /api/mobile/devices/{device_id}
Deactivate or permanently delete a push-notification device:
* **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
Return basic profile information for the authenticated user.
**Response (200 OK):**
```json
{
"owner_id": "john@example.com",
"display_name": "John Doe",
"email": "john@example.com",
"avatar_url": "https://www.gravatar.com/avatar/...",
"is_admin": false
}
```
---
## GraphQL API
DocuElevate exposes a GraphQL API at `/graphql` alongside the REST API. It
supports flexible queries with field selection, making it ideal for dashboards
and integrations that only need a subset of the available data.
### Endpoint
| Method | URL | Description |
|--------|-----|-------------|
| `POST` | `/graphql` | Execute a GraphQL query or mutation |
| `GET` | `/graphql` | Open the GraphiQL interactive playground |
### Authentication
The GraphQL endpoint honours the same authentication rules as the REST API:
- **`AUTH_ENABLED=False`** (default, single-user mode): all queries are
allowed without credentials.
- **`AUTH_ENABLED=True`** (multi-user mode): a valid session cookie **or**
an `Authorization: Bearer <token>` API token is required. Admin-only
queries (settings, users) additionally require the `is_admin` flag.
### Available Queries
| Field | Returns | Notes |
|-------|---------|-------|
| `documents(ownerId, limit, offset)` | `[DocumentType]` | Paginated list of documents |
| `document(id)` | `DocumentType` | Single document by primary key |
| `pipelines(ownerId, limit, offset)` | `[PipelineType]` | Paginated list of pipelines with steps |
| `pipeline(id)` | `PipelineType` | Single pipeline by primary key |
| `settings(limit, offset)` | `[SettingType]` | Non-sensitive app settings (**admin only**) |
| `users(limit, offset)` | `[UserType]` | User profiles (**admin only**) |
| `user(userId)` | `UserType` | Single user profile (**admin only**) |
> **Note:** Sensitive configuration keys (API secrets, passwords, tokens) are
> automatically excluded from the `settings` query regardless of the caller's
> privilege level.
### GraphiQL Playground
Navigate to `http://<your-instance>/graphql` in a browser to open the
interactive GraphiQL IDE, which provides schema documentation, auto-complete,
and the ability to run queries directly.
### Example Queries
**List recent documents:**
```graphql
{
documents(limit: 5) {
id
originalFilename
mimeType
fileSize
documentTitle
createdAt
}
}
```
**Fetch a pipeline with its steps:**
```graphql
{
pipeline(id: 1) {
id
name
description
isDefault
isActive
steps {
position
stepType
label
enabled
}
}
}
```
**List application settings (admin only):**
```graphql
{
settings {
key
value
updatedAt
}
}
```
**List user profiles (admin only):**
```graphql
{
users(limit: 10) {
userId
displayName
subscriptionTier
isBlocked
}
}
```
**Using variables:**
```graphql
query GetDocument($id: Int!) {
document(id: $id) {
id
originalFilename
documentTitle
isDuplicate
ocrQualityScore
}
}
```
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" }
}
}
```
+285
View File
@@ -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/)
+66 -1
View File
@@ -20,10 +20,11 @@ For a complete list of configuration options, see the [Configuration Guide](Conf
## Authentication Methods
DocuElevate supports two primary authentication methods:
DocuElevate supports multiple authentication methods that can be used independently or together:
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))
## Session Security
@@ -153,6 +154,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
@@ -189,3 +246,11 @@ If you encounter issues with authentication:
- For most providers, you can visit the `/.well-known/openid-configuration` endpoint to verify their settings
For more general configuration issues, see the [Configuration Troubleshooting Guide](ConfigurationTroubleshooting.md).
## Social Login
DocuElevate supports social login with Google, Microsoft, Apple, and Dropbox. Social login allows users to authenticate using their existing accounts with these providers, without needing a separate DocuElevate password.
Social login can be used alongside any other authentication method (simple auth, OIDC, local signup). Each social provider is independently configured.
For detailed setup instructions, prerequisites, and provider-specific configuration, see the **[Social Login Setup Guide](SocialLoginSetup.md)**.
+190
View File
@@ -0,0 +1,190 @@
# Compliance Templates Guide
DocuElevate includes pre-built compliance templates for **GDPR**, **HIPAA**, and **SOC 2** that help you configure your instance to meet regulatory requirements. This guide covers how to use the compliance dashboard, apply templates, and monitor your compliance status.
## Overview
The compliance templates feature provides:
- **Pre-built configurations** for GDPR, HIPAA, and SOC 2
- **One-click apply** to configure all required settings at once
- **Compliance status dashboard** to monitor your regulatory posture
- **Individual check results** showing which settings are compliant and which need attention
## Accessing the Dashboard
The compliance dashboard is available to **admin users only**.
1. Log in as an administrator
2. Click **Admin** in the navigation bar
3. Select **Compliance** from the dropdown menu
Or navigate directly to: `/admin/compliance`
## Available Templates
### GDPR (General Data Protection Regulation)
The European Union regulation for data protection and privacy. The GDPR template enforces:
| Setting | Value | Purpose |
|---------|-------|---------|
| `AUTH_ENABLED` | `True` | Controls access to personal data |
| `SENTRY_SEND_DEFAULT_PII` | `False` | Prevents PII leaking to external services |
| `SECURITY_HEADERS_ENABLED` | `True` | Protects against common web vulnerabilities |
| `SECURITY_HEADER_HSTS_ENABLED` | `True` | Ensures encrypted connections |
| `SECURITY_HEADER_CSP_ENABLED` | `True` | Prevents XSS and injection attacks |
| `SECURITY_HEADER_X_FRAME_OPTIONS_ENABLED` | `True` | Prevents clickjacking |
| `ENABLE_DEDUPLICATION` | `True` | Data minimisation — avoids duplicate storage |
### HIPAA (Health Insurance Portability and Accountability Act)
United States regulation for protecting health information. The HIPAA template includes all GDPR settings plus:
| Setting | Value | Purpose |
|---------|-------|---------|
| `MULTI_USER_ENABLED` | `True` | Individual accounts for access accountability |
### SOC 2 (Service Organization Control 2)
Trust Service Criteria framework for service organisations. The SOC 2 template includes the same settings as HIPAA, mapped to SOC 2 Trust Service Criteria (CC6.x, PI1.x).
## Applying a Template
1. Navigate to the **Compliance** dashboard (`/admin/compliance`)
2. Find the template you want to apply (GDPR, HIPAA, or SOC 2)
3. Click **Apply Template**
4. Confirm the action in the dialog
5. The template settings are written to the database immediately
> **Note:** Applying a template writes configuration values to the database. Some settings (e.g., security headers) may require a restart to take effect. Check the Settings page for restart indicators.
## Understanding Compliance Status
Each template shows one of four statuses:
| Status | Badge | Meaning |
|--------|-------|---------|
| **Compliant** | Green | All checks are passing |
| **Partial** | Yellow | Some checks are passing, others are not |
| **Non-Compliant** | Red | No checks are passing |
| **Not Applied** | Grey | Template has never been applied |
### Individual Checks
Click **Show Details** on any template card to see individual check results:
-**Passing** — The setting matches the expected compliance value
-**Failing** — The setting does not match; the current and expected values are shown
## API Endpoints
The compliance feature exposes the following API endpoints under `/api/compliance/`:
### List Templates
```bash
GET /api/compliance/templates
```
Returns all compliance templates with their current status.
### Get Single Template
```bash
GET /api/compliance/templates/{name}
```
Returns a single template by name (`gdpr`, `hipaa`, or `soc2`).
### Apply Template
```bash
POST /api/compliance/templates/{name}/apply
```
Applies a compliance template, writing all its settings to the database.
### Get Template Status
```bash
GET /api/compliance/templates/{name}/status
```
Evaluates the live compliance status of a template against current settings.
**Response example:**
```json
{
"status": "partial",
"total": 7,
"passed": 5,
"failed": 2,
"check_results": [
{
"key": "auth_enabled",
"label": "Authentication enabled",
"description": "User authentication must be enabled to control access to personal data.",
"expected": "True",
"actual": "True",
"passing": true
}
]
}
```
### Compliance Summary
```bash
GET /api/compliance/summary
```
Returns an overall compliance summary across all templates.
**Response example:**
```json
{
"overall_status": "partial",
"total_checks": 22,
"total_passed": 18,
"total_failed": 4,
"templates": [
{
"name": "gdpr",
"display_name": "GDPR (General Data Protection Regulation)",
"enabled": true,
"status": "compliant",
"total": 7,
"passed": 7,
"failed": 0,
"applied_at": "2026-03-09T12:00:00+00:00",
"applied_by": "admin@example.com"
}
]
}
```
> **Note:** All API endpoints require admin authentication.
## Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `COMPLIANCE_ENABLED` | `True` | Enable the compliance templates dashboard. Set to `False` to hide the feature. |
## Best Practices
1. **Apply templates before going live** — Set up compliance before processing real documents
2. **Monitor status regularly** — Check the compliance dashboard after configuration changes
3. **Use the refresh button** — After changing settings elsewhere, refresh the compliance page to see updated status
4. **Combine templates** — You can apply multiple templates; settings overlap is handled automatically
5. **Review after updates** — After upgrading DocuElevate, review your compliance status as new checks may be added
## Related Documentation
- [Configuration Guide](./ConfigurationGuide.md) — Full list of configuration options
- [Privacy & Compliance Guide](./PrivacyCompliance.md) — Privacy notice and GDPR compliance details
- [Deployment Guide](./DeploymentGuide.md) — Production deployment with security best practices
- [Security Audit](../SECURITY_AUDIT.md) — Security findings and mitigations
+384 -2
View File
@@ -11,11 +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
@@ -78,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 50100 or CPU > 1.5× |
| High load | 25 % of base | Queue 100200 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.
@@ -303,6 +333,42 @@ DocuElevate can automatically pull document attachments from IMAP mailboxes —
| `IMAP1_SSL` | Use SSL (`true`/`false`). | `true` |
| `IMAP1_POLL_INTERVAL_MINUTES` | Frequency in minutes to poll for new mail. | `5` |
| `IMAP_READONLY_MODE` | When `true`, fetches and processes attachments but does **not** modify the mailbox (no starring, labeling, deleting, or flag changes). Use for pre-production instances sharing a mailbox with production. Default: `false`. | `false` |
| `IMAP_ATTACHMENT_FILTER` | System-wide fallback for which attachment types are ingested when no ingestion profile is assigned to a mailbox. `documents_only` (default) ingests PDFs and office files only — images are skipped. `all` ingests every supported file type including images. Individual IMAP accounts can override this using ingestion profiles. | `documents_only` |
#### IMAP Ingestion Profiles
For fine-grained control, DocuElevate supports **Ingestion Profiles** — named configurations that let you choose exactly which file-type categories to accept from each mailbox.
Each profile contains a list of enabled **categories**:
| Category | Description |
|----------|-------------|
| `pdf` | PDF documents (`.pdf`) |
| `office` | Microsoft Office files (Word, Excel, PowerPoint — `.docx`, `.xlsx`, `.pptx`, …) |
| `opendocument` | LibreOffice/OpenOffice files (`.odt`, `.ods`, `.odp`, …) |
| `text` | Plain text, CSV and RTF files (`.txt`, `.csv`, `.rtf`) |
| `web` | HTML and Markdown files (`.html`, `.htm`, `.md`, `.markdown`) |
| `images` | Image files (`.jpg`, `.png`, `.gif`, `.bmp`, `.tiff`, `.webp`, `.svg`) |
Two built-in system profiles are seeded automatically:
| Profile | Categories |
|---------|------------|
| **Documents Only** | pdf, office, opendocument, text, web (no images) |
| **All Files** | All categories, including images |
Users can create their own custom profiles via the **Email Ingestion** dashboard (`/imap-accounts`) by clicking the **Manage profiles** link or the **+** button next to the profile dropdown. Custom profiles are private to the creating user and can be freely edited or deleted.
**API endpoints for ingestion profiles:**
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/imap-profiles/` | List all visible profiles (system + user's own) |
| `POST` | `/api/imap-profiles/` | Create a new profile |
| `GET` | `/api/imap-profiles/categories` | List available file-type categories |
| `GET` | `/api/imap-profiles/{id}` | Get a single profile |
| `PUT` | `/api/imap-profiles/{id}` | Update a profile (not built-in) |
| `DELETE` | `/api/imap-profiles/{id}` | Delete a profile (not built-in) |
#### Per-User IMAP Integrations
@@ -327,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`. |
@@ -335,6 +404,28 @@ Credentials are encrypted at rest using Fernet encryption.
| `AUTHENTIK_CONFIG_URL` | Configuration URL for Authentik OpenID Connect. |
| `OAUTH_PROVIDER_NAME` | Display name for the OAuth provider button. |
### 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).
| **Variable** | **Description** | **Default** |
|---|---|---|
| `SOCIAL_AUTH_GOOGLE_ENABLED` | Enable Google Sign-In. | `false` |
| `SOCIAL_AUTH_GOOGLE_CLIENT_ID` | Google OAuth2 client ID from the Google Cloud Console. | *(empty)* |
| `SOCIAL_AUTH_GOOGLE_CLIENT_SECRET` | Google OAuth2 client secret. | *(empty)* |
| `SOCIAL_AUTH_MICROSOFT_ENABLED` | Enable Microsoft Sign-In (Azure AD / Microsoft Entra ID). | `false` |
| `SOCIAL_AUTH_MICROSOFT_CLIENT_ID` | Microsoft application (client) ID from Azure App Registrations. | *(empty)* |
| `SOCIAL_AUTH_MICROSOFT_CLIENT_SECRET` | Microsoft client secret. | *(empty)* |
| `SOCIAL_AUTH_MICROSOFT_TENANT` | Azure AD tenant: `common`, `organizations`, `consumers`, or a tenant GUID. | `common` |
| `SOCIAL_AUTH_APPLE_ENABLED` | Enable Sign in with Apple. | `false` |
| `SOCIAL_AUTH_APPLE_CLIENT_ID` | Apple Services ID (e.g. `com.example.docuelevate`). | *(empty)* |
| `SOCIAL_AUTH_APPLE_TEAM_ID` | Apple Developer Team ID. | *(empty)* |
| `SOCIAL_AUTH_APPLE_KEY_ID` | Apple Sign-In private key ID. | *(empty)* |
| `SOCIAL_AUTH_APPLE_PRIVATE_KEY` | Apple Sign-In private key (PEM format). | *(empty)* |
| `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)* |
### Multi-User Mode
When multi-user mode is enabled, each authenticated user gets their own isolated document space.
@@ -398,6 +489,142 @@ 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.
| **Variable** | **Description** | **Default** |
|--------------------------------|---------------------------------------------------------------------------------------------------|-------------|
| `AUDIT_LOGGING_ENABLED` | Enable the HTTP request audit-logging middleware. | `true` |
| `AUDIT_LOG_INCLUDE_CLIENT_IP` | Include the client IP address in audit log entries. Disable for GDPR-sensitive deployments. | `true` |
#### SIEM Integration
Audit events can be forwarded in real time to external SIEM systems for centralised monitoring, alerting, and long-term retention. Two transports are supported:
* **Syslog** RFC 5424 structured-data messages over UDP or TCP. Works with rsyslog, syslog-ng, Graylog, Datadog, etc.
* **HTTP** JSON POST payloads compatible with Splunk HEC, Logstash HTTP input, Grafana Loki push API, and any generic webhook.
| **Variable** | **Description** | **Default** |
|-------------------------------------|---------------------------------------------------------------------------------------------------|---------------|
| `AUDIT_SIEM_ENABLED` | Enable forwarding of audit events to an external SIEM system. | `false` |
| `AUDIT_SIEM_TRANSPORT` | Transport: `syslog` or `http`. | `syslog` |
| `AUDIT_SIEM_SYSLOG_HOST` | Hostname or IP of the syslog receiver. | `localhost` |
| `AUDIT_SIEM_SYSLOG_PORT` | Port of the syslog receiver. | `514` |
| `AUDIT_SIEM_SYSLOG_PROTOCOL` | Protocol for syslog: `udp` or `tcp`. | `udp` |
| `AUDIT_SIEM_HTTP_URL` | HTTP endpoint URL for SIEM delivery (e.g. Splunk HEC, Logstash, Loki). | *(empty)* |
| `AUDIT_SIEM_HTTP_TOKEN` | Bearer / HEC token for the SIEM HTTP endpoint. | *(empty)* |
| `AUDIT_SIEM_HTTP_CUSTOM_HEADERS` | Comma-separated `Key:Value` extra headers for SIEM HTTP requests. | *(empty)* |
**Example Syslog to rsyslog:**
```bash
AUDIT_SIEM_ENABLED=true
AUDIT_SIEM_TRANSPORT=syslog
AUDIT_SIEM_SYSLOG_HOST=syslog.internal.example.com
AUDIT_SIEM_SYSLOG_PORT=514
AUDIT_SIEM_SYSLOG_PROTOCOL=udp
```
**Example Splunk HEC:**
```bash
AUDIT_SIEM_ENABLED=true
AUDIT_SIEM_TRANSPORT=http
AUDIT_SIEM_HTTP_URL=https://splunk.example.com:8088/services/collector/event
AUDIT_SIEM_HTTP_TOKEN=your-hec-token
```
**Example Logstash HTTP input:**
```bash
AUDIT_SIEM_ENABLED=true
AUDIT_SIEM_TRANSPORT=http
AUDIT_SIEM_HTTP_URL=https://logstash.example.com:8080
AUDIT_SIEM_HTTP_TOKEN=
```
### Rate Limiting
DocuElevate implements rate limiting to protect against DoS attacks and API abuse. **Rate limiting is enabled by default** and uses Redis for distributed rate limiting across multiple workers.
@@ -739,6 +966,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.
@@ -887,6 +1156,7 @@ TESSERACT_LANGUAGE=eng+deu
| **Variable** | **Description** |
|-------------------------------------|-----------------------------------------------------------------------------------------------------|
| `PAPERLESS_ENABLED` | Set to `false` to disable Paperless-ngx uploads without removing credentials. Default: `true` |
| `PAPERLESS_NGX_API_TOKEN` | API token for Paperless NGX. |
| `PAPERLESS_HOST` | Root URL for Paperless NGX (e.g. `https://paperless.example.com`). |
| `PAPERLESS_CUSTOM_FIELD_ABSENDER` | (Optional, Legacy) Name of the custom field in Paperless-ngx to store the sender ("absender") information. If set, the extracted sender will be automatically set as a custom field after document upload. Example: `Absender` or `Sender` |
@@ -929,6 +1199,7 @@ PAPERLESS_CUSTOM_FIELDS_MAPPING='{"absender": "Sender", "empfaenger": "Recipient
| **Variable** | **Description** |
|-------------------------|--------------------------------------------------|
| `DROPBOX_ENABLED` | Set to `false` to disable Dropbox uploads without removing credentials. Default: `true` |
| `DROPBOX_APP_KEY` | Dropbox API app key. |
| `DROPBOX_APP_SECRET` | Dropbox API app secret. |
| `DROPBOX_REFRESH_TOKEN` | OAuth2 refresh token for Dropbox. |
@@ -940,6 +1211,7 @@ For detailed setup instructions, see the [Dropbox Setup Guide](DropboxSetup.md).
| **Variable** | **Description** |
|-------------------------|---------------------------------------------------------------|
| `NEXTCLOUD_ENABLED` | Set to `false` to disable Nextcloud uploads without removing credentials. Default: `true` |
| `NEXTCLOUD_UPLOAD_URL` | Nextcloud WebDAV URL (e.g. `https://nc.example.com/remote.php/dav/files/<USERNAME>`). |
| `NEXTCLOUD_USERNAME` | Nextcloud login username. |
| `NEXTCLOUD_PASSWORD` | Nextcloud login password. |
@@ -949,6 +1221,7 @@ For detailed setup instructions, see the [Dropbox Setup Guide](DropboxSetup.md).
| **Variable** | **Description** |
|---------------------------------|-------------------------------------------------------|
| `GOOGLE_DRIVE_ENABLED` | Set to `false` to disable Google Drive uploads without removing credentials. Default: `true` |
| `GOOGLE_DRIVE_USE_OAUTH` | Set to `true` to use OAuth flow (recommended) |
| `GOOGLE_DRIVE_CLIENT_ID` | OAuth Client ID (required if using OAuth flow) |
| `GOOGLE_DRIVE_CLIENT_SECRET` | OAuth Client Secret (required if using OAuth flow) |
@@ -965,6 +1238,7 @@ For detailed setup instructions, see the [Google Drive Setup Guide](GoogleDriveS
| **Variable** | **Description** |
|-------------------------|---------------------------------------------------------------|
| `WEBDAV_ENABLED` | Set to `false` to disable WebDAV uploads without removing credentials. Default: `true` |
| `WEBDAV_URL` | WebDAV server URL (e.g. `https://webdav.example.com/path`). |
| `WEBDAV_USERNAME` | WebDAV authentication username. |
| `WEBDAV_PASSWORD` | WebDAV authentication password. |
@@ -975,6 +1249,7 @@ For detailed setup instructions, see the [Google Drive Setup Guide](GoogleDriveS
| **Variable** | **Description** |
|-------------------------|---------------------------------------------------------------|
| `FTP_ENABLED` | Set to `false` to disable FTP uploads without removing credentials. Default: `true` |
| `FTP_HOST` | FTP server hostname or IP address. |
| `FTP_PORT` | FTP port (default: `21`). |
| `FTP_USERNAME` | FTP authentication username. |
@@ -987,6 +1262,7 @@ For detailed setup instructions, see the [Google Drive Setup Guide](GoogleDriveS
| **Variable** | **Description** |
|------------------------------|-------------------------------------------------------|
| `SFTP_ENABLED` | Set to `false` to disable SFTP uploads without removing credentials. Default: `true` |
| `SFTP_HOST` | SFTP server hostname or IP address. |
| `SFTP_PORT` | SFTP port (default: `22`). |
| `SFTP_USERNAME` | SFTP authentication username. |
@@ -1018,6 +1294,7 @@ For detailed setup instructions, see the [Google Drive Setup Guide](GoogleDriveS
| **Variable** | **Description** |
|----------------------------------|---------------------------------------------------------------------|
| `DEST_EMAIL_ENABLED` | Set to `false` to disable email delivery without removing credentials. Default: `true` |
| `DEST_EMAIL_HOST` | SMTP server hostname for document delivery. |
| `DEST_EMAIL_PORT` | SMTP port for document delivery (default: `587`). |
| `DEST_EMAIL_USERNAME` | SMTP authentication username for document delivery. |
@@ -1030,6 +1307,7 @@ For detailed setup instructions, see the [Google Drive Setup Guide](GoogleDriveS
| **Variable** | **Description** |
|---------------------------------|-------------------------------------------------------|
| `ONEDRIVE_ENABLED` | Set to `false` to disable OneDrive uploads without removing credentials. Default: `true` |
| `ONEDRIVE_CLIENT_ID` | Azure AD application client ID |
| `ONEDRIVE_CLIENT_SECRET` | Azure AD application client secret |
| `ONEDRIVE_TENANT_ID` | Azure AD tenant ID: use "common" for personal accounts or your tenant ID for corporate accounts |
@@ -1038,10 +1316,25 @@ 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** |
|---------------------------------|-------------------------------------------------------|
| `S3_ENABLED` | Set to `false` to disable S3 uploads without removing credentials. Default: `true` |
| `AWS_ACCESS_KEY_ID` | AWS IAM access key ID |
| `AWS_SECRET_ACCESS_KEY` | AWS IAM secret access key |
| `AWS_REGION` | AWS region where your S3 bucket is located (default: `us-east-1`) |
@@ -1052,6 +1345,23 @@ For detailed setup instructions, see the [OneDrive Setup Guide](OneDriveSetup.md
For detailed setup instructions, see the [Amazon S3 Setup Guide](AmazonS3Setup.md).
### iCloud Drive (Apple)
| **Variable** | **Description** |
|---------------------------------|-------------------------------------------------------|
| `ICLOUD_ENABLED` | Set to `false` to disable iCloud uploads without removing credentials. Default: `true` |
| `ICLOUD_USERNAME` | Apple ID email address |
| `ICLOUD_PASSWORD` | App-specific password (generate at [appleid.apple.com](https://appleid.apple.com/account/manage)) |
| `ICLOUD_FOLDER` | Target folder path in iCloud Drive (e.g. `Documents/Uploads`) |
| `ICLOUD_COOKIE_DIRECTORY` | Optional directory for session cookie persistence (default: `~/.pyicloud`) |
> **Note:** Apple does not provide a public REST API for iCloud Drive. This
> integration uses the [pyicloud](https://github.com/picklepete/pyicloud)
> library which relies on an unofficial, reverse-engineered protocol. Because
> most Apple IDs have two-factor authentication enabled, you **must** generate
> an [app-specific password](https://support.apple.com/en-us/102654) and use
> it as `ICLOUD_PASSWORD`.
### Notification System
| **Variable** | **Description** |
@@ -1272,14 +1582,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)
@@ -1359,6 +1685,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
@@ -1565,6 +1892,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
@@ -1592,6 +1928,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`.
+10 -1
View File
@@ -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.
+46 -8
View File
@@ -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
View File
@@ -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.
---
+25 -1
View File
@@ -129,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).
+343
View File
@@ -0,0 +1,343 @@
# Internationalization (i18n) & Localization (l10n) Guide
DocuElevate supports **77 languages** for its web UI, with automatic browser
language detection, user-preference persistence, and an AI-powered fallback
translator for strings that haven't been manually translated yet.
## Supported Languages
| Code | Language | Native Name | Flag | Priority |
|---------|--------------------|--------------------|------|----------|
| `en` | English | English | 🇬🇧 | Tier 1 |
| `de` | German | Deutsch | 🇩🇪 | Tier 1 |
| `fr` | French | Français | 🇫🇷 | Tier 1 |
| `es` | Spanish | Español | 🇪🇸 | Tier 1 |
| `it` | Italian | Italiano | 🇮🇹 | Tier 1 |
| `pt` | Portuguese | Português | 🇵🇹 | Tier 1 |
| `nl` | Dutch | Nederlands | 🇳🇱 | Tier 2 |
| `nb` | Norwegian Bokmål | Norsk bokmål | 🇳🇴 | Tier 2 |
| `no` | Norwegian | Norsk | 🇳🇴 | Tier 2 |
| `da` | Danish | Dansk | 🇩🇰 | Tier 2 |
| `sv` | Swedish | Svenska | 🇸🇪 | Tier 2 |
| `fi` | Finnish | Suomi | 🇫🇮 | Tier 2 |
| `is` | Icelandic | Íslenska | 🇮🇸 | Tier 2 |
| `ga` | Irish | Gaeilge | 🇮🇪 | Tier 2 |
| `lb` | Luxembourgish | Lëtzebuergesch | 🇱🇺 | Tier 2 |
| `ca` | Catalan | Català | 🏴 | Tier 2 |
| `cy` | Welsh | Cymraeg | 🏴󠁧󠁢󠁷󠁬󠁳󠁿 | Tier 2 |
| `fy` | Western Frisian | Frysk | 🇳🇱 | Tier 2 |
| `gl` | Galician | Galego | 🇪🇸 | Tier 2 |
| `li` | Limburgish | Limburgs | 🇳🇱 | Tier 2 |
| `vls` | Flemish | West-Vlams | 🇧🇪 | Tier 2 |
| `nds` | Low German | Plattdüütsch | 🇩🇪 | Tier 2 |
| `pl` | Polish | Polski | 🇵🇱 | Tier 3 |
| `cs` | Czech | Čeština | 🇨🇿 | Tier 3 |
| `sk` | Slovak | Slovenčina | 🇸🇰 | Tier 3 |
| `hu` | Hungarian | Magyar | 🇭🇺 | Tier 3 |
| `sl` | Slovenian | Slovenščina | 🇸🇮 | Tier 3 |
| `hr` | Croatian | Hrvatski | 🇭🇷 | Tier 3 |
| `ro` | Romanian | Română | 🇷🇴 | Tier 3 |
| `bg` | Bulgarian | Български | 🇧🇬 | Tier 3 |
| `el` | Greek | Ελληνικά | 🇬🇷 | Tier 3 |
| `et` | Estonian | Eesti | 🇪🇪 | Tier 3 |
| `lv` | Latvian | Latviešu | 🇱🇻 | Tier 3 |
| `lt` | Lithuanian | Lietuvių | 🇱🇹 | Tier 3 |
| `sr` | Serbian | Српски | 🇷🇸 | Tier 3 |
| `tr` | Turkish | Türkçe | 🇹🇷 | Tier 4 |
| `uk` | Ukrainian | Українська | 🇺🇦 | Tier 4 |
| `he` | Hebrew | עברית | 🇮🇱 | Tier 4 |
| `ar` | Arabic | العربية | 🇸🇦 | Tier 4 |
| `fa` | Persian | فارسی | 🇮🇷 | Tier 4 |
| `af` | Afrikaans | Afrikaans | 🇿🇦 | Tier 4 |
| `zh` | Chinese | 中文 | 🇨🇳 | Tier 5 |
| `zh-TW` | Traditional Chinese | 繁體中文 | 🇹🇼 | Tier 5 |
| `ja` | Japanese | 日本語 | 🇯🇵 | Tier 5 |
| `ko` | Korean | 한국어 | 🇰🇷 | Tier 5 |
| `vi` | Vietnamese | Tiếng Việt | 🇻🇳 | Tier 5 |
| `pa` | Punjabi | ਪੰਜਾਬੀ | 🇮🇳 | Tier 5 |
| `kn` | Kannada | ಕನ್ನಡ | 🇮🇳 | Tier 5 |
| `hi` | Hindi | हिन्दी | 🇮🇳 | Tier 5 |
| `bn` | Bengali | বাংলা | 🇧🇩 | Tier 5 |
| `gu` | Gujarati | ગુજરાતી | 🇮🇳 | Tier 5 |
| `ml` | Malayalam | മലയാളം | 🇮🇳 | Tier 5 |
| `mr` | Marathi | मराठी | 🇮🇳 | Tier 5 |
| `ta` | Tamil | தமிழ் | 🇮🇳 | Tier 5 |
| `te` | Telugu | తెలుగు | 🇮🇳 | Tier 5 |
| `ur` | Urdu | اردو | 🇵🇰 | Tier 5 |
| `si` | Sinhala | සිංහල | 🇱🇰 | Tier 5 |
| `ne` | Nepali | नेपाली | 🇳🇵 | Tier 5 |
| `th` | Thai | ไทย | 🇹🇭 | Tier 5 |
| `km` | Khmer | ខ្មែរ | 🇰🇭 | Tier 5 |
| `id` | Indonesian | Bahasa Indonesia | 🇮🇩 | Tier 5 |
| `ms` | Malay | Bahasa Melayu | 🇲🇾 | Tier 5 |
| `jv` | Javanese | Basa Jawa | 🇮🇩 | Tier 5 |
| `tl` | Tagalog | Filipino | 🇵🇭 | Tier 5 |
| `mn` | Mongolian | Монгол | 🇲🇳 | Tier 5 |
| `kk` | Kazakh | Қазақ тілі | 🇰🇿 | Tier 5 |
| `uz` | Uzbek | Oʻzbekcha | 🇺🇿 | Tier 5 |
| `az` | Azerbaijani | Azərbaycan dili | 🇦🇿 | Tier 5 |
| `hy` | Armenian | Հայերեն | 🇦🇲 | Tier 5 |
| `ka` | Georgian | ქართული | 🇬🇪 | Tier 5 |
| `sw` | Swahili | Kiswahili | 🇰🇪 | Tier 6 |
| `am` | Amharic | አማርኛ | 🇪🇹 | Tier 6 |
| `ha` | Hausa | Hausa | 🇳🇬 | Tier 6 |
| `yo` | Yoruba | Yorùbá | 🇳🇬 | Tier 6 |
| `ig` | Igbo | Igbo | 🇳🇬 | Tier 6 |
| `zu` | Zulu | isiZulu | 🇿🇦 | Tier 6 |
| `eo` | Esperanto | Esperanto | 🌍 | Tier 7 |
> **Tier 1** languages (major European) have complete, manually-reviewed
> translations. **Tier 23** languages have complete translations but may
> receive less frequent updates. **Tier 4** covers Non-EU European, Middle
> Eastern, and South African (Afrikaans) languages. **Tier 5** covers Asian
> and Central Asian languages. **Tier 6** covers African languages. **Tier 7**
> covers constructed languages.
## How Language Is Detected
DocuElevate resolves the display language in the following priority order:
1. **User profile preference** — stored in the database (`UserProfile.preferred_language`)
and loaded into the session on login
2. **Cookie**`docuelevate_lang` cookie (30-day expiry, set when user selects a language)
3. **Browser `Accept-Language` header** — the highest-priority match among supported languages
4. **Default** — English (`en`)
## Selecting Your Language
### Via the Navigation Bar
Click the 🌐 **globe icon** in the top navigation bar. A dropdown menu shows all
available languages with their native names and flag emoji. The current language
is highlighted with a blue checkmark.
### Via the API
```bash
# Set language to German
curl -X POST http://localhost:8000/api/i18n/language \
-H "Content-Type: application/json" \
-d '{"language": "de"}'
# List all available languages
curl http://localhost:8000/api/i18n/languages
```
### Via Cookie (Programmatic)
Set the `docuelevate_lang` cookie to any supported language code:
```javascript
document.cookie = "docuelevate_lang=fr; max-age=2592000; path=/";
location.reload();
```
## For Developers
### Translation File Structure
Translations are stored as flat JSON files in `frontend/translations/`:
```
frontend/translations/
├── en.json # English (base / reference)
├── af.json # Afrikaans
├── ar.json # Arabic
├── bg.json # Bulgarian
├── ca.json # Catalan
├── cs.json # Czech
├── cy.json # Welsh
├── da.json # Danish
├── de.json # German
├── el.json # Greek
├── eo.json # Esperanto
├── es.json # Spanish
├── et.json # Estonian
├── fa.json # Persian
├── fi.json # Finnish
├── fr.json # French
├── fy.json # Frisian
├── ga.json # Irish
├── gl.json # Galician
├── he.json # Hebrew
├── hr.json # Croatian
├── hu.json # Hungarian
├── is.json # Icelandic
├── it.json # Italian
├── ja.json # Japanese
├── kn.json # Kannada
├── ko.json # Korean
├── lb.json # Luxembourgish
├── li.json # Limburgish
├── lt.json # Lithuanian
├── lv.json # Latvian
├── nb.json # Norwegian Bokmål
├── nds.json # Low German (Plattdeutsch)
├── nl.json # Dutch
├── no.json # Norwegian
├── pa.json # Punjabi
├── pl.json # Polish
├── pt.json # Portuguese
├── ro.json # Romanian
├── ru.json # Russian
├── sk.json # Slovak
├── sl.json # Slovenian
├── sr.json # Serbian
├── sv.json # Swedish
├── tr.json # Turkish
├── uk.json # Ukrainian
├── vi.json # Vietnamese
├── vls.json # West Flemish
└── zh.json # Chinese
```
Each file is a flat key-value dictionary with dot-notation namespacing:
```json
{
"nav.dashboard": "Dashboard",
"nav.upload": "Upload",
"upload.max_size": "Maximum file size: {size}",
"footer.copyright": "DocuElevate {year}"
}
```
### Using Translations in Templates
The `_()` function is available globally in all Jinja2 templates:
```jinja2
{# Simple translation #}
<h1>{{ _("dashboard.title") }}</h1>
{# Translation with placeholders #}
<p>{{ _("upload.max_size", size="50 MB") }}</p>
{# Translation in attributes #}
<button aria-label="{{ _('common.save') }}">{{ _("common.save") }}</button>
```
### Using Translations in Python
```python
from app.utils.i18n import translate
# Basic translation
text = translate("nav.dashboard", "de") # → "Übersicht"
# With placeholders
text = translate("footer.copyright", "fr", year="2025") # → "DocuElevate 2025"
```
### Localization Helpers
Format dates, times, and numbers according to locale conventions:
```jinja2
{# In templates — locale is automatically detected #}
<span>{{ format_date_l10n(document.created_at) }}</span>
<span>{{ format_number_l10n(file_count) }}</span>
```
```python
# In Python
from app.utils.i18n import format_date, format_number
format_date(date(2025, 3, 15), "de") # → "15. March 2025"
format_date(date(2025, 3, 15), "de", short=True) # → "15.03.2025"
format_number(1234567, "de") # → "1.234.567"
format_number(1234.56, "en") # → "1,234.56"
```
### Adding a New Translation Key
1. Add the key and English text to `frontend/translations/en.json`
2. Use `{{ _("your.new.key") }}` in templates or `translate("your.new.key", locale)` in Python
That's it. An external automation script picks up new keys in `en.json` and propagates
translations to all other language files. You never need to touch the non-English JSON
files manually — the translate-and-sync pipeline takes care of it.
### AI Fallback Translation
When a translation key exists in English but not in the target language,
DocuElevate can use the configured AI provider (OpenAI, Anthropic, etc.)
to translate the string on-the-fly:
```python
from app.utils.i18n import translate_with_ai_fallback
# Falls back to AI if no manual translation exists
translated = translate_with_ai_fallback("Welcome to our platform", "de")
```
The AI fallback:
- Uses the `AI_MODEL` or `OPENAI_MODEL` setting
- Caches results in memory for the process lifetime
- Returns the original English text if the AI call fails
- Is designed for graceful degradation — the UI never breaks
### Adding a New Language
1. Create a new JSON file in `frontend/translations/` (e.g., `ja.json`)
2. Copy the structure from `en.json` and translate all values
3. Add the language to `SUPPORTED_LANGUAGES` in `app/utils/i18n.py`:
```python
{"code": "ja", "name": "Japanese", "native": "日本語", "flag": "🇯🇵"},
```
4. Add locale formatting rules to `_LOCALE_FORMATS` in the same file
5. Create a database migration if needed (the `preferred_language` column
already accepts any string up to 10 characters)
### Database Migration
Migration `027_add_user_language_preference` adds a `preferred_language`
column to the `user_profiles` table. This column stores the user's chosen
UI language as an ISO 639-1 code (e.g., `"de"`, `"fr"`). A `NULL` value
means "auto-detect from browser settings."
### API Reference
#### `GET /api/i18n/languages`
Returns all supported languages and the current active language.
**Response:**
```json
{
"languages": [
{"code": "en", "name": "English", "native": "English", "flag": "🇬🇧"},
{"code": "de", "name": "German", "native": "Deutsch", "flag": "🇩🇪"}
],
"current": "en",
"default": "en"
}
```
#### `POST /api/i18n/language`
Set the preferred UI language. Persists in session, cookie, and database.
**Request:**
```json
{"language": "de"}
```
**Response:**
```json
{
"language": "de",
"message": "Language changed to Deutsch"
}
```
## Configuration
No additional configuration is required. The i18n system works out of the box
with the default English language and automatically detects browser preferences.
| Setting | Default | Description |
|---------|---------|-------------|
| Browser `Accept-Language` | Auto-detected | Used when no explicit preference is set |
| `docuelevate_lang` cookie | Not set | Set when user selects a language via the UI |
| `UserProfile.preferred_language` | `NULL` | Stored in DB for authenticated users |
+13 -5
View File
@@ -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):
+360
View File
@@ -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)
+529
View File
@@ -0,0 +1,529 @@
# Mobile App
DocuElevate includes a native mobile application for iOS and Android built with **React Native** and **Expo**. The app allows users to capture documents with the device camera, pick files from the device storage, and receive push notifications when documents finish processing.
## Features
| Feature | iOS | Android |
|---------|-----|---------|
| 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 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)
### Prerequisites
- Node.js 20.19.4 or later (use [nvm](https://github.com/nvm-sh/nvm): `nvm install` inside `mobile/` reads `.nvmrc` automatically)
- [Expo CLI](https://docs.expo.dev/get-started/installation/): `npm install -g @expo/cli`
- [Expo Go](https://expo.dev/client) app on your iOS or Android device (for development)
- A running DocuElevate server reachable from your device
### Run in development mode
```bash
cd mobile
npm install
npx expo start
```
Scan the QR code with **Expo Go** on your device. On iOS you can also use the Camera app.
## Building for Production
DocuElevate uses **Expo Application Services (EAS)** to produce App Store / Play Store binaries.
```bash
# Install EAS CLI globally
npm install -g eas-cli
# Authenticate with Expo
eas login
# Build for iOS (requires Apple Developer account)
eas build --platform ios
# Build for Android
eas build --platform android
```
> **Note:** The mobile app uses `expo-build-properties` with `buildReactNativeFromSource: true` for iOS builds. This is required for Expo SDK 54 (React Native 0.81) compatibility — some native modules still use legacy bridge APIs (`RCTBridge`, `RCTViewManager`, etc.) that are no longer included in the default precompiled XCFrameworks. Building React Native from source makes these headers available, at the cost of slightly longer iOS build times.
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
The mobile app uses the server's existing OAuth2/SSO setup:
1. User enters the DocuElevate server URL on the login screen.
2. The app opens `<server>/login?mobile=1&redirect_uri=docuelevate://callback` in the **system browser** (Safari / Chrome Custom Tabs).
3. The server stores `docuelevate://callback` in the browser session and presents the login page.
4. The user authenticates via SSO or local credentials.
5. After successful authentication the server mints a long-lived API token and redirects the browser to `docuelevate://callback?token=<token>`.
6. `WebBrowser.openAuthSessionAsync` intercepts the `docuelevate://` deep link and returns the URL to the app.
7. The app extracts the token from the URL and stores it securely in the device's keychain (`expo-secure-store`).
> **Security note:** The `redirect_uri` is validated server-side; only URIs with the `docuelevate://` custom scheme (production) or the `exp://` scheme (Expo Go development) are accepted, preventing open-redirect attacks.
### Testing in Expo Go
When developing with **Expo Go** the app does not have the `docuelevate://` custom URL scheme registered. The auth flow adapts automatically:
1. `Linking.createURL('callback')` returns an `exp://` URI pointing at the local dev server (e.g. `exp://192.168.1.5:8081/--/callback`).
2. This URI is sent to the server as `redirect_uri`; the server accepts it alongside the production `docuelevate://` scheme.
3. After successful authentication the server redirects back to the `exp://` URI.
4. `WebBrowser.openAuthSessionAsync` intercepts the deep link and the Expo Go app receives the token.
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:
- Works identically to tokens created manually in the web UI.
- Is shown in the **API Tokens** page (`/api-tokens`) and can be revoked there.
- Is stored in the device's secure keychain, never in plain storage.
## Push Notifications
Push notifications are delivered via the **Expo Push Notification** service, which routes through Apple Push Notification service (APNs) for iOS and Firebase Cloud Messaging (FCM) for Android.
**No server-side APNs/FCM credentials are required** Expo's servers handle the provider integration.
### How it works
1. After login, the app requests notification permission from the operating system.
2. If granted, the app obtains an **Expo Push Token** (`ExponentPushToken[…]`).
3. The token is registered with the backend via `POST /api/mobile/register-device`.
4. When a document finishes processing, the server sends a push notification to all registered devices for that user.
### Managing registered devices
Users can see and remove their registered devices from the **Profile** tab in the app, or via the API:
```bash
# List registered devices
curl -H "Authorization: Bearer <token>" https://your-server/api/mobile/devices
# Remove a device
curl -X DELETE -H "Authorization: Bearer <token>" https://your-server/api/mobile/devices/<id>
```
## Uploading Documents
### Camera Capture
1. Open the **Upload** tab.
2. Tap **Camera**.
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 **Files**.
3. Browse to and select one or more files (PDF, DOCX, images, etc.).
4. Files are uploaded and queued for processing.
### Share Sheet (iOS) / Share Intent (Android)
The app registers itself as a share target so any file can be sent directly to DocuElevate from another app:
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 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:
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| `POST` | `/api/mobile/generate-token` | Session | Exchange SSO session for API token |
| `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 (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.
### POST /api/mobile/generate-token
Exchanges an active web session (cookie) for a permanent API token suitable for use in the mobile app.
**Request:**
```json
{ "device_name": "John's iPhone" }
```
**Response (201):**
```json
{
"token": "de_AbCdEfGhIjKl...",
"token_id": 42,
"name": "Mobile App John's iPhone",
"created_at": "2026-03-10T09:30:00Z"
}
```
> ⚠️ The `token` value is returned **once only**. Store it in the device's secure keychain immediately.
### POST /api/mobile/register-device
Registers an Expo push token for the authenticated user.
**Request:**
```json
{
"push_token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
"device_name": "John's iPhone",
"platform": "ios"
}
```
Supported platforms: `ios`, `android`, `web`.
Re-registering the same token is safe (idempotent).
### GET /api/mobile/whoami
Returns the current user's profile, including the server-stored language preference.
**Response (200):**
```json
{
"owner_id": "john@example.com",
"display_name": "John Doe",
"email": "john@example.com",
"avatar_url": "https://www.gravatar.com/avatar/...",
"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.
If you wish to use **direct FCM/APNs** without Expo's relay, replace the `send_expo_push_notification` function in `app/utils/push_notification.py` with your own implementation.
## Project Structure (mobile/)
```
mobile/
├── 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
│ └── ShareContext.tsx # Shared-file queue (iOS Share Sheet / Android Intent)
├── hooks/
│ └── usePushNotifications.ts # Push token registration
├── screens/
│ ├── 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.
**To fix:**
1. **Refresh the session** by running `eas credentials` and re-authenticating with your Apple ID.
2. **Recommended for automation:** Replace the Apple ID session with an [App Store Connect API Key](https://docs.expo.dev/app-signing/app-credentials/#app-store-connect-api-key). API keys do not expire and work fully non-interactively:
- Create a key at [appstoreconnect.apple.com → Users → Integrations → Keys](https://appstoreconnect.apple.com/access/integrations/api)
- Download the `.p8` file and note the **Key ID** and **Issuer ID**
- Run `eas credentials` → iOS → *Add an App Store Connect API key*
- Upload the `.p8` file when prompted
Once an API key is configured in EAS, automated builds (including CI and EAS Cloud Workflows) will no longer prompt for a password.
### Node.js deprecation warning `[DEP0169]` during EAS build
```
(node:XXXXX) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized…
```
This warning is emitted by **EAS CLI** (an external tool) when it runs on **Node.js 22 or later**, which deprecates `url.parse()`. It does not indicate a problem in the DocuElevate mobile app itself and will not cause a build failure on its own.
The `eas.json` build profiles already include `"NODE_NO_WARNINGS": "1"` in their `env` sections to suppress this warning during EAS Cloud builds. For local builds with a system Node.js ≥ 22, suppress it by running:
```bash
NODE_NO_WARNINGS=1 eas build --platform ios
```
or by activating the project's pinned Node.js version first:
```bash
cd mobile
nvm use # reads .nvmrc → Node 20.19.4 (no deprecation warning)
eas build --platform ios
```
### "Authentication was cancelled or failed"
- Ensure the server URL is correct (including `https://`).
- Verify the server is reachable from your device's network.
- Confirm that `AUTH_ENABLED=True` on the server.
### Push notifications not arriving
1. Check that the app has notification permission (Settings → DocuElevate → Notifications).
2. Verify the device is registered: `GET /api/mobile/devices`.
3. Ensure the server can reach `https://exp.host` (outbound HTTPS on port 443).
4. On Android, add `google-services.json` to the `mobile/` directory if you are building your own binary.
### "Connection refused" or timeout
- Verify that the DocuElevate server is running and accessible.
- Ensure the server's `EXTERNAL_HOSTNAME` or reverse proxy is configured correctly.
- Check that the server accepts CORS requests from `docuelevate://`.
## Related Documentation
- [API Documentation](./API.md)
- [Configuration Guide](./ConfigurationGuide.md)
- [Deployment Guide](./DeploymentGuide.md)
- [Apple App Store Compliance Audit](./AppleAppStoreCompliance.md)
+38 -15
View File
@@ -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
@@ -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.
+1
View File
@@ -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
+1 -1
View File
@@ -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
+185
View File
@@ -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
+375
View File
@@ -0,0 +1,375 @@
# 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.
## Overview
DocuElevate supports four social login providers:
| Provider | Protocol | Best For |
|----------|----------|----------|
| **Google** | OAuth2 / OpenID Connect | Consumers and Google Workspace organizations |
| **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 |
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).
## Prerequisites
Before configuring any social login provider, ensure:
1. **Authentication is enabled**: `AUTH_ENABLED=true` in your `.env` file
2. **Session secret is set**: `SESSION_SECRET` must be a random string of at least 32 characters
3. **HTTPS is configured**: All social login providers require HTTPS redirect URIs in production. Use a reverse proxy (Traefik, Nginx, Caddy) with a valid TLS certificate
4. **External hostname is set**: `EXTERNAL_HOSTNAME` must match your public domain (e.g., `docuelevate.example.com`)
> **Note:** Social login users are regular (non-admin) users by default. To grant admin access, use the Admin Panel (**Settings → User Management**) after the user's first login, or configure admin groups via Authentik/OIDC.
## Callback URLs
Each social login provider uses a callback URL to redirect users back to DocuElevate after authentication. The callback URL pattern is:
```
https://<EXTERNAL_HOSTNAME>/social-callback/<provider>
```
For example, if your DocuElevate instance is at `https://docuelevate.example.com`:
| Provider | Callback URL |
|----------|-------------|
| Google | `https://docuelevate.example.com/social-callback/google` |
| Microsoft | `https://docuelevate.example.com/social-callback/microsoft` |
| Apple | `https://docuelevate.example.com/social-callback/apple` |
| Dropbox | `https://docuelevate.example.com/social-callback/dropbox` |
---
## Google Sign-In
### 1. Create OAuth Credentials in Google Cloud Console
1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project (or select an existing one)
3. Navigate to **APIs & Services → Credentials**
4. Click **Create Credentials → OAuth client ID**
5. If prompted, configure the **OAuth consent screen** first:
- **User Type**: External (or Internal for Google Workspace)
- **App name**: DocuElevate
- **User support email**: Your email
- **Authorized domains**: Your domain (e.g., `example.com`)
- **Scopes**: Add `email`, `profile`, and `openid`
6. Back on the Credentials page, create an **OAuth 2.0 Client ID**:
- **Application type**: Web application
- **Name**: DocuElevate
- **Authorized redirect URIs**: `https://docuelevate.example.com/social-callback/google`
7. Note the **Client ID** and **Client Secret**
### 2. Configure DocuElevate
Add to your `.env` file:
```bash
SOCIAL_AUTH_GOOGLE_ENABLED=true
SOCIAL_AUTH_GOOGLE_CLIENT_ID=123456789-abcdefg.apps.googleusercontent.com
SOCIAL_AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-your-secret-here
```
### 3. Restart DocuElevate
```bash
docker compose restart api worker
```
### Google-Specific Notes
- **Google Workspace**: If you want to restrict sign-in to users in your Google Workspace organization, set the OAuth consent screen to "Internal"
- **Verification**: Google may require app verification if you're using External user type and requesting sensitive scopes. For small teams (<100 users), you can add test users instead
- **Unified Auth**: If you also use Google Drive as a storage destination, users who sign in with Google will already be authenticated with a Google identity — simplifying the Google Drive integration experience
---
## Microsoft Sign-In (Azure AD / Microsoft Entra ID)
### 1. Register an Application in Azure
1. Go to the [Azure Portal](https://portal.azure.com/)
2. Navigate to **Microsoft Entra ID → App registrations**
3. Click **New registration**
4. Fill in:
- **Name**: DocuElevate
- **Supported account types**: Choose based on your needs:
- *Accounts in this organizational directory only* — single-tenant (your org only)
- *Accounts in any organizational directory* — multi-tenant
- *Accounts in any organizational directory and personal Microsoft accounts* — broadest reach
- **Redirect URI**: Select **Web** and enter `https://docuelevate.example.com/social-callback/microsoft`
5. Click **Register**
6. Note the **Application (client) ID**
7. Navigate to **Certificates & secrets → New client secret**
8. Add a description and expiration, then click **Add**
9. Note the **Value** (this is your client secret — it's only shown once!)
### 2. Configure API Permissions
1. In your app registration, go to **API permissions**
2. Ensure these permissions are present (they're usually added by default):
- `openid`
- `profile`
- `email`
3. Click **Grant admin consent** if you're a tenant admin
### 3. Configure DocuElevate
Add to your `.env` file:
```bash
SOCIAL_AUTH_MICROSOFT_ENABLED=true
SOCIAL_AUTH_MICROSOFT_CLIENT_ID=12345678-abcd-efgh-ijkl-123456789012
SOCIAL_AUTH_MICROSOFT_CLIENT_SECRET=your~client~secret~value
SOCIAL_AUTH_MICROSOFT_TENANT=common
```
**Tenant options:**
| Value | Who Can Sign In |
|-------|----------------|
| `common` | Any Microsoft account (personal + any Azure AD organization) |
| `organizations` | Any Azure AD organization (work/school accounts only) |
| `consumers` | Personal Microsoft accounts only (outlook.com, hotmail.com, etc.) |
| `<tenant-id>` | Only users in a specific Azure AD tenant (use the GUID from Azure Portal) |
### 4. Restart DocuElevate
```bash
docker compose restart api worker
```
### Microsoft-Specific Notes
- **Client secret expiration**: Azure AD client secrets expire (max 2 years). Set a calendar reminder to rotate them before they expire
- **Conditional Access**: If your organization uses Azure AD Conditional Access policies, social login will respect them
- **Unified Auth**: If you also use OneDrive as a storage destination, users who sign in with Microsoft will already have a Microsoft identity — potentially simplifying OneDrive integration
---
## Apple Sign-In
Apple Sign-In requires an Apple Developer account ($99/year) and more setup than other providers.
### 1. Configure in Apple Developer Portal
1. Go to the [Apple Developer Portal](https://developer.apple.com/account/)
2. Navigate to **Certificates, Identifiers & Profiles → Identifiers**
3. Click **+** and select **App IDs** → Register an App ID:
- **Description**: DocuElevate
- **Bundle ID**: e.g., `com.example.docuelevate`
- Enable **Sign In with Apple** capability
4. Click **+** again and select **Services IDs**:
- **Description**: DocuElevate Web
- **Identifier**: e.g., `com.example.docuelevate.web` (this is your Client ID)
- Enable **Sign In with Apple**
- Click **Configure** next to Sign In with Apple:
- **Primary App ID**: Select the App ID created above
- **Domains**: `docuelevate.example.com`
- **Return URLs**: `https://docuelevate.example.com/social-callback/apple`
5. Click **Save** and **Continue****Register**
6. Navigate to **Keys** → Click **+** to create a new key:
- **Key Name**: DocuElevate Sign-In
- Enable **Sign In with Apple**
- Click **Configure** and select the App ID created above
- Click **Continue****Register**
- **Download the private key file** (`.p8`) — you can only download it once!
- Note the **Key ID**
7. Note your **Team ID** (shown in the top-right corner of the Developer Portal)
### 2. Configure DocuElevate
Add to your `.env` file:
```bash
SOCIAL_AUTH_APPLE_ENABLED=true
SOCIAL_AUTH_APPLE_CLIENT_ID=com.example.docuelevate.web
SOCIAL_AUTH_APPLE_TEAM_ID=ABCDE12345
SOCIAL_AUTH_APPLE_KEY_ID=FGHIJ67890
SOCIAL_AUTH_APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg...
...your key content here...
-----END PRIVATE KEY-----"
```
> **Tip:** You can also store the private key as a single line with `\n` for line breaks:
> ```bash
> SOCIAL_AUTH_APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMG...\n-----END PRIVATE KEY-----"
> ```
### 3. Restart DocuElevate
```bash
docker compose restart api worker
```
### Apple-Specific Notes
- **Email relay**: Apple offers a "Hide My Email" feature that provides a relay email address (e.g., `abc123@privaterelay.appleid.com`). DocuElevate accepts these addresses
- **First login only**: Apple sends the user's name only on the very first authorization. If the user revokes and re-authorizes, their name may not be sent again
- **Developer account required**: You need an Apple Developer account ($99/year) to use Sign In with Apple
- **Key rotation**: Apple private keys don't expire, but if you suspect compromise, revoke the key in the Developer Portal and create a new one
---
## Dropbox Sign-In
### 1. Create a Dropbox App
1. Go to the [Dropbox App Console](https://www.dropbox.com/developers/apps)
2. Click **Create app**
3. Choose:
- **API**: Scoped access
- **Access type**: Full Dropbox (or App folder, depending on your needs)
- **Name**: DocuElevate Auth (or reuse your existing Dropbox storage app)
4. In the app settings, go to the **OAuth 2** section:
- Add **Redirect URI**: `https://docuelevate.example.com/social-callback/dropbox`
5. Note the **App key** (this is your Client ID) and **App secret** (this is your Client Secret)
> **Tip:** If you already have a Dropbox app configured for DocuElevate's storage integration, you can reuse the same app — just add the social login redirect URI. Alternatively, create a separate app for authentication to keep concerns separated.
### 2. Configure DocuElevate
Add to your `.env` file:
```bash
SOCIAL_AUTH_DROPBOX_ENABLED=true
SOCIAL_AUTH_DROPBOX_CLIENT_ID=your_dropbox_app_key
SOCIAL_AUTH_DROPBOX_CLIENT_SECRET=your_dropbox_app_secret
```
### 3. Restart DocuElevate
```bash
docker compose restart api worker
```
### Dropbox-Specific Notes
- **Unified Auth**: If you also use Dropbox as a storage destination, authenticating via Dropbox establishes the user's Dropbox identity — making it easier to manage Dropbox storage integration
- **App review**: Dropbox may require app review for production apps with more than 50 users. See [Dropbox App Review](https://www.dropbox.com/developers/reference/developer-guide#app-review)
- **Personal vs. Business**: The same app works for both personal Dropbox and Dropbox Business accounts
---
## 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:
| Social Login Provider | Related Storage Destination | Benefit |
|---|---|---|
| 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 |
| 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.
## Combining Multiple Auth Methods
DocuElevate supports running multiple authentication methods simultaneously:
```
┌──────────────────────────────────────────────────┐
│ Login Page │
├──────────────────────────────────────────────────┤
│ Username / Password form (always shown) │
│ │
│ ─── Or continue with ─── │
│ │
│ [Authentik SSO] (if OIDC configured) │
│ [Sign in with Google] (if Google enabled) │
│ [Sign in with Microsoft] (if Microsoft enabled) │
│ [Sign in with Apple] (if Apple enabled) │
│ [Sign in with Dropbox] (if Dropbox enabled) │
│ │
│ [Create account] (if local signup enabled) │
└──────────────────────────────────────────────────┘
```
All methods create or update the same `UserProfile` record, so a user is consistently identified regardless of how they sign in.
## Admin Management
Social login users appear in the **Admin → User Management** panel like any other user. Admins can:
- View which provider a user authenticated with
- Block or unblock social login users
- Set upload limits and subscription tiers
- Grant admin privileges (social login users are never automatically admin)
## Security Considerations
1. **HTTPS is required**: All social login providers require HTTPS callback URLs in production
2. **Credentials are sensitive**: Store client secrets securely — use environment variables, never commit them to source control
3. **Least privilege**: Only request the scopes you need (DocuElevate requests `openid`, `profile`, and `email`)
4. **Rotate secrets**: Set calendar reminders to rotate OAuth client secrets before they expire (especially Microsoft, which has a max 2-year expiration)
5. **Monitor logins**: Check the DocuElevate audit log for unusual login patterns
6. **Social login users are not admins**: Admin access must be explicitly granted by an existing admin
## Troubleshooting
### Common Issues
1. **"Unknown social provider" error**
- The provider is not enabled or credentials are missing
- Check that `SOCIAL_AUTH_<PROVIDER>_ENABLED=true` is set
- Verify client ID and secret are configured
2. **"Could not retrieve email from provider" error**
- The provider didn't return an email address
- For Google: Ensure `email` scope is included (it is by default)
- For Apple: User may have chosen "Hide My Email" — this is expected and should still work
- For Dropbox: Ensure the app has permission to read the user's email
3. **Redirect URI mismatch**
- The callback URL registered with the provider must exactly match what DocuElevate generates
- Check your `EXTERNAL_HOSTNAME` setting
- Ensure you're using HTTPS in production
- The callback URL format is: `https://<EXTERNAL_HOSTNAME>/social-callback/<provider>`
4. **"Social login failed" error**
- Check DocuElevate logs (`docker compose logs api`) for detailed error messages
- Verify the provider's OAuth app is not suspended or in development mode
- For Google: Check if the OAuth consent screen needs verification
- For Microsoft: Ensure admin consent was granted for the required permissions
5. **User can't log in after changing provider settings**
- After changing social login configuration, restart DocuElevate: `docker compose restart api worker`
- Social login settings require a restart to take effect (`restart_required: true`)
### Debug Checklist
- [ ] `AUTH_ENABLED=true` is set
- [ ] `SESSION_SECRET` is at least 32 characters
- [ ] `EXTERNAL_HOSTNAME` matches your public domain
- [ ] Provider-specific `_ENABLED=true` is set
- [ ] Client ID and secret are correctly configured (no extra spaces)
- [ ] Callback URL is registered with the provider
- [ ] HTTPS is working on your domain
- [ ] DocuElevate has been restarted after configuration changes
## Environment Variable Reference
| Variable | Required | Description |
|---|---|---|
| `SOCIAL_AUTH_GOOGLE_ENABLED` | No | Enable Google Sign-In (`true`/`false`). Default: `false` |
| `SOCIAL_AUTH_GOOGLE_CLIENT_ID` | When Google enabled | Google OAuth2 client ID |
| `SOCIAL_AUTH_GOOGLE_CLIENT_SECRET` | When Google enabled | Google OAuth2 client secret |
| `SOCIAL_AUTH_MICROSOFT_ENABLED` | No | Enable Microsoft Sign-In (`true`/`false`). Default: `false` |
| `SOCIAL_AUTH_MICROSOFT_CLIENT_ID` | When Microsoft enabled | Azure AD application (client) ID |
| `SOCIAL_AUTH_MICROSOFT_CLIENT_SECRET` | When Microsoft enabled | Azure AD client secret |
| `SOCIAL_AUTH_MICROSOFT_TENANT` | No | Azure AD tenant. Default: `common` |
| `SOCIAL_AUTH_APPLE_ENABLED` | No | Enable Apple Sign-In (`true`/`false`). Default: `false` |
| `SOCIAL_AUTH_APPLE_CLIENT_ID` | When Apple enabled | Apple Services ID |
| `SOCIAL_AUTH_APPLE_TEAM_ID` | When Apple enabled | Apple Developer Team ID |
| `SOCIAL_AUTH_APPLE_KEY_ID` | When Apple enabled | Apple Sign-In key ID |
| `SOCIAL_AUTH_APPLE_PRIVATE_KEY` | When Apple enabled | Apple Sign-In private key (PEM) |
| `SOCIAL_AUTH_DROPBOX_ENABLED` | No | Enable Dropbox Sign-In (`true`/`false`). Default: `false` |
| `SOCIAL_AUTH_DROPBOX_CLIENT_ID` | When Dropbox enabled | Dropbox App Key |
| `SOCIAL_AUTH_DROPBOX_CLIENT_SECRET` | When Dropbox enabled | Dropbox App Secret |
+2
View File
@@ -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) |
@@ -348,6 +349,7 @@ in task messages or logs.
| `PAPERLESS` | Paperless-ngx REST API, API token |
| `EMAIL` | SMTP/STARTTLS, file as attachment |
| `RCLONE` | `rclone copyto` subprocess, per-user rclone config |
| `ICLOUD` | pyicloud library, Apple ID + app-specific password |
### Multiple Destinations
+197 -39
View File
@@ -2,6 +2,8 @@
This document provides solutions to common problems encountered when using DocuElevate.
> **Tip:** For configuration-specific issues, see also the [Configuration Troubleshooting](ConfigurationTroubleshooting.md) guide.
## Common Issues
### Application Won't Start
@@ -14,7 +16,7 @@ This document provides solutions to common problems encountered when using DocuE
#### Possible Solutions
1. **Check environment variables**
```bash
docker-compose config
docker compose config
```
Ensure all required variables are set properly in your `.env` file.
@@ -30,6 +32,18 @@ This document provides solutions to common problems encountered when using DocuE
```
Ensure the port isn't already in use by another application.
4. **Check Redis connectivity**
```bash
docker compose logs redis
```
Ensure Redis is running — both the API server and Celery worker depend on it.
5. **Check database migrations**
```bash
docker compose exec api alembic upgrade head
```
Ensure the database schema is up-to-date.
### Document Upload Fails
#### Symptoms
@@ -39,41 +53,51 @@ This document provides solutions to common problems encountered when using DocuE
#### Possible Solutions
1. **Check file size limits**
- Default maximum file size is 100MB
- Adjust `client_max_body_size` in your reverse proxy configuration
- Default maximum file size is 1 GB (`MAX_UPLOAD_SIZE`)
- Individual file limit: `MAX_SINGLE_FILE_SIZE` (default: same as `MAX_UPLOAD_SIZE`)
- If using a reverse proxy, adjust `client_max_body_size` (Nginx) or equivalent
2. **Verify storage space**
```bash
df -h
```
Ensure there's sufficient disk space available.
Ensure there's sufficient disk space on the workdir volume.
3. **Check worker process**
```bash
docker-compose logs worker
docker compose logs worker
```
Verify the Celery worker is running and processing tasks.
4. **Check upload quota**
If multi-user mode and subscriptions are enabled, verify the user hasn't exceeded their daily upload limit (`DEFAULT_DAILY_UPLOAD_LIMIT`).
### OCR or Text Extraction Issues
#### Symptoms
- Documents upload but text isn't extracted
- Poor quality text extraction
- API errors related to Azure services
- API errors related to OCR services
#### Possible Solutions
1. **Verify API credentials**
Check the Azure Document Intelligence API key and endpoint in your `.env` file.
1. **Verify the configured OCR provider**
Check which provider is set via the `OCR_PROVIDER` environment variable (defaults to Azure Document Intelligence).
2. **Check document quality**
2. **Verify API credentials**
Check the credentials for your configured OCR provider in your `.env` file:
- **Azure**: `AZURE_DI_KEY` and `AZURE_DI_ENDPOINT`
- **Tesseract**: No credentials required (local), but ensure `TESSERACT_LANGUAGES` is set
- **EasyOCR**: No credentials required (local)
- **Mistral**: `MISTRAL_OCR_API_KEY`
- **Google Document AI**: `GOOGLE_DOCAI_PROJECT_ID`, `GOOGLE_DOCAI_LOCATION`, `GOOGLE_DOCAI_PROCESSOR_ID`
- **AWS Textract**: `AWS_TEXTRACT_ACCESS_KEY_ID`, `AWS_TEXTRACT_SECRET_ACCESS_KEY`, `AWS_TEXTRACT_REGION`
3. **Check document quality**
- Ensure documents are clearly scanned
- Try preprocessing images to improve quality before upload
3. **Test API connectivity**
```bash
curl -X GET -H "Ocp-Apim-Subscription-Key: YOUR_KEY" "YOUR_ENDPOINT"
```
Ensure the API is accessible from your server.
4. **Try multi-provider OCR**
Configure `OCR_PROVIDERS` (comma-separated list) with a merge strategy (`OCR_MERGE_STRATEGY`: `ai_merge`, `longest`, or `primary`) for better results.
### Email Integration Problems
@@ -84,92 +108,226 @@ This document provides solutions to common problems encountered when using DocuE
#### Possible Solutions
1. **Verify IMAP settings**
Check host, port, username, and password in your configuration.
Check host, port, username, and password for `IMAP1_*` / `IMAP2_*` in your configuration.
2. **Test IMAP connectivity**
```bash
telnet mail.example.com 993
docker compose exec api python -c "import imaplib; m = imaplib.IMAP4_SSL('mail.example.com', 993); print('OK')"
```
Ensure the IMAP server is accessible.
Ensure the IMAP server is accessible from the container.
3. **Enable less secure apps**
For Gmail and some providers, you may need to enable access for less secure apps or use app-specific passwords.
3. **Check for app-specific passwords**
For Gmail and some providers, you must use app-specific passwords instead of your account password.
4. **Check firewall settings**
Ensure your server can make outbound connections to the mail server.
Ensure your server can make outbound connections to the mail server on port 993 (IMAP SSL).
5. **Check attachment filter**
If only certain attachments are expected, verify `IMAP_ATTACHMENT_FILTER` is set correctly (`documents_only` or `all`).
### Storage Integration Issues
#### Symptoms
- Files aren't appearing in Dropbox/Nextcloud/Paperless
- Files aren't appearing in configured storage destinations
- Authentication errors in logs
- API rate limiting errors
#### Possible Solutions
1. **Verify API credentials**
Double-check all API keys, tokens, and secrets.
Double-check all API keys, tokens, and secrets for the relevant service.
2. **Check access permissions**
Ensure the application has write permissions to the specified folders.
Ensure the application has write permissions to the specified folders/buckets.
3. **Refresh tokens**
For OAuth-based services like Dropbox, try generating new refresh tokens.
For OAuth-based services like Dropbox, Google Drive, and OneDrive, try re-authorizing through the integration setup pages.
4. **Examine detailed logs**
```bash
docker-compose logs worker | grep -i dropbox
docker compose logs worker | grep -i "upload_to"
```
Look for specific error messages related to the service.
Look for specific error messages related to the storage service.
5. **Check integration status**
Visit the **Integrations** page in the web UI to verify the connection status of each configured storage backend.
## Search Issues
### Symptoms
- Search returns no results or incomplete results
- Search page shows an error
### Possible Solutions
1. **Check Meilisearch is running**
```bash
docker compose logs meilisearch
```
Ensure the Meilisearch container is healthy and accepting connections.
2. **Verify Meilisearch URL**
Check `MEILISEARCH_URL` in your `.env` file (default: `http://meilisearch:7700`).
3. **Rebuild the search index**
If documents are missing from search results, reprocessing them will re-index their content.
## Pipeline & Routing Issues
### Symptoms
- Documents are not processed according to pipeline steps
- Routing rules don't match expected documents
### Possible Solutions
1. **Verify pipeline assignment**
On the file detail page, check which pipeline (if any) is assigned. The system pipeline applies to all documents by default.
2. **Test routing rules**
Use the **Evaluate** button on the Routing Rules page to test whether a rule matches a specific document.
3. **Check step ordering**
Pipeline steps execute in order — ensure OCR comes before metadata extraction if the AI step depends on extracted text.
## Database Issues
#### Symptoms
### Symptoms
- Application errors related to database connections
- Missing or corrupt data
- Slow performance
#### Possible Solutions
### Possible Solutions
1. **Check database connection string**
Verify the `DATABASE_URL` variable in your `.env` file.
2. **Inspect database integrity**
For SQLite:
```bash
sqlite3 database.db "PRAGMA integrity_check;"
```
(For SQLite databases)
For PostgreSQL (recommended for production):
```bash
docker compose exec api python -c "from app.database import engine; print(engine.url)"
```
3. **Perform database migrations**
```bash
docker-compose exec api alembic upgrade head
docker compose exec api alembic upgrade head
```
Ensure the database schema is up-to-date.
4. **Consider PostgreSQL for production**
SQLite is suitable for small deployments, but PostgreSQL is recommended for multi-user production environments. See the [Database Configuration Guide](DatabaseConfiguration.md).
## Authentication Problems
#### Symptoms
### Symptoms
- Unable to log in
- Redirect loops during authentication
- OAuth errors
#### Possible Solutions
1. **Verify Authentik configuration**
Check client ID, client secret, and configuration URL.
### Possible Solutions
1. **Verify OAuth/OIDC configuration**
Check client ID, client secret, and configuration URL for your identity provider.
2. **Check callback URLs**
Ensure the redirect URIs are correctly configured in your OAuth provider.
Ensure the redirect URIs are correctly configured in your OAuth provider. The callback URL is typically `https://your-domain/auth/callback`.
3. **Clear browser cookies and cache**
Authentication issues can sometimes be resolved by clearing browser data.
4. **Check social login credentials**
If using social login (Google, Microsoft, Apple, Dropbox), verify the corresponding `SOCIAL_AUTH_*` environment variables.
5. **Verify `EXTERNAL_HOSTNAME`**
The `EXTERNAL_HOSTNAME` setting must match the domain users access DocuElevate from — OAuth redirect URLs depend on it.
## Mobile App Issues
### Symptoms
- Can't connect to DocuElevate from the mobile app
- Push notifications not received
- Login fails
### Possible Solutions
1. **Verify the server URL**
Ensure the mobile app is configured with the correct DocuElevate server URL (including `https://`).
2. **Check API token**
Generate a fresh API token from the web UI (Profile → API Tokens) and enter it in the mobile app settings.
3. **Check network connectivity**
The mobile device must be able to reach your DocuElevate server. If using a private network, ensure VPN is connected.
4. **Push notifications**
Push notifications require a valid Expo push token. Check the app settings and ensure notifications are enabled at the OS level.
See the [Mobile App Guide](MobileApp.md) for detailed setup instructions.
## CLI Issues
### Symptoms
- CLI commands fail with connection errors
- Authentication rejected
### Possible Solutions
1. **Verify URL and token**
```bash
docuelevate --url https://your-instance --token de_xxx list
```
Ensure the URL is correct (include the scheme) and the API token is valid.
2. **Check environment variables**
The CLI reads `DOCUELEVATE_URL` and `DOCUELEVATE_API_TOKEN` from the environment. Verify they are exported.
3. **Test API directly**
```bash
curl -H "Authorization: Bearer de_xxx" https://your-instance/api/files
```
If this fails, the issue is with the server, not the CLI.
See the [CLI Guide](CLIGuide.md) for detailed usage.
## Performance Issues
### Symptoms
- Slow document processing
- High memory usage
- Queue backing up
### Possible Solutions
1. **Check worker concurrency**
The Celery worker processes tasks in parallel. If the queue is backing up, consider scaling workers or adjusting concurrency.
2. **Enable batch throttling**
Set `PROCESSALL_THROTTLE_THRESHOLD` and `PROCESSALL_THROTTLE_DELAY` to prevent overwhelming external APIs.
3. **Monitor the queue**
Visit the **Admin → Queue** page to see pending, active, and failed tasks.
4. **Use PostgreSQL**
SQLite can become a bottleneck under load. Migrate to PostgreSQL for better concurrent performance. See the [Database Configuration Guide](DatabaseConfiguration.md).
5. **Check Redis memory**
```bash
docker compose exec redis redis-cli info memory
```
Ensure Redis has sufficient memory for the task queue and cache.
## Getting Additional Help
If you continue to experience issues after trying these solutions:
1. **Check the logs** for detailed error messages
1. **Check the logs** for detailed error messages:
```bash
docker-compose logs --tail=100
docker compose logs --tail=200
```
2. **Open an issue** on the [GitHub repository](https://github.com/christianlouis/document-processor/issues)
2. **Check the status page** at `/status` in the web UI for an overview of all service connections.
3. **Contact the developer** via the information provided on the About page
3. **Open an issue** on the [GitHub repository](https://github.com/christianlouis/DocuElevate/issues) with:
- A description of the problem
- Relevant log output
- Your DocuElevate version (shown on the About page or in the `VERSION` file)
4. **Consult additional documentation**:
- [Configuration Guide](ConfigurationGuide.md) — All environment variables
- [Configuration Troubleshooting](ConfigurationTroubleshooting.md) — Configuration-specific issues
- [Deployment Guide](DeploymentGuide.md) — Infrastructure and deployment
+63 -2
View File
@@ -63,6 +63,15 @@ DocuElevate features a simple navigation system with the following main sections
- **Search**: Dedicated full-text search across all document content
- **About**: Information about DocuElevate
### Other Ways to Use DocuElevate
Beyond the web interface, DocuElevate is available through several additional clients:
- **Mobile App (iOS & Android)** — Capture documents with your phone camera or upload from your photo library. See the [Mobile App Guide](MobileApp.md) for setup and usage.
- **Browser Extension** — Clip web pages or send files to DocuElevate directly from Chrome, Firefox, or Edge. See the [Browser Extension Guide](BrowserExtension.md) for installation.
- **CLI Tool** — Upload, download, search, and manage documents from the command line or scripts. See the [CLI Guide](CLIGuide.md) for details.
- **REST & GraphQL API** — Full programmatic access for automation and integrations. See the [API Documentation](API.md).
## Uploading Documents
DocuElevate provides multiple convenient ways to upload documents to the system.
@@ -78,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
@@ -161,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
@@ -626,6 +635,58 @@ Pass no `pipeline_id` to clear the assignment and fall back to the system defaul
Admins can create **system pipelines** that appear in every user's pipeline list. These can be set as the global default so all users benefit from a consistent processing baseline. Navigate to **Pipelines** and check the **System pipeline** box when creating a new one (admin only).
### Conditional routing rules
Routing rules automatically assign incoming documents to the right pipeline
based on their properties — no manual pipeline selection required.
**How it works:**
1. Define one or more routing rules via the API
(`POST /api/routing-rules`).
2. Each rule specifies a **field** to inspect, an **operator** (condition),
a **value** to compare against, and a **target pipeline**.
3. When a document is processed, rules are evaluated **in position order**
(lowest first). The first matching rule wins and the document is routed
to that pipeline.
4. If no rule matches, the document is processed by the default pipeline.
**Available fields:**
| Field | Description |
|-------|-------------|
| `file_type` | MIME type, e.g. `application/pdf` |
| `filename` | Original filename |
| `size` | File size in bytes |
| `document_type` | AI-classified type (Invoice, Contract, …) |
| `category` | Alias for `document_type` |
| `metadata.<key>` | Any key from the AI-extracted metadata JSON |
**Available operators:**
| Operator | Description |
|----------|-------------|
| `equals` / `not_equals` | Exact match (case-insensitive) |
| `contains` / `not_contains` | Substring match (case-insensitive) |
| `regex` | Full Python regex match (case-insensitive) |
| `gt` / `lt` / `gte` / `lte` | Numeric comparison (greater/less than) |
**Example:** Route invoices to one pipeline and large files to another:
```
Rule 1: field=document_type, operator=equals, value=Invoice, target_pipeline=3
Rule 2: field=size, operator=gt, value=1048576, target_pipeline=5
```
With first-match-wins logic, an invoice of any size matches Rule 1 and is
routed to pipeline 3. A non-invoice file larger than 1 MB matches Rule 2
and is routed to pipeline 5. Everything else falls back to the default
pipeline.
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).
## API Access
For programmatic access, DocuElevate provides a comprehensive REST API:
+48
View File
@@ -63,6 +63,54 @@ DocuElevate will process the following attachment types from emails:
| TIFF | `.tif`, `.tiff` | Common format from older scanners/fax |
| Multi-page TIFF | `.tif` | Full multi-page support |
### Controlling Which Attachment Types Are Ingested
By default, DocuElevate only ingests **document** attachments (PDFs, Word, Excel, PowerPoint, OpenDocument, RTF, TXT, CSV, HTML, Markdown). Images are **not** ingested by default — this prevents cluttering your document archive with inline images or unrelated photo attachments.
#### Global Default (Admin Setting)
Set the `IMAP_ATTACHMENT_FILTER` environment variable to control the system-wide fallback when no ingestion profile is assigned to a mailbox:
| Value | Behaviour |
|-------|-----------|
| `documents_only` | **(Default)** Only PDFs and office/document files. Images are skipped. |
| `all` | All supported file types, including images. |
```env
IMAP_ATTACHMENT_FILTER=documents_only
```
#### Ingestion Profiles (Fine-Grained Per-Mailbox Control)
For precise control, you can create **Ingestion Profiles** that let you pick exactly which file-type categories to accept from each mailbox. This is more powerful than the binary global toggle and works independently per mailbox.
**Available categories:**
| Category | File types included |
|----------|---------------------|
| PDF | `.pdf` |
| Microsoft Office | `.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, `.pptx`, and macro-enabled variants |
| OpenDocument | `.odt`, `.ods`, `.odp`, `.odg`, `.odf` (LibreOffice / OpenOffice) |
| Text & Data | `.txt`, `.csv`, `.rtf` |
| Web & Markup | `.html`, `.htm`, `.md`, `.markdown` |
| Images | `.jpg`, `.png`, `.gif`, `.bmp`, `.tiff`, `.webp`, `.svg` |
**Managing profiles:**
1. Go to **Email Ingestion** (`/imap-accounts`)
2. Click **Manage profiles** (or the **+** icon next to the profile dropdown)
3. Create a new profile, give it a name, and tick the categories you want
4. When adding or editing a mailbox, select your profile from the dropdown
Two built-in profiles are always available and cannot be deleted:
- **Documents Only** — PDF, Office, OpenDocument, Text, Web (no images)
- **All Files** — all categories including images
Users can also create unlimited **custom profiles** to mix and match exactly the categories they need per mailbox (e.g. a scanner mailbox that only accepts PDFs, or a finance mailbox that accepts Office and CSV but not images).
Custom profiles are created via the UI or the `/api/imap-profiles/` API.
---
## Setting Up Your Scanner/Device