mirror of
https://github.com/btouchard/ackify.git
synced 2025-12-30 09:29:41 -06:00
feat: improve build stage
This commit is contained in:
684
docs/en/admin-guide.md
Normal file
684
docs/en/admin-guide.md
Normal file
@@ -0,0 +1,684 @@
|
||||
# Admin Guide
|
||||
|
||||
Complete guide for administrators using Ackify to manage documents, expected signers, and email reminders.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Getting Admin Access](#getting-admin-access)
|
||||
- [Admin Dashboard](#admin-dashboard)
|
||||
- [Document Management](#document-management)
|
||||
- [Expected Signers](#expected-signers)
|
||||
- [Email Reminders](#email-reminders)
|
||||
- [Monitoring & Statistics](#monitoring--statistics)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Getting Admin Access
|
||||
|
||||
### Prerequisites
|
||||
|
||||
To access admin features, your email must be configured in the `ACKIFY_ADMIN_EMAILS` environment variable.
|
||||
|
||||
```bash
|
||||
# In .env file
|
||||
ACKIFY_ADMIN_EMAILS=admin@company.com,manager@company.com
|
||||
```
|
||||
|
||||
**After adding your email:**
|
||||
1. Restart Ackify: `docker compose restart ackify-ce`
|
||||
2. Log out and log in again
|
||||
3. You should now see "Admin" link in the navigation
|
||||
|
||||
### Verify Admin Access
|
||||
|
||||
Visit `/admin` - if you see the admin dashboard, you have admin access.
|
||||
|
||||
---
|
||||
|
||||
## Admin Dashboard
|
||||
|
||||
**URL**: `/admin`
|
||||
|
||||
The admin dashboard provides:
|
||||
- **Total Documents**: Number of documents in the system
|
||||
- **Expected Readers**: Total number of expected signers across all documents
|
||||
- **Active Documents**: Documents that are not soft-deleted
|
||||
- **Document List**: Paginated list (20 per page) with search
|
||||
|
||||
### Dashboard Features
|
||||
|
||||
#### Quick Stats
|
||||
Three KPI cards at the top:
|
||||
- Total documents count
|
||||
- Total expected readers/signers
|
||||
- Active documents (non-deleted)
|
||||
|
||||
#### Document Search
|
||||
- Search by title, document ID, or URL
|
||||
- Real-time filtering
|
||||
|
||||
#### Document List
|
||||
**Desktop view** - Table with columns:
|
||||
- Document ID
|
||||
- Title
|
||||
- URL
|
||||
- Created date
|
||||
- Creator
|
||||
- Actions (View details)
|
||||
|
||||
**Mobile view** - Card layout with:
|
||||
- Document ID and title
|
||||
- Creation info
|
||||
- Tap to view details
|
||||
|
||||
#### Pagination
|
||||
- 20 documents per page
|
||||
- Previous/Next buttons
|
||||
- Current page indicator
|
||||
|
||||
---
|
||||
|
||||
## Document Management
|
||||
|
||||
### Creating a Document
|
||||
|
||||
**From Admin Dashboard:**
|
||||
1. Click "Create New Document" button
|
||||
2. Fill in the form:
|
||||
- **Reference** (required): URL, file path, or custom ID
|
||||
- **Title** (optional): Auto-generated from URL if empty
|
||||
- **Description** (optional): Additional context
|
||||
3. Click "Create Document"
|
||||
|
||||
**Automatic Features:**
|
||||
- **Unique ID Generation**: Collision-resistant base36 doc_id
|
||||
- **Title Extraction**: Auto-extracts from URL if not provided
|
||||
- **Checksum Calculation**: For remote URLs (if admin and file < 10MB)
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Reference: https://docs.company.com/policy-2025.pdf
|
||||
Title: Security Policy 2025 (auto-extracted or manual)
|
||||
Description: Annual security compliance policy
|
||||
```
|
||||
|
||||
**Result:**
|
||||
- doc_id: `k7m2n4p8` (auto-generated)
|
||||
- Checksum: Auto-calculated SHA-256 (if URL is accessible)
|
||||
|
||||
### Viewing Document Details
|
||||
|
||||
**URL**: `/admin/docs/{docId}`
|
||||
|
||||
Provides comprehensive document information:
|
||||
|
||||
#### 1. **Metadata Section**
|
||||
Edit document information:
|
||||
- Title
|
||||
- URL
|
||||
- Description
|
||||
- Checksum (SHA-256, SHA-512, or MD5)
|
||||
- Checksum Algorithm
|
||||
|
||||
**To edit:**
|
||||
1. Click "Edit Metadata" button
|
||||
2. Modify fields
|
||||
3. Click "Save Changes"
|
||||
4. Confirmation modal for critical changes (checksum, algorithm)
|
||||
|
||||
#### 2. **Statistics Panel**
|
||||
Real-time signature tracking:
|
||||
- **Expected**: Number of expected signers
|
||||
- **Signed**: Number who have signed
|
||||
- **Pending**: Not yet signed
|
||||
- **Completion**: Percentage complete
|
||||
|
||||
#### 3. **Expected Signers Section**
|
||||
Lists all expected signers with status:
|
||||
- **Email**: Signer's email address
|
||||
- **Status**: ✅ Signed or ⏳ Pending
|
||||
- **Added**: Date when added to expected list
|
||||
- **Days Since Added**: Time tracking
|
||||
- **Last Reminder**: When last reminder was sent
|
||||
- **Reminder Count**: Total reminders sent
|
||||
- **Actions**: Remove signer button
|
||||
|
||||
**Color coding:**
|
||||
- Green background: Signer has signed
|
||||
- Default background: Pending signature
|
||||
|
||||
#### 4. **Unexpected Signatures**
|
||||
Shows users who signed but weren't on expected list:
|
||||
- User email
|
||||
- Signed date
|
||||
- Indicates organic/unexpected participation
|
||||
|
||||
#### 5. **Actions**
|
||||
- **Send Reminders**: Email pending signers
|
||||
- **Share Link**: Generate and copy signing link
|
||||
- **Delete Document**: Soft delete (preserves signature history)
|
||||
|
||||
### Updating Document Metadata
|
||||
|
||||
**Important fields:**
|
||||
|
||||
**Title & Description:**
|
||||
- Can be changed freely
|
||||
- No confirmation required
|
||||
|
||||
**URL:**
|
||||
- Updates where document is located
|
||||
- Confirmation modal shown
|
||||
|
||||
**Checksum & Algorithm:**
|
||||
- Critical for integrity verification
|
||||
- Confirmation modal warns of impact
|
||||
- Change only if document version changed
|
||||
|
||||
**Workflow:**
|
||||
1. Click "Edit Metadata"
|
||||
2. Modify desired fields
|
||||
3. Click "Save Changes"
|
||||
4. If checksum/algorithm changed, confirm in modal
|
||||
5. Success notification displayed
|
||||
|
||||
### Deleting a Document
|
||||
|
||||
**Soft Delete Behavior:**
|
||||
- Document marked as deleted (`deleted_at` timestamp set)
|
||||
- Signature history preserved
|
||||
- Document no longer appears in public lists
|
||||
- Admin can still view via direct URL
|
||||
- Signatures CASCADE update (marked with `doc_deleted_at`)
|
||||
|
||||
**To delete:**
|
||||
1. Go to document detail page (`/admin/docs/{docId}`)
|
||||
2. Click "Delete Document" button
|
||||
3. Confirm deletion in modal
|
||||
4. Document moved to deleted state
|
||||
|
||||
**Note**: There is no "undelete" - this is permanent soft delete.
|
||||
|
||||
---
|
||||
|
||||
## Expected Signers
|
||||
|
||||
Expected signers are users you want to track for document completion.
|
||||
|
||||
### Adding Expected Signers
|
||||
|
||||
**From document detail page:**
|
||||
1. Scroll to "Expected Signers" section
|
||||
2. Click "Add Expected Signer" button
|
||||
3. Enter email address(es):
|
||||
- Single: `alice@company.com`
|
||||
- Multiple: Comma-separated `alice@company.com,bob@company.com`
|
||||
4. Optionally add notes
|
||||
5. Click "Add"
|
||||
|
||||
**API endpoint:**
|
||||
```http
|
||||
POST /api/v1/admin/documents/{docId}/signers
|
||||
Content-Type: application/json
|
||||
X-CSRF-Token: {token}
|
||||
|
||||
{
|
||||
"emails": ["alice@company.com", "bob@company.com"],
|
||||
"notes": "Board members - Q1 2025"
|
||||
}
|
||||
```
|
||||
|
||||
**Constraints:**
|
||||
- Email must be valid format
|
||||
- UNIQUE constraint: Cannot add same email twice to same document
|
||||
- Added by current admin user (tracked in `added_by`)
|
||||
|
||||
### Removing Expected Signers
|
||||
|
||||
**From document detail page:**
|
||||
1. Find signer in Expected Signers list
|
||||
2. Click "Remove" button next to their email
|
||||
3. Confirm removal
|
||||
|
||||
**API endpoint:**
|
||||
```http
|
||||
DELETE /api/v1/admin/documents/{docId}/signers/{email}
|
||||
X-CSRF-Token: {token}
|
||||
```
|
||||
|
||||
**Effect:**
|
||||
- Signer removed from expected list
|
||||
- Does NOT delete their signature if they already signed
|
||||
- Reminder history preserved in `reminder_logs`
|
||||
|
||||
### Tracking Completion Status
|
||||
|
||||
**Document Status API:**
|
||||
```http
|
||||
GET /api/v1/admin/documents/{docId}/status
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"docId": "abc123",
|
||||
"expectedCount": 10,
|
||||
"signedCount": 7,
|
||||
"pendingCount": 3,
|
||||
"completionPercentage": 70.0
|
||||
}
|
||||
```
|
||||
|
||||
**Visual indicators:**
|
||||
- Progress bar showing completion percentage
|
||||
- Color-coded status: Green (signed), Orange (pending)
|
||||
- Days since added (helps identify slow signers)
|
||||
|
||||
---
|
||||
|
||||
## Email Reminders
|
||||
|
||||
Email reminders are sent asynchronously via the `email_queue` system.
|
||||
|
||||
### Sending Reminders
|
||||
|
||||
**From document detail page:**
|
||||
1. Click "Send Reminders" button
|
||||
2. Modal opens with options:
|
||||
- **Send to**: All pending OR specific emails
|
||||
- **Document URL**: Pre-filled, can customize
|
||||
- **Language**: en, fr, es, de, it
|
||||
3. Click "Send Reminders"
|
||||
4. Confirmation: "X reminders queued for sending"
|
||||
|
||||
**API endpoint:**
|
||||
```http
|
||||
POST /api/v1/admin/documents/{docId}/reminders
|
||||
Content-Type: application/json
|
||||
X-CSRF-Token: {token}
|
||||
|
||||
{
|
||||
"emails": ["alice@company.com"], // Optional: specific emails
|
||||
"docURL": "https://docs.company.com/policy.pdf",
|
||||
"locale": "en"
|
||||
}
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- Sends to ALL pending signers if `emails` not specified
|
||||
- Sends to specific `emails` if provided (even if already signed)
|
||||
- Emails queued in `email_queue` table
|
||||
- Background worker processes queue
|
||||
- Retry on failure (3 attempts, exponential backoff)
|
||||
|
||||
### Email Templates
|
||||
|
||||
**Location**: `backend/templates/emails/`
|
||||
|
||||
**Available templates:**
|
||||
- `reminder.html` - HTML version
|
||||
- `reminder.txt` - Plain text version
|
||||
|
||||
**Variables available in templates:**
|
||||
- `{{.DocTitle}}` - Document title
|
||||
- `{{.DocURL}}` - Document URL
|
||||
- `{{.RecipientEmail}}` - Recipient's email
|
||||
- `{{.SenderName}}` - Admin who sent reminder
|
||||
- `{{.OrganisationName}}` - From ACKIFY_ORGANISATION
|
||||
|
||||
**Locales**: en, fr, es, de, it
|
||||
- Template directory: `templates/emails/{locale}/`
|
||||
- Fallback to default locale if translation missing
|
||||
|
||||
### Reminder History
|
||||
|
||||
**View reminder log:**
|
||||
```http
|
||||
GET /api/v1/admin/documents/{docId}/reminders
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"reminders": [
|
||||
{
|
||||
"id": 123,
|
||||
"docId": "abc123",
|
||||
"recipientEmail": "alice@company.com",
|
||||
"sentAt": "2025-01-15T10:30:00Z",
|
||||
"sentBy": "admin@company.com",
|
||||
"templateUsed": "reminder",
|
||||
"status": "sent",
|
||||
"errorMessage": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Status values:**
|
||||
- `queued` - In email_queue, not yet processed
|
||||
- `sent` - Successfully delivered
|
||||
- `failed` - Delivery failed (check errorMessage)
|
||||
- `bounced` - Email bounced back
|
||||
|
||||
**Tracking:**
|
||||
- Last reminder sent date shown per signer
|
||||
- Reminder count shown per signer
|
||||
- Helps avoid over-sending
|
||||
|
||||
### Email Queue Monitoring
|
||||
|
||||
**Check queue status (PostgreSQL):**
|
||||
```sql
|
||||
-- Pending emails
|
||||
SELECT id, to_addresses, subject, status, scheduled_for
|
||||
FROM email_queue
|
||||
WHERE status IN ('pending', 'processing')
|
||||
ORDER BY priority DESC, scheduled_for ASC;
|
||||
|
||||
-- Failed emails
|
||||
SELECT id, to_addresses, last_error, retry_count
|
||||
FROM email_queue
|
||||
WHERE status = 'failed';
|
||||
```
|
||||
|
||||
**Worker configuration:**
|
||||
- Batch size: 10 emails
|
||||
- Poll interval: 5 seconds
|
||||
- Max retries: 3
|
||||
- Cleanup: 7 days retention
|
||||
|
||||
---
|
||||
|
||||
## Monitoring & Statistics
|
||||
|
||||
### Document-Level Statistics
|
||||
|
||||
**Completion tracking:**
|
||||
- Expected vs Signed counts
|
||||
- Pending signer list
|
||||
- Completion percentage
|
||||
- Average time to sign
|
||||
|
||||
**Reminder effectiveness:**
|
||||
- Reminders sent count
|
||||
- Success/failure rates
|
||||
- Time between reminder and signature
|
||||
|
||||
### System-Wide Metrics
|
||||
|
||||
**PostgreSQL queries:**
|
||||
|
||||
```sql
|
||||
-- Total documents
|
||||
SELECT COUNT(*) FROM documents WHERE deleted_at IS NULL;
|
||||
|
||||
-- Total signatures
|
||||
SELECT COUNT(*) FROM signatures;
|
||||
|
||||
-- Documents by completion status
|
||||
SELECT
|
||||
CASE
|
||||
WHEN signed_count = expected_count THEN '100%'
|
||||
WHEN signed_count >= expected_count * 0.75 THEN '75-99%'
|
||||
WHEN signed_count >= expected_count * 0.50 THEN '50-74%'
|
||||
ELSE '<50%'
|
||||
END as completion_bracket,
|
||||
COUNT(*) as doc_count
|
||||
FROM (
|
||||
SELECT
|
||||
d.doc_id,
|
||||
COUNT(DISTINCT es.email) as expected_count,
|
||||
COUNT(DISTINCT s.user_email) as signed_count
|
||||
FROM documents d
|
||||
LEFT JOIN expected_signers es ON d.doc_id = es.doc_id
|
||||
LEFT JOIN signatures s ON d.doc_id = s.doc_id AND s.user_email = es.email
|
||||
WHERE d.deleted_at IS NULL
|
||||
GROUP BY d.doc_id
|
||||
) stats
|
||||
GROUP BY completion_bracket;
|
||||
|
||||
-- Email queue statistics
|
||||
SELECT status, COUNT(*), MIN(created_at), MAX(created_at)
|
||||
FROM email_queue
|
||||
GROUP BY status;
|
||||
```
|
||||
|
||||
### Export Data
|
||||
|
||||
**Signatures for a document:**
|
||||
```sql
|
||||
COPY (
|
||||
SELECT s.user_email, s.user_name, s.signed_at, s.payload_hash
|
||||
FROM signatures s
|
||||
WHERE s.doc_id = 'your_doc_id'
|
||||
ORDER BY s.signed_at
|
||||
) TO '/tmp/signatures_export.csv' WITH CSV HEADER;
|
||||
```
|
||||
|
||||
**Expected signers status:**
|
||||
```sql
|
||||
COPY (
|
||||
SELECT
|
||||
es.email,
|
||||
CASE WHEN s.id IS NOT NULL THEN 'Signed' ELSE 'Pending' END as status,
|
||||
es.added_at,
|
||||
s.signed_at
|
||||
FROM expected_signers es
|
||||
LEFT JOIN signatures s ON es.doc_id = s.doc_id AND es.email = s.user_email
|
||||
WHERE es.doc_id = 'your_doc_id'
|
||||
) TO '/tmp/expected_signers_export.csv' WITH CSV HEADER;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Document Creation
|
||||
|
||||
✅ **Do:**
|
||||
- Use descriptive titles
|
||||
- Add clear descriptions
|
||||
- Include document URL for easy access
|
||||
- Store checksum for integrity verification
|
||||
- Create expected signers list before sharing
|
||||
|
||||
❌ **Don't:**
|
||||
- Use generic titles like "Document 1"
|
||||
- Leave URL empty if document is accessible online
|
||||
- Change checksums unless document actually changed
|
||||
|
||||
### 2. Expected Signers Management
|
||||
|
||||
✅ **Do:**
|
||||
- Add expected signers before sending document link
|
||||
- Use clear notes to explain why signers are expected
|
||||
- Review pending signers regularly
|
||||
- Remove signers who are no longer relevant
|
||||
|
||||
❌ **Don't:**
|
||||
- Add hundreds of signers at once (use batches)
|
||||
- Send reminders too frequently (max once per week)
|
||||
- Remove signers who have already signed (preserve history)
|
||||
|
||||
### 3. Email Reminders
|
||||
|
||||
✅ **Do:**
|
||||
- Wait 3-5 days before first reminder
|
||||
- Send in recipient's preferred language
|
||||
- Include clear document title and URL
|
||||
- Track reminder history to avoid spam
|
||||
- Send reminders during business hours
|
||||
|
||||
❌ **Don't:**
|
||||
- Send daily reminders (causes fatigue)
|
||||
- Send without checking if already signed
|
||||
- Use generic subjects (personalize with doc title)
|
||||
- Send outside business hours
|
||||
|
||||
### 4. Data Integrity
|
||||
|
||||
✅ **Do:**
|
||||
- Regularly backup PostgreSQL database
|
||||
- Verify checksums match actual documents
|
||||
- Monitor email queue for failures
|
||||
- Review unexpected signatures (may indicate broader interest)
|
||||
- Export important signature data
|
||||
|
||||
❌ **Don't:**
|
||||
- Delete documents with active signatures
|
||||
- Modify timestamps manually in database
|
||||
- Ignore failed email deliveries
|
||||
- Change checksums without updating the document
|
||||
|
||||
### 5. Security
|
||||
|
||||
✅ **Do:**
|
||||
- Limit admin access to trusted users only
|
||||
- Use HTTPS in production (`ACKIFY_BASE_URL=https://...`)
|
||||
- Rotate `ACKIFY_OAUTH_COOKIE_SECRET` periodically
|
||||
- Monitor admin actions via application logs
|
||||
- Use OAuth allowed domain restrictions
|
||||
|
||||
❌ **Don't:**
|
||||
- Share admin credentials
|
||||
- Run without HTTPS in production
|
||||
- Disable CSRF protection
|
||||
- Ignore authentication failures in logs
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. Admin Link Not Visible
|
||||
|
||||
**Problem**: Can't see "Admin" link in navigation
|
||||
|
||||
**Solutions:**
|
||||
- Verify email in `ACKIFY_ADMIN_EMAILS` environment variable
|
||||
- Restart Ackify: `docker compose restart ackify-ce`
|
||||
- Log out and log back in
|
||||
- Check logs: `docker compose logs ackify-ce | grep admin`
|
||||
|
||||
#### 2. Emails Not Sending
|
||||
|
||||
**Problem**: Reminders queued but not delivered
|
||||
|
||||
**Diagnosis:**
|
||||
```sql
|
||||
SELECT * FROM email_queue WHERE status = 'failed' ORDER BY created_at DESC LIMIT 10;
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
- Check SMTP configuration (`ACKIFY_MAIL_HOST`, `ACKIFY_MAIL_USERNAME`, etc.)
|
||||
- Verify SMTP credentials are correct
|
||||
- Check email worker logs: `docker compose logs ackify-ce | grep email`
|
||||
- Ensure `ACKIFY_MAIL_FROM` is valid sender address
|
||||
- Test SMTP connection manually
|
||||
|
||||
#### 3. Duplicate Signer Error
|
||||
|
||||
**Problem**: "Email already exists as expected signer"
|
||||
|
||||
**Cause**: UNIQUE constraint on (doc_id, email)
|
||||
|
||||
**Solution**: This is expected behavior - each email can only be added once per document
|
||||
|
||||
#### 4. Checksum Mismatch
|
||||
|
||||
**Problem**: Users report checksum doesn't match
|
||||
|
||||
**Solutions:**
|
||||
- Verify stored checksum matches actual document
|
||||
- Check algorithm used (SHA-256, SHA-512, MD5)
|
||||
- Recalculate checksum and update via Edit Metadata
|
||||
- Ensure users are downloading correct version
|
||||
|
||||
#### 5. Document Not Appearing
|
||||
|
||||
**Problem**: Created document doesn't show in list
|
||||
|
||||
**Solutions:**
|
||||
- Check if document was soft-deleted (`deleted_at IS NOT NULL`)
|
||||
- Verify creation succeeded (check response/logs)
|
||||
- Clear browser cache
|
||||
- Check database: `SELECT * FROM documents WHERE doc_id = 'your_id';`
|
||||
|
||||
#### 6. Signature Already Exists
|
||||
|
||||
**Problem**: User can't sign document again
|
||||
|
||||
**Cause**: UNIQUE constraint (doc_id, user_sub) - one signature per user per document
|
||||
|
||||
**Solution**: This is expected - users cannot sign the same document twice
|
||||
|
||||
### Getting Help
|
||||
|
||||
**Logs:**
|
||||
```bash
|
||||
# Application logs
|
||||
docker compose logs -f ackify-ce
|
||||
|
||||
# Database logs
|
||||
docker compose logs -f ackify-db
|
||||
|
||||
# Email worker logs (grep email)
|
||||
docker compose logs ackify-ce | grep -i email
|
||||
```
|
||||
|
||||
**Database inspection:**
|
||||
```bash
|
||||
# Connect to PostgreSQL
|
||||
docker compose exec ackify-db psql -U ackifyr ackify
|
||||
|
||||
# Useful queries
|
||||
SELECT * FROM documents ORDER BY created_at DESC LIMIT 10;
|
||||
SELECT * FROM expected_signers WHERE doc_id = 'your_doc_id';
|
||||
SELECT * FROM email_queue WHERE status != 'sent' ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
**Report issues:**
|
||||
- GitHub: https://github.com/btouchard/ackify-ce/issues
|
||||
- Include logs and error messages
|
||||
- Describe expected vs actual behavior
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Environment Variables
|
||||
```bash
|
||||
ACKIFY_ADMIN_EMAILS=admin@company.com
|
||||
ACKIFY_MAIL_HOST=smtp.gmail.com
|
||||
ACKIFY_MAIL_FROM=noreply@company.com
|
||||
```
|
||||
|
||||
### Key Endpoints
|
||||
```
|
||||
GET /admin # Dashboard
|
||||
GET /admin/docs/{docId} # Document detail
|
||||
POST /admin/documents/{docId}/signers # Add signer
|
||||
POST /admin/documents/{docId}/reminders # Send reminders
|
||||
PUT /admin/documents/{docId}/metadata # Update metadata
|
||||
```
|
||||
|
||||
### Important Tables
|
||||
- `documents` - Document metadata
|
||||
- `signatures` - User signatures
|
||||
- `expected_signers` - Who should sign
|
||||
- `reminder_logs` - Email history
|
||||
- `email_queue` - Async email queue
|
||||
|
||||
### Keyboard Shortcuts (Frontend)
|
||||
- Search bar auto-focus on dashboard
|
||||
- Enter to submit forms
|
||||
- Esc to close modals
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-10-26
|
||||
**Version**: 1.0.0
|
||||
355
docs/en/architecture.md
Normal file
355
docs/en/architecture.md
Normal file
@@ -0,0 +1,355 @@
|
||||
# Architecture
|
||||
|
||||
Technical stack and design principles of Ackify.
|
||||
|
||||
## Overview
|
||||
|
||||
Ackify is a **modern monolithic application** with clear backend/frontend separation.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Client Browser │
|
||||
│ (Vue.js 3 SPA + TypeScript) │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│ HTTPS / JSON
|
||||
┌──────────────▼──────────────────────────┐
|
||||
│ Go Backend (API-first) │
|
||||
│ ├─ RESTful API v1 (chi router) │
|
||||
│ ├─ OAuth2 Service │
|
||||
│ ├─ Ed25519 Crypto │
|
||||
│ └─ SMTP Email (optional) │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│ PostgreSQL protocol
|
||||
┌──────────────▼──────────────────────────┐
|
||||
│ PostgreSQL 16 Database │
|
||||
│ (Signatures + Metadata + Sessions) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Backend (Go)
|
||||
|
||||
### Simplified Clean Architecture
|
||||
|
||||
```
|
||||
backend/
|
||||
├── cmd/
|
||||
│ ├── community/ # Entry point + dependency injection
|
||||
│ └── migrate/ # SQL migrations tool
|
||||
├── internal/
|
||||
│ ├── domain/
|
||||
│ │ └── models/ # Business entities (User, Signature, Document)
|
||||
│ ├── application/
|
||||
│ │ └── services/ # Business logic (SignatureService, etc.)
|
||||
│ ├── infrastructure/
|
||||
│ │ ├── auth/ # OAuth2 service
|
||||
│ │ ├── database/ # PostgreSQL repositories
|
||||
│ │ ├── email/ # SMTP service
|
||||
│ │ ├── config/ # Environment variables
|
||||
│ │ └── i18n/ # Backend i18n
|
||||
│ └── presentation/
|
||||
│ ├── api/ # HTTP handlers API v1
|
||||
│ └── handlers/ # Legacy OAuth handlers
|
||||
├── pkg/
|
||||
│ ├── crypto/ # Ed25519 signatures
|
||||
│ ├── logger/ # Structured logging
|
||||
│ ├── services/ # OAuth provider detection
|
||||
│ └── web/ # HTTP server setup
|
||||
├── migrations/ # SQL migrations
|
||||
├── templates/ # Email templates (HTML/text)
|
||||
└── locales/ # Backend translations
|
||||
```
|
||||
|
||||
### Applied Go Principles
|
||||
|
||||
**Interfaces**:
|
||||
- ✅ Defined in the package that uses them
|
||||
- ✅ Principle "accept interfaces, return structs"
|
||||
- ✅ Repositories implemented in `infrastructure/database/`
|
||||
|
||||
**Dependency Injection**:
|
||||
- ✅ Explicit constructors in `main.go`
|
||||
- ✅ No complex DI container
|
||||
- ✅ Clear and visible dependencies
|
||||
|
||||
**Code Quality**:
|
||||
- ✅ `go fmt` and `go vet` clean
|
||||
- ✅ No dead code
|
||||
- ✅ Simple and focused interfaces
|
||||
|
||||
## Frontend (Vue.js 3)
|
||||
|
||||
### SPA Structure
|
||||
|
||||
```
|
||||
webapp/
|
||||
├── src/
|
||||
│ ├── components/ # Reusable components
|
||||
│ │ ├── ui/ # shadcn/vue components
|
||||
│ │ └── ...
|
||||
│ ├── pages/ # Pages (router views)
|
||||
│ │ ├── Home.vue
|
||||
│ │ ├── Admin.vue
|
||||
│ │ └── ...
|
||||
│ ├── services/ # API client (axios)
|
||||
│ ├── stores/ # Pinia state management
|
||||
│ ├── router/ # Vue Router config
|
||||
│ ├── locales/ # Translations (fr, en, es, de, it)
|
||||
│ └── composables/ # Vue composables
|
||||
├── public/ # Static assets
|
||||
└── scripts/ # Build scripts
|
||||
```
|
||||
|
||||
### Frontend Stack
|
||||
|
||||
- **Vue 3** - Composition API
|
||||
- **TypeScript** - Type safety
|
||||
- **Vite** - Build tool (fast HMR)
|
||||
- **Pinia** - State management
|
||||
- **Vue Router** - Client routing
|
||||
- **Tailwind CSS** - Utility-first styling
|
||||
- **shadcn/vue** - UI components
|
||||
- **vue-i18n** - Internationalization
|
||||
|
||||
### Routing
|
||||
|
||||
```typescript
|
||||
const routes = [
|
||||
{ path: '/', component: Home }, // Public
|
||||
{ path: '/signatures', component: MySignatures }, // Auth required
|
||||
{ path: '/admin', component: Admin } // Admin only
|
||||
]
|
||||
```
|
||||
|
||||
Frontend handles:
|
||||
- Route `/` with query param `?doc=xxx` → Signature page
|
||||
- Route `/admin` → Admin dashboard
|
||||
- Route `/signatures` → My signatures
|
||||
|
||||
## Database
|
||||
|
||||
### PostgreSQL Schema
|
||||
|
||||
Main tables:
|
||||
- `signatures` - Ed25519 signatures
|
||||
- `documents` - Document metadata
|
||||
- `expected_signers` - Signer tracking
|
||||
- `reminder_logs` - Email history
|
||||
- `checksum_verifications` - Integrity verifications
|
||||
- `oauth_sessions` - OAuth2 sessions + refresh tokens
|
||||
|
||||
See [Database](database.md) for complete schema.
|
||||
|
||||
### Migrations
|
||||
|
||||
- Format: `XXXX_description.up.sql` / `XXXX_description.down.sql`
|
||||
- Applied automatically on startup (service `ackify-migrate`)
|
||||
- Tool: `/backend/cmd/migrate`
|
||||
|
||||
## Security
|
||||
|
||||
### Cryptography
|
||||
|
||||
**Ed25519**:
|
||||
- Digital signatures (elliptic curve)
|
||||
- 256-bit private key
|
||||
- Guaranteed non-repudiation
|
||||
|
||||
**SHA-256**:
|
||||
- Payload hashing before signing
|
||||
- Tampering detection
|
||||
- Blockchain-like chaining (`prev_hash`)
|
||||
|
||||
**AES-256-GCM**:
|
||||
- OAuth2 refresh token encryption
|
||||
- Key derived from `ACKIFY_OAUTH_COOKIE_SECRET`
|
||||
|
||||
### OAuth2 + PKCE
|
||||
|
||||
**Flow**:
|
||||
1. Client generates `code_verifier` (random)
|
||||
2. Calculates `code_challenge = SHA256(code_verifier)`
|
||||
3. Auth request with `code_challenge`
|
||||
4. Provider returns `code`
|
||||
5. Token exchange with `code + code_verifier`
|
||||
|
||||
**Security**:
|
||||
- Protection against code interception
|
||||
- S256 method (SHA-256)
|
||||
- Automatically enabled
|
||||
|
||||
### Sessions
|
||||
|
||||
- Secure cookies (HttpOnly, Secure, SameSite=Lax)
|
||||
- HMAC-SHA256 encryption
|
||||
- PostgreSQL storage with encrypted refresh tokens
|
||||
- Duration: 30 days
|
||||
- Automatic cleanup: 37 days
|
||||
|
||||
## Build & Deployment
|
||||
|
||||
### Multi-Stage Docker
|
||||
|
||||
```dockerfile
|
||||
# Stage 1 - Frontend build
|
||||
FROM node:22-alpine AS frontend
|
||||
COPY webapp/ /build/webapp/
|
||||
RUN npm ci && npm run build
|
||||
# Output: webapp/dist/
|
||||
|
||||
# Stage 2 - Backend build + embed frontend
|
||||
FROM golang:alpine AS backend
|
||||
ENV GOTOOLCHAIN=auto
|
||||
COPY backend/ /build/backend/
|
||||
COPY --from=frontend /build/webapp/dist/ /build/backend/cmd/community/web/dist/
|
||||
RUN go build -o community ./cmd/community
|
||||
# Frontend embedded via embed.FS
|
||||
|
||||
# Stage 3 - Runtime (distroless)
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
COPY --from=backend /build/backend/community /app/community
|
||||
CMD ["/app/community"]
|
||||
```
|
||||
|
||||
**Result**:
|
||||
- Final image < 30 MB
|
||||
- Single binary (backend + frontend)
|
||||
- No runtime dependencies
|
||||
|
||||
### Runtime Injection
|
||||
|
||||
`ACKIFY_BASE_URL` is injected into `index.html` at startup:
|
||||
|
||||
```go
|
||||
// Replaces __ACKIFY_BASE_URL__ with actual value
|
||||
html = strings.ReplaceAll(html, "__ACKIFY_BASE_URL__", baseURL)
|
||||
```
|
||||
|
||||
Allows changing domain without rebuild.
|
||||
|
||||
## Performance
|
||||
|
||||
### Backend
|
||||
|
||||
- **Connection pooling** PostgreSQL (25 max)
|
||||
- **Prepared statements** - SQL injection prevention
|
||||
- **Rate limiting** - 5 auth/min, 10 doc/min, 100 req/min
|
||||
- **Structured logging** - JSON with request IDs
|
||||
|
||||
### Frontend
|
||||
|
||||
- **Code splitting** - Lazy loading routes
|
||||
- **Tree shaking** - Dead code elimination
|
||||
- **Minification** - Optimized production builds
|
||||
- **HMR** - Hot Module Replacement (dev)
|
||||
|
||||
### Database
|
||||
|
||||
- **Indexes** on (doc_id, user_sub, session_id)
|
||||
- **Constraints** UNIQUE for guarantees
|
||||
- **Triggers** for immutability
|
||||
- **Autovacuum** enabled
|
||||
|
||||
## Scalability
|
||||
|
||||
### Current Limits
|
||||
|
||||
- ✅ Monolith: ~10k req/s
|
||||
- ✅ PostgreSQL: Single instance
|
||||
- ✅ Sessions: In-database (no Redis)
|
||||
|
||||
### Horizontal Scaling (future)
|
||||
|
||||
For > 100k req/s:
|
||||
1. **Load Balancer** - Multiple backend instances
|
||||
2. **PostgreSQL read replicas** - Separate read/write
|
||||
3. **Redis** - Session cache + rate limiting
|
||||
4. **CDN** - Static assets
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Structured Logs
|
||||
|
||||
JSON format:
|
||||
```json
|
||||
{
|
||||
"level": "info",
|
||||
"timestamp": "2025-01-15T14:30:00Z",
|
||||
"request_id": "abc123",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/signatures",
|
||||
"duration_ms": 42,
|
||||
"status": 201
|
||||
}
|
||||
```
|
||||
|
||||
### Health Check
|
||||
|
||||
```http
|
||||
GET /api/v1/health
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"database": "connected"
|
||||
}
|
||||
```
|
||||
|
||||
### Metrics (future)
|
||||
|
||||
- Prometheus metrics endpoint
|
||||
- Grafana dashboards
|
||||
- Alerting (PagerDuty, Slack)
|
||||
|
||||
## Tests
|
||||
|
||||
### Coverage
|
||||
|
||||
**72.6% code coverage** (unit + integration)
|
||||
|
||||
- Unit tests: 180+ tests
|
||||
- Integration tests: 33 PostgreSQL tests
|
||||
- CI/CD: GitHub Actions + Codecov
|
||||
|
||||
See [Development](development.md) to run tests.
|
||||
|
||||
## Technical Choices
|
||||
|
||||
### Why Go?
|
||||
|
||||
- ✅ Native performance (compiled)
|
||||
- ✅ Simple concurrency (goroutines)
|
||||
- ✅ Strong typing
|
||||
- ✅ Single binary
|
||||
- ✅ Simple deployment
|
||||
|
||||
### Why Vue 3?
|
||||
|
||||
- ✅ Modern Composition API
|
||||
- ✅ Native TypeScript
|
||||
- ✅ Reactive by default
|
||||
- ✅ Rich ecosystem
|
||||
- ✅ Excellent performance
|
||||
|
||||
### Why PostgreSQL?
|
||||
|
||||
- ✅ ACID compliance
|
||||
- ✅ Integrity constraints
|
||||
- ✅ Triggers
|
||||
- ✅ JSON support
|
||||
- ✅ Mature and stable
|
||||
|
||||
### Why Ed25519?
|
||||
|
||||
- ✅ Modern security (elliptic curve)
|
||||
- ✅ Performance > RSA
|
||||
- ✅ Short signatures (64 bytes)
|
||||
- ✅ Standard crypto/ed25519 Go
|
||||
|
||||
## References
|
||||
|
||||
- [Go Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
- [Vue 3 Composition API](https://vuejs.org/guide/extras/composition-api-faq.html)
|
||||
- [Ed25519 Spec](https://ed25519.cr.yp.to/)
|
||||
- [OAuth2 + PKCE](https://oauth.net/2/pkce/)
|
||||
604
docs/en/database.md
Normal file
604
docs/en/database.md
Normal file
@@ -0,0 +1,604 @@
|
||||
# Database
|
||||
|
||||
PostgreSQL schema, migrations, and integrity guarantees.
|
||||
|
||||
## Overview
|
||||
|
||||
Ackify uses **PostgreSQL 16+** with:
|
||||
- Versioned SQL migrations
|
||||
- Strict integrity constraints
|
||||
- Triggers for immutability
|
||||
- Indexes for performance
|
||||
|
||||
## Main Schema
|
||||
|
||||
### Table `signatures`
|
||||
|
||||
Stores Ed25519 cryptographic signatures.
|
||||
|
||||
```sql
|
||||
CREATE TABLE signatures (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
doc_id TEXT NOT NULL,
|
||||
user_sub TEXT NOT NULL, -- OAuth user ID (sub claim)
|
||||
user_email TEXT NOT NULL,
|
||||
user_name TEXT, -- User name (optional)
|
||||
signed_at TIMESTAMPTZ NOT NULL,
|
||||
payload_hash TEXT NOT NULL, -- SHA-256 of payload
|
||||
signature TEXT NOT NULL, -- Ed25519 signature (base64)
|
||||
nonce TEXT NOT NULL, -- Anti-replay attack
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
referer TEXT, -- Source (optional)
|
||||
prev_hash TEXT, -- Hash of previous signature (chaining)
|
||||
UNIQUE (doc_id, user_sub) -- ONE signature per user/document
|
||||
);
|
||||
|
||||
CREATE INDEX idx_signatures_doc_id ON signatures(doc_id);
|
||||
CREATE INDEX idx_signatures_user_sub ON signatures(user_sub);
|
||||
```
|
||||
|
||||
**Guarantees**:
|
||||
- ✅ One signature per user/document (UNIQUE constraint)
|
||||
- ✅ Immutable timestamp via PostgreSQL trigger
|
||||
- ✅ Hash chaining (blockchain-like) via `prev_hash`
|
||||
- ✅ Cryptographic non-repudiation (Ed25519)
|
||||
|
||||
### Table `documents`
|
||||
|
||||
Document metadata.
|
||||
|
||||
```sql
|
||||
CREATE TABLE documents (
|
||||
doc_id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
url TEXT NOT NULL DEFAULT '', -- Source document URL
|
||||
checksum TEXT NOT NULL DEFAULT '', -- SHA-256, SHA-512, or MD5
|
||||
checksum_algorithm TEXT NOT NULL DEFAULT 'SHA-256',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
created_by TEXT NOT NULL DEFAULT '' -- Creator admin's user_sub
|
||||
);
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
- Title, description displayed in interface
|
||||
- URL included in reminder emails
|
||||
- Checksum for integrity verification (optional)
|
||||
|
||||
### Table `expected_signers`
|
||||
|
||||
Expected signers for tracking.
|
||||
|
||||
```sql
|
||||
CREATE TABLE expected_signers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
doc_id TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '', -- Name for personalization
|
||||
added_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
added_by TEXT NOT NULL, -- Admin who added
|
||||
notes TEXT,
|
||||
UNIQUE (doc_id, email)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_expected_signers_doc_id ON expected_signers(doc_id);
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- Completion tracking (% signed)
|
||||
- Email reminder sending
|
||||
- Unexpected signature detection
|
||||
|
||||
### Table `reminder_logs`
|
||||
|
||||
Email reminder history.
|
||||
|
||||
```sql
|
||||
CREATE TABLE reminder_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
doc_id TEXT NOT NULL,
|
||||
recipient_email TEXT NOT NULL,
|
||||
sent_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
sent_by TEXT NOT NULL, -- Admin who sent
|
||||
template_used TEXT NOT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('sent', 'failed', 'bounced')),
|
||||
error_message TEXT,
|
||||
FOREIGN KEY (doc_id, recipient_email)
|
||||
REFERENCES expected_signers(doc_id, email)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_reminder_logs_doc_id ON reminder_logs(doc_id);
|
||||
```
|
||||
|
||||
### Table `checksum_verifications`
|
||||
|
||||
Integrity verification history.
|
||||
|
||||
```sql
|
||||
CREATE TABLE checksum_verifications (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
doc_id TEXT NOT NULL,
|
||||
verified_by TEXT NOT NULL,
|
||||
verified_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
stored_checksum TEXT NOT NULL,
|
||||
calculated_checksum TEXT NOT NULL,
|
||||
algorithm TEXT NOT NULL,
|
||||
is_valid BOOLEAN NOT NULL,
|
||||
error_message TEXT,
|
||||
FOREIGN KEY (doc_id) REFERENCES documents(doc_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_checksum_verifications_doc_id ON checksum_verifications(doc_id);
|
||||
```
|
||||
|
||||
### Table `oauth_sessions`
|
||||
|
||||
OAuth2 sessions with encrypted refresh tokens.
|
||||
|
||||
```sql
|
||||
CREATE TABLE oauth_sessions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
session_id TEXT NOT NULL UNIQUE, -- Gorilla session ID
|
||||
user_sub TEXT NOT NULL, -- OAuth user ID
|
||||
refresh_token_encrypted BYTEA NOT NULL, -- Encrypted AES-256-GCM
|
||||
access_token_expires_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
last_refreshed_at TIMESTAMPTZ,
|
||||
user_agent TEXT,
|
||||
ip_address INET
|
||||
);
|
||||
|
||||
CREATE INDEX idx_oauth_sessions_session_id ON oauth_sessions(session_id);
|
||||
CREATE INDEX idx_oauth_sessions_user_sub ON oauth_sessions(user_sub);
|
||||
CREATE INDEX idx_oauth_sessions_updated_at ON oauth_sessions(updated_at);
|
||||
```
|
||||
|
||||
**Security**:
|
||||
- Encrypted refresh tokens (AES-256-GCM)
|
||||
- Automatic cleanup after 37 days
|
||||
- IP + User-Agent tracking to detect theft
|
||||
|
||||
### Table `email_queue`
|
||||
|
||||
Asynchronous email queue with retry mechanism.
|
||||
|
||||
```sql
|
||||
CREATE TABLE email_queue (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
-- Email metadata
|
||||
to_addresses TEXT[] NOT NULL, -- Recipient email addresses
|
||||
cc_addresses TEXT[], -- CC addresses (optional)
|
||||
bcc_addresses TEXT[], -- BCC addresses (optional)
|
||||
subject TEXT NOT NULL, -- Email subject
|
||||
template TEXT NOT NULL, -- Template name (e.g., 'reminder')
|
||||
locale TEXT NOT NULL DEFAULT 'fr', -- Email language (en, fr, es, de, it)
|
||||
data JSONB NOT NULL DEFAULT '{}', -- Template variables
|
||||
headers JSONB, -- Custom email headers (optional)
|
||||
|
||||
-- Queue management
|
||||
status TEXT NOT NULL DEFAULT 'pending' -- pending, processing, sent, failed, cancelled
|
||||
CHECK (status IN ('pending', 'processing', 'sent', 'failed', 'cancelled')),
|
||||
priority INT NOT NULL DEFAULT 0, -- Higher = processed first (0=normal, 10=high, 100=urgent)
|
||||
retry_count INT NOT NULL DEFAULT 0, -- Number of retry attempts
|
||||
max_retries INT NOT NULL DEFAULT 3, -- Maximum retry limit
|
||||
|
||||
-- Tracking
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
scheduled_for TIMESTAMPTZ NOT NULL DEFAULT now(), -- Earliest processing time
|
||||
processed_at TIMESTAMPTZ, -- When email was sent
|
||||
next_retry_at TIMESTAMPTZ, -- Calculated retry time (exponential backoff)
|
||||
|
||||
-- Error tracking
|
||||
last_error TEXT, -- Last error message
|
||||
error_details JSONB, -- Detailed error information
|
||||
|
||||
-- Reference tracking (optional)
|
||||
reference_type TEXT, -- e.g., 'reminder', 'notification'
|
||||
reference_id TEXT, -- e.g., doc_id
|
||||
created_by TEXT -- User who queued the email
|
||||
);
|
||||
|
||||
-- Indexes for efficient queue processing
|
||||
CREATE INDEX idx_email_queue_status_scheduled
|
||||
ON email_queue(status, scheduled_for)
|
||||
WHERE status IN ('pending', 'processing');
|
||||
|
||||
CREATE INDEX idx_email_queue_priority_scheduled
|
||||
ON email_queue(priority DESC, scheduled_for ASC)
|
||||
WHERE status = 'pending';
|
||||
|
||||
CREATE INDEX idx_email_queue_retry
|
||||
ON email_queue(next_retry_at)
|
||||
WHERE status = 'processing' AND retry_count < max_retries;
|
||||
|
||||
CREATE INDEX idx_email_queue_reference
|
||||
ON email_queue(reference_type, reference_id);
|
||||
|
||||
CREATE INDEX idx_email_queue_created_at
|
||||
ON email_queue(created_at DESC);
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- **Asynchronous processing**: Emails processed by background worker
|
||||
- **Retry mechanism**: Exponential backoff (1min, 2min, 4min, 8min, 16min, 32min...)
|
||||
- **Priority support**: High-priority emails processed first
|
||||
- **Scheduled sending**: Delay email delivery with `scheduled_for`
|
||||
- **Error tracking**: Detailed error logging and retry history
|
||||
- **Reference tracking**: Link emails to documents or other entities
|
||||
|
||||
**Automatic retry calculation**:
|
||||
```sql
|
||||
-- Function to calculate next retry time with exponential backoff
|
||||
CREATE OR REPLACE FUNCTION calculate_next_retry_time(retry_count INT)
|
||||
RETURNS TIMESTAMPTZ AS $$
|
||||
BEGIN
|
||||
-- Exponential backoff: 1min, 2min, 4min, 8min, 16min, 32min...
|
||||
RETURN now() + (interval '1 minute' * power(2, retry_count));
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
```
|
||||
|
||||
**Worker configuration**:
|
||||
- Batch size: 10 emails per batch
|
||||
- Poll interval: 5 seconds
|
||||
- Concurrent sends: 5 simultaneous emails
|
||||
- Old email cleanup: 7 days retention for sent/failed emails
|
||||
|
||||
## Migrations
|
||||
|
||||
### Migration Management
|
||||
|
||||
Migrations are in `/backend/migrations/` with format:
|
||||
|
||||
```
|
||||
XXXX_description.up.sql # "up" migration
|
||||
XXXX_description.down.sql # "down" rollback
|
||||
```
|
||||
|
||||
**Current files**:
|
||||
- `0001_init.up.sql` - Signatures table
|
||||
- `0002_expected_signers.up.sql` - Expected signers
|
||||
- `0003_reminder_logs.up.sql` - Reminder logs
|
||||
- `0004_add_name_to_expected_signers.up.sql` - Signer names
|
||||
- `0005_create_documents_table.up.sql` - Documents metadata
|
||||
- `0006_create_new_tables.up.sql` - Checksum verifications and email queue
|
||||
- `0007_oauth_sessions.up.sql` - OAuth sessions with refresh tokens
|
||||
|
||||
### Applying Migrations
|
||||
|
||||
**Via Docker Compose** (automatic):
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
# The ackify-migrate service applies migrations on startup
|
||||
```
|
||||
|
||||
**Manually**:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
go run ./cmd/migrate up
|
||||
```
|
||||
|
||||
**Rollback last migration**:
|
||||
|
||||
```bash
|
||||
go run ./cmd/migrate down
|
||||
```
|
||||
|
||||
### Custom Migrations
|
||||
|
||||
To create a new migration:
|
||||
|
||||
1. Create `XXXX_my_feature.up.sql`:
|
||||
```sql
|
||||
-- Migration up
|
||||
ALTER TABLE signatures ADD COLUMN new_field TEXT;
|
||||
```
|
||||
|
||||
2. Create `XXXX_my_feature.down.sql`:
|
||||
```sql
|
||||
-- Rollback
|
||||
ALTER TABLE signatures DROP COLUMN new_field;
|
||||
```
|
||||
|
||||
3. Apply:
|
||||
```bash
|
||||
go run ./cmd/migrate up
|
||||
```
|
||||
|
||||
## PostgreSQL Triggers
|
||||
|
||||
### Immutability of `created_at`
|
||||
|
||||
Trigger preventing `created_at` modification:
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION prevent_created_at_update()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.created_at <> OLD.created_at THEN
|
||||
RAISE EXCEPTION 'created_at cannot be modified';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER prevent_signatures_created_at_update
|
||||
BEFORE UPDATE ON signatures
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION prevent_created_at_update();
|
||||
```
|
||||
|
||||
**Guarantee**: No signature can be backdated.
|
||||
|
||||
### Auto-update of `updated_at`
|
||||
|
||||
For tables with `updated_at`:
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER update_documents_updated_at
|
||||
BEFORE UPDATE ON documents
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
```
|
||||
|
||||
## Useful Queries
|
||||
|
||||
### View document signatures
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
user_email,
|
||||
user_name,
|
||||
signed_at,
|
||||
payload_hash,
|
||||
signature
|
||||
FROM signatures
|
||||
WHERE doc_id = 'my_document'
|
||||
ORDER BY signed_at DESC;
|
||||
```
|
||||
|
||||
### Completion status
|
||||
|
||||
```sql
|
||||
WITH expected AS (
|
||||
SELECT COUNT(*) as total
|
||||
FROM expected_signers
|
||||
WHERE doc_id = 'my_document'
|
||||
),
|
||||
signed AS (
|
||||
SELECT COUNT(*) as count
|
||||
FROM signatures s
|
||||
INNER JOIN expected_signers e ON s.user_email = e.email AND s.doc_id = e.doc_id
|
||||
WHERE s.doc_id = 'my_document'
|
||||
)
|
||||
SELECT
|
||||
e.total as expected,
|
||||
s.count as signed,
|
||||
ROUND(100.0 * s.count / NULLIF(e.total, 0), 2) as completion_pct
|
||||
FROM expected e, signed s;
|
||||
```
|
||||
|
||||
### Missing signers
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
e.email,
|
||||
e.name,
|
||||
e.added_at
|
||||
FROM expected_signers e
|
||||
LEFT JOIN signatures s ON e.email = s.user_email AND e.doc_id = s.doc_id
|
||||
WHERE e.doc_id = 'my_document' AND s.id IS NULL
|
||||
ORDER BY e.added_at;
|
||||
```
|
||||
|
||||
### Unexpected signatures
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
s.user_email,
|
||||
s.signed_at
|
||||
FROM signatures s
|
||||
LEFT JOIN expected_signers e ON s.user_email = e.email AND s.doc_id = e.doc_id
|
||||
WHERE s.doc_id = 'my_document' AND e.id IS NULL
|
||||
ORDER BY s.signed_at DESC;
|
||||
```
|
||||
|
||||
### Email queue status
|
||||
|
||||
```sql
|
||||
-- View pending emails
|
||||
SELECT
|
||||
id,
|
||||
to_addresses,
|
||||
subject,
|
||||
status,
|
||||
priority,
|
||||
retry_count,
|
||||
scheduled_for,
|
||||
created_at
|
||||
FROM email_queue
|
||||
WHERE status IN ('pending', 'processing')
|
||||
ORDER BY priority DESC, scheduled_for ASC
|
||||
LIMIT 20;
|
||||
|
||||
-- Failed emails needing attention
|
||||
SELECT
|
||||
id,
|
||||
to_addresses,
|
||||
subject,
|
||||
retry_count,
|
||||
max_retries,
|
||||
last_error,
|
||||
next_retry_at
|
||||
FROM email_queue
|
||||
WHERE status = 'failed'
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- Email statistics by status
|
||||
SELECT
|
||||
status,
|
||||
COUNT(*) as count,
|
||||
MIN(created_at) as oldest,
|
||||
MAX(created_at) as newest
|
||||
FROM email_queue
|
||||
GROUP BY status
|
||||
ORDER BY status;
|
||||
```
|
||||
|
||||
## Backup & Restore
|
||||
|
||||
### PostgreSQL Backup
|
||||
|
||||
```bash
|
||||
# Full backup
|
||||
docker compose exec ackify-db pg_dump -U ackifyr ackify > backup.sql
|
||||
|
||||
# Compressed backup
|
||||
docker compose exec ackify-db pg_dump -U ackifyr ackify | gzip > backup.sql.gz
|
||||
```
|
||||
|
||||
### Restore
|
||||
|
||||
```bash
|
||||
# Restore from backup
|
||||
cat backup.sql | docker compose exec -T ackify-db psql -U ackifyr ackify
|
||||
|
||||
# Restore from compressed backup
|
||||
gunzip -c backup.sql.gz | docker compose exec -T ackify-db psql -U ackifyr ackify
|
||||
```
|
||||
|
||||
### Automated Backup
|
||||
|
||||
Example cron for daily backup:
|
||||
|
||||
```bash
|
||||
0 2 * * * docker compose -f /path/to/compose.yml exec -T ackify-db pg_dump -U ackifyr ackify | gzip > /backups/ackify-$(date +\%Y\%m\%d).sql.gz
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
### Indexes
|
||||
|
||||
Indexes are automatically created for:
|
||||
- `signatures(doc_id)` - Document queries
|
||||
- `signatures(user_sub)` - User queries
|
||||
- `expected_signers(doc_id)` - Completion tracking
|
||||
- `oauth_sessions(session_id)` - Session lookups
|
||||
|
||||
### Connection Pooling
|
||||
|
||||
The Go backend automatically handles connection pooling:
|
||||
- Max open connections: 25
|
||||
- Max idle connections: 5
|
||||
- Connection max lifetime: 5 minutes
|
||||
|
||||
### Vacuum & Analyze
|
||||
|
||||
PostgreSQL handles automatically via `autovacuum`. To force:
|
||||
|
||||
```sql
|
||||
VACUUM ANALYZE signatures;
|
||||
VACUUM ANALYZE documents;
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Table sizes
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
|
||||
```
|
||||
|
||||
### Statistics
|
||||
|
||||
```sql
|
||||
SELECT * FROM pg_stat_user_tables WHERE schemaname = 'public';
|
||||
```
|
||||
|
||||
### Active connections
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
datname,
|
||||
usename,
|
||||
application_name,
|
||||
client_addr,
|
||||
state,
|
||||
query
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = 'ackify';
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
### In Production
|
||||
|
||||
- ✅ Use SSL: `?sslmode=require` in DSN
|
||||
- ✅ Strong password for PostgreSQL
|
||||
- ✅ Restrict network connections
|
||||
- ✅ Encrypted backups
|
||||
- ✅ Regular secret rotation
|
||||
|
||||
### SSL Configuration
|
||||
|
||||
```bash
|
||||
# In .env
|
||||
ACKIFY_DB_DSN=postgres://user:pass@host:5432/ackify?sslmode=require
|
||||
```
|
||||
|
||||
### Audit Trail
|
||||
|
||||
All important operations are tracked:
|
||||
- `signatures.created_at` - Signature timestamp
|
||||
- `expected_signers.added_by` - Who added
|
||||
- `reminder_logs.sent_by` - Who sent reminder
|
||||
- `checksum_verifications.verified_by` - Who verified
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Blocked migrations
|
||||
|
||||
```bash
|
||||
# Check status
|
||||
docker compose logs ackify-migrate
|
||||
|
||||
# Force rollback
|
||||
docker compose exec ackify-ce /app/migrate down
|
||||
docker compose exec ackify-ce /app/migrate up
|
||||
```
|
||||
|
||||
### UNIQUE constraint violated
|
||||
|
||||
Error: `duplicate key value violates unique constraint`
|
||||
|
||||
**Cause**: User already signed this document.
|
||||
|
||||
**Solution**: This is normal behavior (one signature per user/doc).
|
||||
|
||||
### Connection refused
|
||||
|
||||
Verify PostgreSQL is started:
|
||||
|
||||
```bash
|
||||
docker compose ps ackify-db
|
||||
docker compose logs ackify-db
|
||||
```
|
||||
122
docs/en/deployment.md
Normal file
122
docs/en/deployment.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Deployment
|
||||
|
||||
Production deployment guide with Docker Compose.
|
||||
|
||||
## Production with Docker Compose
|
||||
|
||||
### Recommended Architecture
|
||||
|
||||
```
|
||||
[Internet] → [Reverse Proxy (Traefik/Nginx)] → [Ackify Container]
|
||||
↓
|
||||
[PostgreSQL Container]
|
||||
```
|
||||
|
||||
### Production compose.yml
|
||||
|
||||
See the `/compose.yml` file at the project root for complete configuration.
|
||||
|
||||
**Included services**:
|
||||
- `ackify-migrate` - PostgreSQL migrations (run once)
|
||||
- `ackify-ce` - Main application
|
||||
- `ackify-db` - PostgreSQL 16
|
||||
|
||||
### Production .env Configuration
|
||||
|
||||
```bash
|
||||
# Application
|
||||
APP_DNS=sign.company.com
|
||||
ACKIFY_BASE_URL=https://sign.company.com
|
||||
ACKIFY_ORGANISATION="ACME Corporation"
|
||||
ACKIFY_LOG_LEVEL=info
|
||||
|
||||
# Database (strong password)
|
||||
POSTGRES_USER=ackifyr
|
||||
POSTGRES_PASSWORD=$(openssl rand -base64 32)
|
||||
POSTGRES_DB=ackify
|
||||
|
||||
# OAuth2
|
||||
ACKIFY_OAUTH_PROVIDER=google
|
||||
ACKIFY_OAUTH_CLIENT_ID=your_client_id
|
||||
ACKIFY_OAUTH_CLIENT_SECRET=your_client_secret
|
||||
ACKIFY_OAUTH_ALLOWED_DOMAIN=@company.com
|
||||
|
||||
# Security (generate with openssl)
|
||||
ACKIFY_OAUTH_COOKIE_SECRET=$(openssl rand -base64 64)
|
||||
ACKIFY_ED25519_PRIVATE_KEY=$(openssl rand -base64 64)
|
||||
|
||||
# Administration
|
||||
ACKIFY_ADMIN_EMAILS=admin@company.com,cto@company.com
|
||||
```
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
### Traefik
|
||||
|
||||
Add labels in `compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
ackify-ce:
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.ackify.rule=Host(`sign.company.com`)"
|
||||
- "traefik.http.routers.ackify.entrypoints=websecure"
|
||||
- "traefik.http.routers.ackify.tls.certresolver=letsencrypt"
|
||||
```
|
||||
|
||||
### Nginx
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name sign.company.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/sign.company.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/sign.company.com/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- ✅ HTTPS with valid certificate
|
||||
- ✅ Strong secrets (64+ bytes)
|
||||
- ✅ PostgreSQL SSL in production
|
||||
- ✅ Restricted OAuth domain
|
||||
- ✅ Logs in info mode
|
||||
- ✅ Automatic backup
|
||||
- ✅ Active monitoring
|
||||
|
||||
## Backup
|
||||
|
||||
```bash
|
||||
# Daily PostgreSQL backup
|
||||
docker compose exec -T ackify-db pg_dump -U ackifyr ackify | gzip > backup-$(date +%Y%m%d).sql.gz
|
||||
|
||||
# Restore
|
||||
gunzip -c backup.sql.gz | docker compose exec -T ackify-db psql -U ackifyr ackify
|
||||
```
|
||||
|
||||
## Update
|
||||
|
||||
```bash
|
||||
# Pull new image
|
||||
docker compose pull ackify-ce
|
||||
|
||||
# Restart
|
||||
docker compose up -d
|
||||
|
||||
# Verify
|
||||
docker compose logs -f ackify-ce
|
||||
curl https://sign.company.com/api/v1/health
|
||||
```
|
||||
|
||||
See [Getting Started](getting-started.md) for more details.
|
||||
513
docs/en/development.md
Normal file
513
docs/en/development.md
Normal file
@@ -0,0 +1,513 @@
|
||||
# Development
|
||||
|
||||
Guide for contributing and developing on Ackify.
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Go 1.24.5+**
|
||||
- **Node.js 22+** and npm
|
||||
- **PostgreSQL 16+**
|
||||
- **Docker & Docker Compose**
|
||||
- Git
|
||||
|
||||
### Clone & Setup
|
||||
|
||||
```bash
|
||||
# Clone
|
||||
git clone https://github.com/btouchard/ackify-ce.git
|
||||
cd ackify-ce
|
||||
|
||||
# Copy .env
|
||||
cp .env.example .env
|
||||
|
||||
# Edit .env with your OAuth2 credentials
|
||||
nano .env
|
||||
```
|
||||
|
||||
## Backend Development
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
go mod download
|
||||
go build ./cmd/community
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
# Start PostgreSQL with Docker
|
||||
docker compose up -d ackify-db
|
||||
|
||||
# Apply migrations
|
||||
go run ./cmd/migrate up
|
||||
|
||||
# Launch app
|
||||
./community
|
||||
```
|
||||
|
||||
API accessible at `http://localhost:8080`.
|
||||
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
go test -v -short ./...
|
||||
|
||||
# Tests with coverage
|
||||
go test -coverprofile=coverage.out ./internal/... ./pkg/...
|
||||
|
||||
# View coverage
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
# Integration tests (PostgreSQL required)
|
||||
docker compose -f ../compose.test.yml up -d
|
||||
INTEGRATION_TESTS=1 go test -tags=integration -v ./internal/infrastructure/database/
|
||||
docker compose -f ../compose.test.yml down
|
||||
```
|
||||
|
||||
### Linting
|
||||
|
||||
```bash
|
||||
# Format
|
||||
go fmt ./...
|
||||
|
||||
# Vet
|
||||
go vet ./...
|
||||
|
||||
# Staticcheck (optional)
|
||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
staticcheck ./...
|
||||
```
|
||||
|
||||
## Frontend Development
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
cd webapp
|
||||
npm install
|
||||
```
|
||||
|
||||
### Dev Server
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Frontend accessible at `http://localhost:5173` with Hot Module Replacement.
|
||||
|
||||
### Production Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
# Output: webapp/dist/
|
||||
```
|
||||
|
||||
### Type Checking
|
||||
|
||||
```bash
|
||||
npm run type-check
|
||||
```
|
||||
|
||||
### i18n Validation
|
||||
|
||||
```bash
|
||||
npm run lint:i18n
|
||||
```
|
||||
|
||||
Verifies all translations are complete.
|
||||
|
||||
## Docker Development
|
||||
|
||||
### Local Build
|
||||
|
||||
```bash
|
||||
# Build complete image (frontend + backend)
|
||||
docker compose -f compose.local.yml up -d --build
|
||||
|
||||
# Logs
|
||||
docker compose -f compose.local.yml logs -f ackify-ce
|
||||
|
||||
# Rebuild after changes
|
||||
docker compose -f compose.local.yml up -d --force-recreate ackify-ce --build
|
||||
```
|
||||
|
||||
### Debug
|
||||
|
||||
```bash
|
||||
# Shell in container
|
||||
docker compose exec ackify-ce sh
|
||||
|
||||
# PostgreSQL shell
|
||||
docker compose exec ackify-db psql -U ackifyr -d ackify
|
||||
```
|
||||
|
||||
## Code Structure
|
||||
|
||||
### Backend
|
||||
|
||||
```
|
||||
backend/
|
||||
├── cmd/
|
||||
│ ├── community/ # main.go + dependency injection
|
||||
│ └── migrate/ # Migration tool
|
||||
├── internal/
|
||||
│ ├── domain/models/ # Entities (User, Signature, Document)
|
||||
│ ├── application/services/ # Business logic
|
||||
│ ├── infrastructure/
|
||||
│ │ ├── auth/ # OAuth2
|
||||
│ │ ├── database/ # Repositories
|
||||
│ │ ├── email/ # SMTP
|
||||
│ │ └── config/ # Config
|
||||
│ └── presentation/api/ # HTTP handlers
|
||||
└── pkg/ # Utilities
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```
|
||||
webapp/src/
|
||||
├── components/ # Vue components
|
||||
├── pages/ # Pages (router)
|
||||
├── services/ # API client
|
||||
├── stores/ # Pinia stores
|
||||
├── router/ # Vue Router
|
||||
└── locales/ # Translations
|
||||
```
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### Go
|
||||
|
||||
**Naming**:
|
||||
- Packages: lowercase, singular (`user`, `signature`)
|
||||
- Interfaces: suffix `er` or descriptive (`SignatureRepository`, `EmailSender`)
|
||||
- Constructors: `New...()` or `...From...()`
|
||||
|
||||
**Example**:
|
||||
```go
|
||||
// Service
|
||||
type SignatureService struct {
|
||||
repo SignatureRepository
|
||||
crypto CryptoService
|
||||
}
|
||||
|
||||
func NewSignatureService(repo SignatureRepository, crypto CryptoService) *SignatureService {
|
||||
return &SignatureService{repo: repo, crypto: crypto}
|
||||
}
|
||||
|
||||
// Method
|
||||
func (s *SignatureService) CreateSignature(ctx context.Context, docID, userSub string) (*models.Signature, error) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Errors**:
|
||||
```go
|
||||
// Wrapping
|
||||
return nil, fmt.Errorf("failed to create signature: %w", err)
|
||||
|
||||
// Custom errors
|
||||
var ErrAlreadySigned = errors.New("user has already signed this document")
|
||||
```
|
||||
|
||||
### TypeScript
|
||||
|
||||
**Naming**:
|
||||
- Components: PascalCase (`DocumentCard.vue`)
|
||||
- Composables: camelCase with `use` prefix (`useAuth.ts`)
|
||||
- Stores: camelCase with `Store` suffix (`userStore.ts`)
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
// Composable
|
||||
export function useAuth() {
|
||||
const user = ref<User | null>(null)
|
||||
|
||||
async function login() {
|
||||
// ...
|
||||
}
|
||||
|
||||
return { user, login }
|
||||
}
|
||||
|
||||
// Store
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const currentUser = ref<User | null>(null)
|
||||
|
||||
async function fetchMe() {
|
||||
const { data } = await api.get('/users/me')
|
||||
currentUser.value = data
|
||||
}
|
||||
|
||||
return { currentUser, fetchMe }
|
||||
})
|
||||
```
|
||||
|
||||
## Adding a Feature
|
||||
|
||||
### 1. Planning
|
||||
|
||||
- Define required API endpoints
|
||||
- SQL schema if needed
|
||||
- User interface
|
||||
|
||||
### 2. Backend
|
||||
|
||||
```bash
|
||||
# 1. Create migration if needed
|
||||
touch backend/migrations/XXXX_my_feature.up.sql
|
||||
touch backend/migrations/XXXX_my_feature.down.sql
|
||||
|
||||
# 2. Create model
|
||||
# backend/internal/domain/models/my_model.go
|
||||
|
||||
# 3. Create repository interface
|
||||
# backend/internal/application/services/my_service.go
|
||||
|
||||
# 4. Implement repository
|
||||
# backend/internal/infrastructure/database/my_repository.go
|
||||
|
||||
# 5. Create API handler
|
||||
# backend/internal/presentation/api/myfeature/handler.go
|
||||
|
||||
# 6. Register routes
|
||||
# backend/internal/presentation/api/router.go
|
||||
```
|
||||
|
||||
### 3. Frontend
|
||||
|
||||
```bash
|
||||
# 1. Create API service
|
||||
# webapp/src/services/myFeatureService.ts
|
||||
|
||||
# 2. Create Pinia store
|
||||
# webapp/src/stores/myFeatureStore.ts
|
||||
|
||||
# 3. Create components
|
||||
# webapp/src/components/MyFeature.vue
|
||||
|
||||
# 4. Add translations
|
||||
# webapp/src/locales/{fr,en,es,de,it}.json
|
||||
|
||||
# 5. Add routes if needed
|
||||
# webapp/src/router/index.ts
|
||||
```
|
||||
|
||||
### 4. Tests
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
# backend/internal/presentation/api/myfeature/handler_test.go
|
||||
|
||||
# Test
|
||||
go test -v ./internal/presentation/api/myfeature/
|
||||
```
|
||||
|
||||
### 5. Documentation
|
||||
|
||||
Update:
|
||||
- `/api/openapi.yaml` - OpenAPI specification
|
||||
- `/docs/api.md` - API documentation
|
||||
- `/docs/features/my-feature.md` - User guide
|
||||
|
||||
## Debugging
|
||||
|
||||
### Backend
|
||||
|
||||
```go
|
||||
// Structured logs
|
||||
logger.Info("signature created",
|
||||
"doc_id", docID,
|
||||
"user_sub", userSub,
|
||||
"signature_id", sig.ID,
|
||||
)
|
||||
|
||||
// Debug via Delve (optional)
|
||||
dlv debug ./cmd/community
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```typescript
|
||||
// Vue DevTools (Chrome/Firefox extension)
|
||||
// Inspect: Components, Pinia stores, Router
|
||||
|
||||
// Console debug
|
||||
console.log('[DEBUG] User:', user.value)
|
||||
|
||||
// Breakpoints via browser
|
||||
debugger
|
||||
```
|
||||
|
||||
## SQL Migrations
|
||||
|
||||
### Create Migration
|
||||
|
||||
```sql
|
||||
-- XXXX_add_field.up.sql
|
||||
ALTER TABLE signatures ADD COLUMN new_field TEXT;
|
||||
|
||||
-- XXXX_add_field.down.sql
|
||||
ALTER TABLE signatures DROP COLUMN new_field;
|
||||
```
|
||||
|
||||
### Apply
|
||||
|
||||
```bash
|
||||
go run ./cmd/migrate up
|
||||
```
|
||||
|
||||
### Rollback
|
||||
|
||||
```bash
|
||||
go run ./cmd/migrate down
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
### Setup PostgreSQL Test
|
||||
|
||||
```bash
|
||||
docker compose -f compose.test.yml up -d
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
|
||||
```bash
|
||||
INTEGRATION_TESTS=1 go test -tags=integration -v ./internal/infrastructure/database/
|
||||
```
|
||||
|
||||
### Cleanup
|
||||
|
||||
```bash
|
||||
docker compose -f compose.test.yml down -v
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
Project uses `.github/workflows/ci.yml`:
|
||||
|
||||
**Jobs**:
|
||||
1. **Lint** - go fmt, go vet, eslint
|
||||
2. **Test Backend** - Unit + integration tests
|
||||
3. **Test Frontend** - Type checking + i18n validation
|
||||
4. **Coverage** - Upload to Codecov
|
||||
5. **Build** - Verify Docker image builds
|
||||
|
||||
### Pre-commit Hooks (optional)
|
||||
|
||||
```bash
|
||||
# Install pre-commit
|
||||
pip install pre-commit
|
||||
|
||||
# Setup hooks
|
||||
pre-commit install
|
||||
|
||||
# Run manually
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
## Contribution
|
||||
|
||||
### Git Workflow
|
||||
|
||||
```bash
|
||||
# 1. Create branch
|
||||
git checkout -b feature/my-feature
|
||||
|
||||
# 2. Develop + commit
|
||||
git add .
|
||||
git commit -m "feat: add my feature"
|
||||
|
||||
# 3. Push
|
||||
git push origin feature/my-feature
|
||||
|
||||
# 4. Create Pull Request on GitHub
|
||||
```
|
||||
|
||||
### Commit Messages
|
||||
|
||||
Format: `type: description`
|
||||
|
||||
**Types**:
|
||||
- `feat` - New feature
|
||||
- `fix` - Bug fix
|
||||
- `docs` - Documentation
|
||||
- `refactor` - Refactoring
|
||||
- `test` - Tests
|
||||
- `chore` - Maintenance
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
feat: add checksum verification feature
|
||||
fix: resolve OAuth callback redirect loop
|
||||
docs: update API documentation for signatures
|
||||
refactor: simplify signature service logic
|
||||
test: add integration tests for expected signers
|
||||
```
|
||||
|
||||
### Code Review
|
||||
|
||||
**Checklist**:
|
||||
- ✅ Tests pass (CI green)
|
||||
- ✅ Code formatted (`go fmt`, `eslint`)
|
||||
- ✅ No committed secrets
|
||||
- ✅ Documentation updated
|
||||
- ✅ Complete translations (i18n)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Backend won't start
|
||||
|
||||
```bash
|
||||
# Check PostgreSQL
|
||||
docker compose ps ackify-db
|
||||
docker compose logs ackify-db
|
||||
|
||||
# Check environment variables
|
||||
cat .env
|
||||
|
||||
# Detailed logs
|
||||
ACKIFY_LOG_LEVEL=debug ./community
|
||||
```
|
||||
|
||||
### Frontend build fails
|
||||
|
||||
```bash
|
||||
# Clean and reinstall
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
|
||||
# Check Node version
|
||||
node --version # Should be 22+
|
||||
```
|
||||
|
||||
### Tests fail
|
||||
|
||||
```bash
|
||||
# Backend - check PostgreSQL test
|
||||
docker compose -f compose.test.yml ps
|
||||
|
||||
# Frontend - check types
|
||||
npm run type-check
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Go Documentation](https://go.dev/doc/)
|
||||
- [Vue 3 Guide](https://vuejs.org/guide/)
|
||||
- [PostgreSQL Docs](https://www.postgresql.org/docs/)
|
||||
- [Chi Router](https://github.com/go-chi/chi)
|
||||
- [Pinia](https://pinia.vuejs.org/)
|
||||
|
||||
## Support
|
||||
|
||||
- [GitHub Issues](https://github.com/btouchard/ackify-ce/issues)
|
||||
- [GitHub Discussions](https://github.com/btouchard/ackify-ce/discussions)
|
||||
684
docs/fr/admin-guide.md
Normal file
684
docs/fr/admin-guide.md
Normal file
@@ -0,0 +1,684 @@
|
||||
# Guide Administrateur
|
||||
|
||||
Guide complet pour les administrateurs utilisant Ackify pour gérer les documents, les signataires attendus et les rappels email.
|
||||
|
||||
## Table des Matières
|
||||
|
||||
- [Obtenir l'accès Admin](#obtenir-laccès-admin)
|
||||
- [Dashboard Admin](#dashboard-admin)
|
||||
- [Gestion des Documents](#gestion-des-documents)
|
||||
- [Signataires Attendus](#signataires-attendus)
|
||||
- [Rappels Email](#rappels-email)
|
||||
- [Monitoring & Statistiques](#monitoring--statistiques)
|
||||
- [Bonnes Pratiques](#bonnes-pratiques)
|
||||
- [Dépannage](#dépannage)
|
||||
|
||||
---
|
||||
|
||||
## Obtenir l'accès Admin
|
||||
|
||||
### Prérequis
|
||||
|
||||
Pour accéder aux fonctionnalités admin, votre email doit être configuré dans la variable d'environnement `ACKIFY_ADMIN_EMAILS`.
|
||||
|
||||
```bash
|
||||
# Dans le fichier .env
|
||||
ACKIFY_ADMIN_EMAILS=admin@company.com,manager@company.com
|
||||
```
|
||||
|
||||
**Après ajout de votre email:**
|
||||
1. Redémarrer Ackify: `docker compose restart ackify-ce`
|
||||
2. Se déconnecter et se reconnecter
|
||||
3. Vous devriez maintenant voir le lien "Admin" dans la navigation
|
||||
|
||||
### Vérifier l'accès Admin
|
||||
|
||||
Visitez `/admin` - si vous voyez le dashboard admin, vous avez l'accès admin.
|
||||
|
||||
---
|
||||
|
||||
## Dashboard Admin
|
||||
|
||||
**URL**: `/admin`
|
||||
|
||||
Le dashboard admin fournit:
|
||||
- **Total Documents**: Nombre de documents dans le système
|
||||
- **Lecteurs Attendus**: Nombre total de signataires attendus sur tous les documents
|
||||
- **Documents Actifs**: Documents non supprimés
|
||||
- **Liste Documents**: Liste paginée (20 par page) avec recherche
|
||||
|
||||
### Fonctionnalités du Dashboard
|
||||
|
||||
#### Statistiques Rapides
|
||||
Trois cartes KPI en haut:
|
||||
- Nombre total de documents
|
||||
- Total des lecteurs/signataires attendus
|
||||
- Documents actifs (non supprimés)
|
||||
|
||||
#### Recherche de Documents
|
||||
- Recherche par titre, ID document ou URL
|
||||
- Filtrage en temps réel
|
||||
|
||||
#### Liste des Documents
|
||||
**Vue desktop** - Tableau avec colonnes:
|
||||
- ID Document
|
||||
- Titre
|
||||
- URL
|
||||
- Date de création
|
||||
- Créateur
|
||||
- Actions (Voir détails)
|
||||
|
||||
**Vue mobile** - Mise en page carte avec:
|
||||
- ID document et titre
|
||||
- Info création
|
||||
- Tap pour voir détails
|
||||
|
||||
#### Pagination
|
||||
- 20 documents par page
|
||||
- Boutons Précédent/Suivant
|
||||
- Indicateur de page actuelle
|
||||
|
||||
---
|
||||
|
||||
## Gestion des Documents
|
||||
|
||||
### Créer un Document
|
||||
|
||||
**Depuis le Dashboard Admin:**
|
||||
1. Cliquer sur le bouton "Créer Nouveau Document"
|
||||
2. Remplir le formulaire:
|
||||
- **Référence** (requis): URL, chemin fichier ou ID personnalisé
|
||||
- **Titre** (optionnel): Auto-généré depuis l'URL si vide
|
||||
- **Description** (optionnel): Contexte additionnel
|
||||
3. Cliquer sur "Créer Document"
|
||||
|
||||
**Fonctionnalités Automatiques:**
|
||||
- **Génération ID Unique**: doc_id base36 résistant aux collisions
|
||||
- **Extraction Titre**: Auto-extrait de l'URL si non fourni
|
||||
- **Calcul Checksum**: Pour URLs distantes (si admin et fichier < 10MB)
|
||||
|
||||
**Exemple:**
|
||||
```
|
||||
Référence: https://docs.company.com/politique-2025.pdf
|
||||
Titre: Politique de Sécurité 2025 (auto-extrait ou manuel)
|
||||
Description: Politique annuelle de conformité sécurité
|
||||
```
|
||||
|
||||
**Résultat:**
|
||||
- doc_id: `k7m2n4p8` (auto-généré)
|
||||
- Checksum: SHA-256 auto-calculé (si URL accessible)
|
||||
|
||||
### Voir les Détails d'un Document
|
||||
|
||||
**URL**: `/admin/docs/{docId}`
|
||||
|
||||
Fournit des informations complètes sur le document:
|
||||
|
||||
#### 1. **Section Métadonnées**
|
||||
Éditer les informations du document:
|
||||
- Titre
|
||||
- URL
|
||||
- Description
|
||||
- Checksum (SHA-256, SHA-512 ou MD5)
|
||||
- Algorithme Checksum
|
||||
|
||||
**Pour éditer:**
|
||||
1. Cliquer sur "Éditer Métadonnées"
|
||||
2. Modifier les champs
|
||||
3. Cliquer sur "Sauvegarder"
|
||||
4. Modal de confirmation pour changements critiques (checksum, algorithme)
|
||||
|
||||
#### 2. **Panneau Statistiques**
|
||||
Suivi des signatures en temps réel:
|
||||
- **Attendus**: Nombre de signataires attendus
|
||||
- **Signés**: Nombre ayant signé
|
||||
- **En Attente**: Pas encore signé
|
||||
- **Complétion**: Pourcentage complété
|
||||
|
||||
#### 3. **Section Signataires Attendus**
|
||||
Liste tous les signataires attendus avec statut:
|
||||
- **Email**: Adresse email du signataire
|
||||
- **Statut**: ✅ Signé ou ⏳ En attente
|
||||
- **Ajouté**: Date d'ajout à la liste attendue
|
||||
- **Jours Depuis Ajout**: Suivi du temps
|
||||
- **Dernier Rappel**: Quand le dernier rappel a été envoyé
|
||||
- **Nb Rappels**: Total rappels envoyés
|
||||
- **Actions**: Bouton retirer signataire
|
||||
|
||||
**Code couleur:**
|
||||
- Fond vert: Signataire a signé
|
||||
- Fond par défaut: Signature en attente
|
||||
|
||||
#### 4. **Signatures Inattendues**
|
||||
Affiche les utilisateurs ayant signé mais pas sur la liste attendue:
|
||||
- Email utilisateur
|
||||
- Date de signature
|
||||
- Indique participation organique/inattendue
|
||||
|
||||
#### 5. **Actions**
|
||||
- **Envoyer Rappels**: Emailer les signataires en attente
|
||||
- **Partager Lien**: Générer et copier lien de signature
|
||||
- **Supprimer Document**: Suppression douce (préserve historique signatures)
|
||||
|
||||
### Mettre à Jour les Métadonnées du Document
|
||||
|
||||
**Champs importants:**
|
||||
|
||||
**Titre & Description:**
|
||||
- Peuvent être changés librement
|
||||
- Pas de confirmation requise
|
||||
|
||||
**URL:**
|
||||
- Met à jour où le document est localisé
|
||||
- Modal de confirmation affiché
|
||||
|
||||
**Checksum & Algorithme:**
|
||||
- Critique pour vérification d'intégrité
|
||||
- Modal de confirmation avertit de l'impact
|
||||
- Changer uniquement si version du document a changé
|
||||
|
||||
**Workflow:**
|
||||
1. Cliquer sur "Éditer Métadonnées"
|
||||
2. Modifier les champs désirés
|
||||
3. Cliquer sur "Sauvegarder"
|
||||
4. Si checksum/algorithme changé, confirmer dans modal
|
||||
5. Notification de succès affichée
|
||||
|
||||
### Supprimer un Document
|
||||
|
||||
**Comportement Suppression Douce:**
|
||||
- Document marqué comme supprimé (timestamp `deleted_at` défini)
|
||||
- Historique signatures préservé
|
||||
- Document n'apparaît plus dans listes publiques
|
||||
- Admin peut toujours voir via URL directe
|
||||
- Signatures CASCADE update (marquées avec `doc_deleted_at`)
|
||||
|
||||
**Pour supprimer:**
|
||||
1. Aller sur page détail document (`/admin/docs/{docId}`)
|
||||
2. Cliquer sur bouton "Supprimer Document"
|
||||
3. Confirmer suppression dans modal
|
||||
4. Document déplacé en état supprimé
|
||||
|
||||
**Note**: Il n'y a pas de "restauration" - c'est une suppression douce permanente.
|
||||
|
||||
---
|
||||
|
||||
## Signataires Attendus
|
||||
|
||||
Les signataires attendus sont les utilisateurs que vous souhaitez suivre pour la complétion du document.
|
||||
|
||||
### Ajouter des Signataires Attendus
|
||||
|
||||
**Depuis la page détail document:**
|
||||
1. Défiler jusqu'à la section "Signataires Attendus"
|
||||
2. Cliquer sur bouton "Ajouter Signataire Attendu"
|
||||
3. Entrer adresse(s) email:
|
||||
- Simple: `alice@company.com`
|
||||
- Multiple: Séparées par virgules `alice@company.com,bob@company.com`
|
||||
4. Optionnellement ajouter des notes
|
||||
5. Cliquer sur "Ajouter"
|
||||
|
||||
**Endpoint API:**
|
||||
```http
|
||||
POST /api/v1/admin/documents/{docId}/signers
|
||||
Content-Type: application/json
|
||||
X-CSRF-Token: {token}
|
||||
|
||||
{
|
||||
"emails": ["alice@company.com", "bob@company.com"],
|
||||
"notes": "Membres du conseil - Q1 2025"
|
||||
}
|
||||
```
|
||||
|
||||
**Contraintes:**
|
||||
- Email doit avoir format valide
|
||||
- Contrainte UNIQUE: Impossible d'ajouter même email deux fois au même document
|
||||
- Ajouté par admin utilisateur actuel (suivi dans `added_by`)
|
||||
|
||||
### Retirer des Signataires Attendus
|
||||
|
||||
**Depuis la page détail document:**
|
||||
1. Trouver signataire dans liste Signataires Attendus
|
||||
2. Cliquer sur bouton "Retirer" à côté de leur email
|
||||
3. Confirmer le retrait
|
||||
|
||||
**Endpoint API:**
|
||||
```http
|
||||
DELETE /api/v1/admin/documents/{docId}/signers/{email}
|
||||
X-CSRF-Token: {token}
|
||||
```
|
||||
|
||||
**Effet:**
|
||||
- Signataire retiré de la liste attendue
|
||||
- NE supprime PAS leur signature s'ils ont déjà signé
|
||||
- Historique des rappels préservé dans `reminder_logs`
|
||||
|
||||
### Suivre le Statut de Complétion
|
||||
|
||||
**API Statut Document:**
|
||||
```http
|
||||
GET /api/v1/admin/documents/{docId}/status
|
||||
```
|
||||
|
||||
**Réponse:**
|
||||
```json
|
||||
{
|
||||
"docId": "abc123",
|
||||
"expectedCount": 10,
|
||||
"signedCount": 7,
|
||||
"pendingCount": 3,
|
||||
"completionPercentage": 70.0
|
||||
}
|
||||
```
|
||||
|
||||
**Indicateurs visuels:**
|
||||
- Barre de progression montrant pourcentage complétion
|
||||
- Statut code couleur: Vert (signé), Orange (en attente)
|
||||
- Jours depuis ajout (aide identifier signataires lents)
|
||||
|
||||
---
|
||||
|
||||
## Rappels Email
|
||||
|
||||
Les rappels email sont envoyés de manière asynchrone via le système `email_queue`.
|
||||
|
||||
### Envoyer des Rappels
|
||||
|
||||
**Depuis la page détail document:**
|
||||
1. Cliquer sur bouton "Envoyer Rappels"
|
||||
2. Modal s'ouvre avec options:
|
||||
- **Envoyer à**: Tous en attente OU emails spécifiques
|
||||
- **URL Document**: Pré-rempli, peut personnaliser
|
||||
- **Langue**: en, fr, es, de, it
|
||||
3. Cliquer sur "Envoyer Rappels"
|
||||
4. Confirmation: "X rappels mis en file pour envoi"
|
||||
|
||||
**Endpoint API:**
|
||||
```http
|
||||
POST /api/v1/admin/documents/{docId}/reminders
|
||||
Content-Type: application/json
|
||||
X-CSRF-Token: {token}
|
||||
|
||||
{
|
||||
"emails": ["alice@company.com"], // Optionnel: emails spécifiques
|
||||
"docURL": "https://docs.company.com/politique.pdf",
|
||||
"locale": "fr"
|
||||
}
|
||||
```
|
||||
|
||||
**Comportement:**
|
||||
- Envoie à TOUS les signataires en attente si `emails` non spécifié
|
||||
- Envoie aux `emails` spécifiques si fournis (même si déjà signé)
|
||||
- Emails mis en file dans table `email_queue`
|
||||
- Worker background traite la file
|
||||
- Retry en cas d'échec (3 tentatives, exponential backoff)
|
||||
|
||||
### Templates Email
|
||||
|
||||
**Emplacement**: `backend/templates/emails/`
|
||||
|
||||
**Templates disponibles:**
|
||||
- `reminder.html` - Version HTML
|
||||
- `reminder.txt` - Version texte simple
|
||||
|
||||
**Variables disponibles dans les templates:**
|
||||
- `{{.DocTitle}}` - Titre document
|
||||
- `{{.DocURL}}` - URL document
|
||||
- `{{.RecipientEmail}}` - Email destinataire
|
||||
- `{{.SenderName}}` - Admin ayant envoyé rappel
|
||||
- `{{.OrganisationName}}` - Depuis ACKIFY_ORGANISATION
|
||||
|
||||
**Locales**: en, fr, es, de, it
|
||||
- Répertoire template: `templates/emails/{locale}/`
|
||||
- Fallback vers locale par défaut si traduction manquante
|
||||
|
||||
### Historique des Rappels
|
||||
|
||||
**Voir log des rappels:**
|
||||
```http
|
||||
GET /api/v1/admin/documents/{docId}/reminders
|
||||
```
|
||||
|
||||
**Réponse:**
|
||||
```json
|
||||
{
|
||||
"reminders": [
|
||||
{
|
||||
"id": 123,
|
||||
"docId": "abc123",
|
||||
"recipientEmail": "alice@company.com",
|
||||
"sentAt": "2025-01-15T10:30:00Z",
|
||||
"sentBy": "admin@company.com",
|
||||
"templateUsed": "reminder",
|
||||
"status": "sent",
|
||||
"errorMessage": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Valeurs de statut:**
|
||||
- `queued` - Dans email_queue, pas encore traité
|
||||
- `sent` - Livré avec succès
|
||||
- `failed` - Échec de livraison (voir errorMessage)
|
||||
- `bounced` - Email retourné
|
||||
|
||||
**Suivi:**
|
||||
- Date dernier rappel envoyé affiché par signataire
|
||||
- Nombre rappels affiché par signataire
|
||||
- Aide éviter sur-envoi
|
||||
|
||||
### Monitoring de la File Email
|
||||
|
||||
**Vérifier statut de la file (PostgreSQL):**
|
||||
```sql
|
||||
-- Emails en attente
|
||||
SELECT id, to_addresses, subject, status, scheduled_for
|
||||
FROM email_queue
|
||||
WHERE status IN ('pending', 'processing')
|
||||
ORDER BY priority DESC, scheduled_for ASC;
|
||||
|
||||
-- Emails échoués
|
||||
SELECT id, to_addresses, last_error, retry_count
|
||||
FROM email_queue
|
||||
WHERE status = 'failed';
|
||||
```
|
||||
|
||||
**Configuration du worker:**
|
||||
- Taille lot: 10 emails
|
||||
- Intervalle polling: 5 secondes
|
||||
- Max retries: 3
|
||||
- Cleanup: Rétention 7 jours
|
||||
|
||||
---
|
||||
|
||||
## Monitoring & Statistiques
|
||||
|
||||
### Statistiques Niveau Document
|
||||
|
||||
**Suivi complétion:**
|
||||
- Nombre Attendus vs Signés
|
||||
- Liste signataires en attente
|
||||
- Pourcentage complétion
|
||||
- Temps moyen pour signer
|
||||
|
||||
**Efficacité des rappels:**
|
||||
- Nombre rappels envoyés
|
||||
- Taux succès/échec
|
||||
- Temps entre rappel et signature
|
||||
|
||||
### Métriques Système Global
|
||||
|
||||
**Requêtes PostgreSQL:**
|
||||
|
||||
```sql
|
||||
-- Total documents
|
||||
SELECT COUNT(*) FROM documents WHERE deleted_at IS NULL;
|
||||
|
||||
-- Total signatures
|
||||
SELECT COUNT(*) FROM signatures;
|
||||
|
||||
-- Documents par statut complétion
|
||||
SELECT
|
||||
CASE
|
||||
WHEN signed_count = expected_count THEN '100%'
|
||||
WHEN signed_count >= expected_count * 0.75 THEN '75-99%'
|
||||
WHEN signed_count >= expected_count * 0.50 THEN '50-74%'
|
||||
ELSE '<50%'
|
||||
END as completion_bracket,
|
||||
COUNT(*) as doc_count
|
||||
FROM (
|
||||
SELECT
|
||||
d.doc_id,
|
||||
COUNT(DISTINCT es.email) as expected_count,
|
||||
COUNT(DISTINCT s.user_email) as signed_count
|
||||
FROM documents d
|
||||
LEFT JOIN expected_signers es ON d.doc_id = es.doc_id
|
||||
LEFT JOIN signatures s ON d.doc_id = s.doc_id AND s.user_email = es.email
|
||||
WHERE d.deleted_at IS NULL
|
||||
GROUP BY d.doc_id
|
||||
) stats
|
||||
GROUP BY completion_bracket;
|
||||
|
||||
-- Statistiques file email
|
||||
SELECT status, COUNT(*), MIN(created_at), MAX(created_at)
|
||||
FROM email_queue
|
||||
GROUP BY status;
|
||||
```
|
||||
|
||||
### Exporter les Données
|
||||
|
||||
**Signatures pour un document:**
|
||||
```sql
|
||||
COPY (
|
||||
SELECT s.user_email, s.user_name, s.signed_at, s.payload_hash
|
||||
FROM signatures s
|
||||
WHERE s.doc_id = 'votre_doc_id'
|
||||
ORDER BY s.signed_at
|
||||
) TO '/tmp/signatures_export.csv' WITH CSV HEADER;
|
||||
```
|
||||
|
||||
**Statut signataires attendus:**
|
||||
```sql
|
||||
COPY (
|
||||
SELECT
|
||||
es.email,
|
||||
CASE WHEN s.id IS NOT NULL THEN 'Signé' ELSE 'En attente' END as status,
|
||||
es.added_at,
|
||||
s.signed_at
|
||||
FROM expected_signers es
|
||||
LEFT JOIN signatures s ON es.doc_id = s.doc_id AND es.email = s.user_email
|
||||
WHERE es.doc_id = 'votre_doc_id'
|
||||
) TO '/tmp/expected_signers_export.csv' WITH CSV HEADER;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bonnes Pratiques
|
||||
|
||||
### 1. Création de Documents
|
||||
|
||||
✅ **À Faire:**
|
||||
- Utiliser titres descriptifs
|
||||
- Ajouter descriptions claires
|
||||
- Inclure URL document pour accès facile
|
||||
- Stocker checksum pour vérification intégrité
|
||||
- Créer liste signataires attendus avant partage
|
||||
|
||||
❌ **À Éviter:**
|
||||
- Utiliser titres génériques comme "Document 1"
|
||||
- Laisser URL vide si document accessible en ligne
|
||||
- Changer checksums sauf si document réellement changé
|
||||
|
||||
### 2. Gestion Signataires Attendus
|
||||
|
||||
✅ **À Faire:**
|
||||
- Ajouter signataires attendus avant d'envoyer lien document
|
||||
- Utiliser notes claires pour expliquer pourquoi signataires attendus
|
||||
- Réviser signataires en attente régulièrement
|
||||
- Retirer signataires qui ne sont plus pertinents
|
||||
|
||||
❌ **À Éviter:**
|
||||
- Ajouter centaines de signataires d'un coup (utiliser lots)
|
||||
- Envoyer rappels trop fréquemment (max une fois par semaine)
|
||||
- Retirer signataires ayant déjà signé (préserver historique)
|
||||
|
||||
### 3. Rappels Email
|
||||
|
||||
✅ **À Faire:**
|
||||
- Attendre 3-5 jours avant premier rappel
|
||||
- Envoyer dans langue préférée du destinataire
|
||||
- Inclure titre et URL document clairs
|
||||
- Suivre historique rappels pour éviter spam
|
||||
- Envoyer rappels pendant heures bureau
|
||||
|
||||
❌ **À Éviter:**
|
||||
- Envoyer rappels quotidiens (cause fatigue)
|
||||
- Envoyer sans vérifier si déjà signé
|
||||
- Utiliser sujets génériques (personnaliser avec titre doc)
|
||||
- Envoyer hors heures bureau
|
||||
|
||||
### 4. Intégrité des Données
|
||||
|
||||
✅ **À Faire:**
|
||||
- Sauvegarder régulièrement base PostgreSQL
|
||||
- Vérifier checksums correspondent documents réels
|
||||
- Monitorer file email pour échecs
|
||||
- Réviser signatures inattendues (peut indiquer intérêt plus large)
|
||||
- Exporter données signatures importantes
|
||||
|
||||
❌ **À Éviter:**
|
||||
- Supprimer documents avec signatures actives
|
||||
- Modifier timestamps manuellement en base
|
||||
- Ignorer échecs livraison email
|
||||
- Changer checksums sans mettre à jour document
|
||||
|
||||
### 5. Sécurité
|
||||
|
||||
✅ **À Faire:**
|
||||
- Limiter accès admin aux utilisateurs de confiance uniquement
|
||||
- Utiliser HTTPS en production (`ACKIFY_BASE_URL=https://...`)
|
||||
- Tourner `ACKIFY_OAUTH_COOKIE_SECRET` périodiquement
|
||||
- Monitorer actions admin via logs application
|
||||
- Utiliser restrictions domaine OAuth autorisé
|
||||
|
||||
❌ **À Éviter:**
|
||||
- Partager identifiants admin
|
||||
- Fonctionner sans HTTPS en production
|
||||
- Désactiver protection CSRF
|
||||
- Ignorer échecs authentification dans logs
|
||||
|
||||
---
|
||||
|
||||
## Dépannage
|
||||
|
||||
### Problèmes Courants
|
||||
|
||||
#### 1. Lien Admin Non Visible
|
||||
|
||||
**Problème**: Ne peut pas voir lien "Admin" dans navigation
|
||||
|
||||
**Solutions:**
|
||||
- Vérifier email dans variable `ACKIFY_ADMIN_EMAILS`
|
||||
- Redémarrer Ackify: `docker compose restart ackify-ce`
|
||||
- Se déconnecter et se reconnecter
|
||||
- Vérifier logs: `docker compose logs ackify-ce | grep admin`
|
||||
|
||||
#### 2. Emails Non Envoyés
|
||||
|
||||
**Problème**: Rappels mis en file mais pas livrés
|
||||
|
||||
**Diagnostic:**
|
||||
```sql
|
||||
SELECT * FROM email_queue WHERE status = 'failed' ORDER BY created_at DESC LIMIT 10;
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
- Vérifier configuration SMTP (`ACKIFY_MAIL_HOST`, `ACKIFY_MAIL_USERNAME`, etc.)
|
||||
- Vérifier identifiants SMTP corrects
|
||||
- Vérifier logs worker email: `docker compose logs ackify-ce | grep email`
|
||||
- S'assurer `ACKIFY_MAIL_FROM` est adresse expéditeur valide
|
||||
- Tester connexion SMTP manuellement
|
||||
|
||||
#### 3. Erreur Signataire Dupliqué
|
||||
|
||||
**Problème**: "Email existe déjà comme signataire attendu"
|
||||
|
||||
**Cause**: Contrainte UNIQUE sur (doc_id, email)
|
||||
|
||||
**Solution**: Comportement attendu - chaque email ne peut être ajouté qu'une fois par document
|
||||
|
||||
#### 4. Checksum Non Correspondant
|
||||
|
||||
**Problème**: Utilisateurs rapportent checksum ne correspond pas
|
||||
|
||||
**Solutions:**
|
||||
- Vérifier checksum stocké correspond document réel
|
||||
- Vérifier algorithme utilisé (SHA-256, SHA-512, MD5)
|
||||
- Recalculer checksum et mettre à jour via Éditer Métadonnées
|
||||
- S'assurer utilisateurs téléchargent version correcte
|
||||
|
||||
#### 5. Document N'apparaît Pas
|
||||
|
||||
**Problème**: Document créé n'apparaît pas dans liste
|
||||
|
||||
**Solutions:**
|
||||
- Vérifier si document supprimé en douce (`deleted_at IS NOT NULL`)
|
||||
- Vérifier création réussie (vérifier réponse/logs)
|
||||
- Vider cache navigateur
|
||||
- Vérifier base: `SELECT * FROM documents WHERE doc_id = 'votre_id';`
|
||||
|
||||
#### 6. Signature Déjà Existe
|
||||
|
||||
**Problème**: Utilisateur ne peut pas signer document à nouveau
|
||||
|
||||
**Cause**: Contrainte UNIQUE (doc_id, user_sub) - une signature par utilisateur par document
|
||||
|
||||
**Solution**: Comportement attendu - utilisateurs ne peuvent pas signer même document deux fois
|
||||
|
||||
### Obtenir de l'Aide
|
||||
|
||||
**Logs:**
|
||||
```bash
|
||||
# Logs application
|
||||
docker compose logs -f ackify-ce
|
||||
|
||||
# Logs base de données
|
||||
docker compose logs -f ackify-db
|
||||
|
||||
# Logs worker email (grep email)
|
||||
docker compose logs ackify-ce | grep -i email
|
||||
```
|
||||
|
||||
**Inspection base de données:**
|
||||
```bash
|
||||
# Se connecter à PostgreSQL
|
||||
docker compose exec ackify-db psql -U ackifyr ackify
|
||||
|
||||
# Requêtes utiles
|
||||
SELECT * FROM documents ORDER BY created_at DESC LIMIT 10;
|
||||
SELECT * FROM expected_signers WHERE doc_id = 'votre_doc_id';
|
||||
SELECT * FROM email_queue WHERE status != 'sent' ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
**Rapporter problèmes:**
|
||||
- GitHub: https://github.com/btouchard/ackify-ce/issues
|
||||
- Inclure logs et messages erreur
|
||||
- Décrire comportement attendu vs réel
|
||||
|
||||
---
|
||||
|
||||
## Référence Rapide
|
||||
|
||||
### Variables Environnement
|
||||
```bash
|
||||
ACKIFY_ADMIN_EMAILS=admin@company.com
|
||||
ACKIFY_MAIL_HOST=smtp.gmail.com
|
||||
ACKIFY_MAIL_FROM=noreply@company.com
|
||||
```
|
||||
|
||||
### Endpoints Clés
|
||||
```
|
||||
GET /admin # Dashboard
|
||||
GET /admin/docs/{docId} # Détail document
|
||||
POST /admin/documents/{docId}/signers # Ajouter signataire
|
||||
POST /admin/documents/{docId}/reminders # Envoyer rappels
|
||||
PUT /admin/documents/{docId}/metadata # Mettre à jour métadonnées
|
||||
```
|
||||
|
||||
### Tables Importantes
|
||||
- `documents` - Métadonnées documents
|
||||
- `signatures` - Signatures utilisateurs
|
||||
- `expected_signers` - Qui doit signer
|
||||
- `reminder_logs` - Historique emails
|
||||
- `email_queue` - File email async
|
||||
|
||||
### Raccourcis Clavier (Frontend)
|
||||
- Barre recherche auto-focus sur dashboard
|
||||
- Entrée pour soumettre formulaires
|
||||
- Échap pour fermer modales
|
||||
|
||||
---
|
||||
|
||||
**Dernière Mise à Jour**: 2025-10-26
|
||||
**Version**: 1.0.0
|
||||
Reference in New Issue
Block a user