diff --git a/Dockerfile b/Dockerfile index 847864f..6caa6b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,16 @@ -FROM node:22-alpine AS spa-builder +# syntax=docker/dockerfile:1.7 + +# Build the SPA with the build platform to avoid slow QEMU emulation on arm64 +FROM --platform=$BUILDPLATFORM node:22-bookworm-slim AS spa-builder WORKDIR /app/webapp COPY webapp/package*.json ./ -RUN npm ci +# Speed up and stabilize npm installs in CI +# - no-audit/no-fund: skip network calls +# - no-progress: cleaner logs +# - cache mount: reuse npm cache between builds +RUN --mount=type=cache,target=/root/.npm \ + npm ci --no-audit --no-fund --no-progress COPY webapp/ ./ RUN npm run build @@ -14,22 +22,32 @@ RUN adduser -D -g '' ackuser WORKDIR /app COPY go.mod go.sum ./ ENV GOTOOLCHAIN=auto -RUN go mod download && go mod verify +# Cache Go modules and build cache between builds +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + go mod download && go mod verify COPY backend/ ./backend/ RUN mkdir -p backend/cmd/community/web/dist COPY --from=spa-builder /app/webapp/dist ./backend/cmd/community/web/dist +# Cross-compile per target platform +ARG TARGETOS +ARG TARGETARCH ARG VERSION="dev" ARG COMMIT="unknown" ARG BUILD_DATE="unknown" -RUN CGO_ENABLED=0 GOOS=linux go build \ +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \ -a -installsuffix cgo \ -ldflags="-w -s -X main.Version=${VERSION} -X main.Commit=${COMMIT} -X main.BuildDate=${BUILD_DATE}" \ -o ackify ./backend/cmd/community -RUN CGO_ENABLED=0 GOOS=linux go build \ +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \ -a -installsuffix cgo \ -ldflags="-w -s" \ -o migrate ./backend/cmd/migrate diff --git a/docs/en/admin-guide.md b/docs/en/admin-guide.md new file mode 100644 index 0000000..0cc6bcb --- /dev/null +++ b/docs/en/admin-guide.md @@ -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 diff --git a/docs/en/architecture.md b/docs/en/architecture.md new file mode 100644 index 0000000..8be8cf8 --- /dev/null +++ b/docs/en/architecture.md @@ -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/) diff --git a/docs/en/database.md b/docs/en/database.md new file mode 100644 index 0000000..3c20055 --- /dev/null +++ b/docs/en/database.md @@ -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 +``` diff --git a/docs/en/deployment.md b/docs/en/deployment.md new file mode 100644 index 0000000..e9b3ab7 --- /dev/null +++ b/docs/en/deployment.md @@ -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. diff --git a/docs/en/development.md b/docs/en/development.md new file mode 100644 index 0000000..5556c71 --- /dev/null +++ b/docs/en/development.md @@ -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(null) + + async function login() { + // ... + } + + return { user, login } +} + +// Store +export const useUserStore = defineStore('user', () => { + const currentUser = ref(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) diff --git a/docs/fr/admin-guide.md b/docs/fr/admin-guide.md new file mode 100644 index 0000000..8e0600c --- /dev/null +++ b/docs/fr/admin-guide.md @@ -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