mirror of
https://github.com/btouchard/ackify-ce.git
synced 2026-01-25 15:38:48 -06:00
- Implement PKCE (Proof Key for Code Exchange) with S256 method - Add crypto/pkce module with code verifier and challenge generation - Modify OAuth flow to include code_challenge in authorization requests - Update HandleCallback to validate code_verifier during token exchange - Extend session lifetime from 7 to 30 days - Add comprehensive unit tests for PKCE functions - Maintain backward compatibility with fallback for non-PKCE sessions - Add detailed logging for OAuth flow with PKCE tracking PKCE enhances security by preventing authorization code interception attacks, as recommended by OAuth 2.1 and OIDC standards. feat: add encrypted refresh token storage with automatic cleanup - Add oauth_sessions table for storing encrypted refresh tokens - Implement AES-256-GCM encryption for refresh tokens using cookie secret - Create OAuth session repository with full CRUD operations - Add SessionWorker for automatic cleanup of expired sessions - Configure cleanup to run every 24h for sessions older than 37 days - Modify OAuth flow to store refresh tokens after successful authentication - Track client IP and user agent for session security validation - Link OAuth sessions to user sessions via session ID - Add comprehensive encryption tests with security validations - Integrate SessionWorker into server lifecycle with graceful shutdown This enables persistent OAuth sessions with secure token storage, reducing the need for frequent re-authentication from 7 to 30 days.
937 lines
23 KiB
YAML
937 lines
23 KiB
YAML
openapi: 3.0.3
|
|
info:
|
|
title: Ackify API
|
|
description: |
|
|
REST API for Ackify - Document signature tracking with cryptographic Ed25519 signatures.
|
|
|
|
## Authentication
|
|
Most endpoints require OAuth2 authentication via session cookies.
|
|
Admin endpoints additionally require the user's email to be in the ACKIFY_ADMIN_EMAILS list.
|
|
|
|
## CSRF Protection
|
|
Write operations (POST, PUT, DELETE) require a CSRF token obtained from `GET /api/v1/csrf`.
|
|
Include the token in the `X-CSRF-Token` header.
|
|
version: 1.0.0
|
|
contact:
|
|
name: Ackify Support
|
|
url: https://github.com/btouchard/ackify-ce
|
|
license:
|
|
name: AGPL-3.0-or-later
|
|
url: https://www.gnu.org/licenses/agpl-3.0.html
|
|
|
|
servers:
|
|
- url: /api/v1
|
|
description: API v1
|
|
|
|
tags:
|
|
- name: Health
|
|
description: System health checks
|
|
- name: Auth
|
|
description: OAuth2 authentication endpoints
|
|
- name: Users
|
|
description: User information
|
|
- name: Documents
|
|
description: Document management (public)
|
|
- name: Signatures
|
|
description: Signature creation and retrieval
|
|
- name: Admin - Documents
|
|
description: Admin document management
|
|
- name: Admin - Signers
|
|
description: Admin expected signers management
|
|
- name: Admin - Reminders
|
|
description: Admin email reminder management
|
|
|
|
paths:
|
|
/health:
|
|
get:
|
|
summary: Health check
|
|
description: Returns the health status of the API
|
|
tags:
|
|
- Health
|
|
responses:
|
|
'200':
|
|
description: Service is healthy
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
status:
|
|
type: string
|
|
example: ok
|
|
timestamp:
|
|
type: string
|
|
format: date-time
|
|
|
|
/csrf:
|
|
get:
|
|
summary: Get CSRF token
|
|
description: Returns a CSRF token required for write operations
|
|
tags:
|
|
- Auth
|
|
responses:
|
|
'200':
|
|
description: CSRF token generated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
csrfToken:
|
|
type: string
|
|
example: abc123def456
|
|
|
|
/auth/start:
|
|
post:
|
|
summary: Start OAuth2 flow
|
|
description: Initiates OAuth2 authentication with the configured provider
|
|
tags:
|
|
- Auth
|
|
requestBody:
|
|
required: false
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
redirectTo:
|
|
type: string
|
|
description: URL to redirect to after successful authentication
|
|
example: /signatures
|
|
responses:
|
|
'302':
|
|
description: Redirect to OAuth provider
|
|
'400':
|
|
description: Invalid request
|
|
|
|
/auth/callback:
|
|
get:
|
|
summary: OAuth2 callback
|
|
description: Handles OAuth2 provider callback after user authentication
|
|
tags:
|
|
- Auth
|
|
parameters:
|
|
- name: code
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
- name: state
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'302':
|
|
description: Redirect to application
|
|
'400':
|
|
description: Invalid callback parameters
|
|
'401':
|
|
description: Authentication failed
|
|
|
|
/auth/logout:
|
|
get:
|
|
summary: Logout
|
|
description: Logs out the current user and clears the session
|
|
tags:
|
|
- Auth
|
|
responses:
|
|
'302':
|
|
description: Redirect to home page
|
|
|
|
/auth/check:
|
|
get:
|
|
summary: Check authentication status
|
|
description: Checks if user has an active OAuth session (only available if ACKIFY_OAUTH_AUTO_LOGIN=true)
|
|
tags:
|
|
- Auth
|
|
responses:
|
|
'200':
|
|
description: Authentication status
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
authenticated:
|
|
type: boolean
|
|
|
|
/users/me:
|
|
get:
|
|
summary: Get current user
|
|
description: Returns information about the currently authenticated user
|
|
tags:
|
|
- Users
|
|
security:
|
|
- sessionAuth: []
|
|
responses:
|
|
'200':
|
|
description: Current user information
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/User'
|
|
'401':
|
|
description: Not authenticated
|
|
|
|
/documents:
|
|
get:
|
|
summary: List documents
|
|
description: Returns a paginated list of all documents
|
|
tags:
|
|
- Documents
|
|
parameters:
|
|
- name: page
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
default: 1
|
|
- name: limit
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
default: 20
|
|
maximum: 100
|
|
responses:
|
|
'200':
|
|
description: List of documents
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
documents:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Document'
|
|
total:
|
|
type: integer
|
|
page:
|
|
type: integer
|
|
limit:
|
|
type: integer
|
|
|
|
post:
|
|
summary: Create document
|
|
description: Creates a new document with metadata
|
|
tags:
|
|
- Documents
|
|
security:
|
|
- csrfToken: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CreateDocumentRequest'
|
|
responses:
|
|
'201':
|
|
description: Document created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Document'
|
|
'400':
|
|
description: Invalid request
|
|
'403':
|
|
description: CSRF token missing or invalid
|
|
|
|
/documents/{docId}:
|
|
get:
|
|
summary: Get document
|
|
description: Returns document metadata and signature count
|
|
tags:
|
|
- Documents
|
|
parameters:
|
|
- name: docId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Document details
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/DocumentWithCount'
|
|
'404':
|
|
description: Document not found
|
|
|
|
/documents/{docId}/signatures:
|
|
get:
|
|
summary: Get document signatures
|
|
description: Returns all signatures for a document
|
|
tags:
|
|
- Documents
|
|
parameters:
|
|
- name: docId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: List of signatures
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
signatures:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Signature'
|
|
|
|
/documents/{docId}/signatures/status:
|
|
get:
|
|
summary: Get user signature status
|
|
description: Checks if the current user has signed this document
|
|
tags:
|
|
- Signatures
|
|
security:
|
|
- sessionAuth: []
|
|
parameters:
|
|
- name: docId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Signature status
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
hasSigned:
|
|
type: boolean
|
|
signature:
|
|
$ref: '#/components/schemas/Signature'
|
|
|
|
/documents/{docId}/expected-signers:
|
|
get:
|
|
summary: Get expected signers
|
|
description: Returns the list of expected signers for a document
|
|
tags:
|
|
- Documents
|
|
parameters:
|
|
- name: docId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: List of expected signers
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
expectedSigners:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/ExpectedSigner'
|
|
|
|
/documents/find-or-create:
|
|
get:
|
|
summary: Find or create document
|
|
description: Finds a document by reference, or creates it if it doesn't exist
|
|
tags:
|
|
- Documents
|
|
parameters:
|
|
- name: ref
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
description: Document reference (URL, path, or custom ID)
|
|
responses:
|
|
'200':
|
|
description: Document found or created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Document'
|
|
|
|
/signatures:
|
|
get:
|
|
summary: Get user signatures
|
|
description: Returns all signatures created by the current user
|
|
tags:
|
|
- Signatures
|
|
security:
|
|
- sessionAuth: []
|
|
responses:
|
|
'200':
|
|
description: List of user signatures
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
signatures:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Signature'
|
|
|
|
post:
|
|
summary: Create signature
|
|
description: Creates a cryptographic Ed25519 signature for a document
|
|
tags:
|
|
- Signatures
|
|
security:
|
|
- sessionAuth: []
|
|
- csrfToken: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CreateSignatureRequest'
|
|
responses:
|
|
'201':
|
|
description: Signature created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Signature'
|
|
'400':
|
|
description: Invalid request
|
|
'409':
|
|
description: User has already signed this document
|
|
|
|
/admin/documents:
|
|
get:
|
|
summary: List all documents (admin)
|
|
description: Returns paginated list of all documents with admin metadata
|
|
tags:
|
|
- Admin - Documents
|
|
security:
|
|
- sessionAuth: []
|
|
- adminRole: []
|
|
parameters:
|
|
- name: page
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
default: 1
|
|
- name: limit
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
default: 20
|
|
responses:
|
|
'200':
|
|
description: List of documents
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
documents:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Document'
|
|
total:
|
|
type: integer
|
|
|
|
/admin/documents/{docId}:
|
|
get:
|
|
summary: Get document details (admin)
|
|
description: Returns detailed document information
|
|
tags:
|
|
- Admin - Documents
|
|
security:
|
|
- sessionAuth: []
|
|
- adminRole: []
|
|
parameters:
|
|
- name: docId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Document details
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Document'
|
|
|
|
delete:
|
|
summary: Delete document (admin)
|
|
description: Soft deletes a document
|
|
tags:
|
|
- Admin - Documents
|
|
security:
|
|
- sessionAuth: []
|
|
- adminRole: []
|
|
- csrfToken: []
|
|
parameters:
|
|
- name: docId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'204':
|
|
description: Document deleted
|
|
'404':
|
|
description: Document not found
|
|
|
|
/admin/documents/{docId}/metadata:
|
|
put:
|
|
summary: Update document metadata (admin)
|
|
description: Updates document title, URL, checksum, and description
|
|
tags:
|
|
- Admin - Documents
|
|
security:
|
|
- sessionAuth: []
|
|
- adminRole: []
|
|
- csrfToken: []
|
|
parameters:
|
|
- name: docId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/UpdateDocumentMetadataRequest'
|
|
responses:
|
|
'200':
|
|
description: Metadata updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Document'
|
|
|
|
/admin/documents/{docId}/signers:
|
|
get:
|
|
summary: Get document with signers (admin)
|
|
description: Returns document with both expected and actual signers
|
|
tags:
|
|
- Admin - Signers
|
|
security:
|
|
- sessionAuth: []
|
|
- adminRole: []
|
|
parameters:
|
|
- name: docId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Document with signers
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/DocumentWithSigners'
|
|
|
|
post:
|
|
summary: Add expected signer (admin)
|
|
description: Adds one or more expected signers to a document
|
|
tags:
|
|
- Admin - Signers
|
|
security:
|
|
- sessionAuth: []
|
|
- adminRole: []
|
|
- csrfToken: []
|
|
parameters:
|
|
- name: docId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/AddExpectedSignerRequest'
|
|
responses:
|
|
'201':
|
|
description: Signer(s) added
|
|
'400':
|
|
description: Invalid request
|
|
|
|
/admin/documents/{docId}/signers/{email}:
|
|
delete:
|
|
summary: Remove expected signer (admin)
|
|
description: Removes an expected signer from a document
|
|
tags:
|
|
- Admin - Signers
|
|
security:
|
|
- sessionAuth: []
|
|
- adminRole: []
|
|
- csrfToken: []
|
|
parameters:
|
|
- name: docId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
- name: email
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'204':
|
|
description: Signer removed
|
|
|
|
/admin/documents/{docId}/status:
|
|
get:
|
|
summary: Get document status (admin)
|
|
description: Returns completion statistics for a document
|
|
tags:
|
|
- Admin - Documents
|
|
security:
|
|
- sessionAuth: []
|
|
- adminRole: []
|
|
parameters:
|
|
- name: docId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Document status
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/DocumentStatus'
|
|
|
|
/admin/documents/{docId}/reminders:
|
|
get:
|
|
summary: Get reminder history (admin)
|
|
description: Returns email reminder send history for a document
|
|
tags:
|
|
- Admin - Reminders
|
|
security:
|
|
- sessionAuth: []
|
|
- adminRole: []
|
|
parameters:
|
|
- name: docId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Reminder history
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
reminders:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/ReminderLog'
|
|
|
|
post:
|
|
summary: Send reminders (admin)
|
|
description: Sends email reminders to pending signers
|
|
tags:
|
|
- Admin - Reminders
|
|
security:
|
|
- sessionAuth: []
|
|
- adminRole: []
|
|
- csrfToken: []
|
|
parameters:
|
|
- name: docId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/SendRemindersRequest'
|
|
responses:
|
|
'200':
|
|
description: Reminders queued
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
queued:
|
|
type: integer
|
|
description: Number of emails queued for sending
|
|
|
|
/openapi.json:
|
|
get:
|
|
summary: Get OpenAPI specification
|
|
description: Returns this OpenAPI specification in JSON format
|
|
tags:
|
|
- Health
|
|
responses:
|
|
'200':
|
|
description: OpenAPI spec
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
|
|
components:
|
|
securitySchemes:
|
|
sessionAuth:
|
|
type: apiKey
|
|
in: cookie
|
|
name: session
|
|
description: OAuth2 session cookie
|
|
|
|
csrfToken:
|
|
type: apiKey
|
|
in: header
|
|
name: X-CSRF-Token
|
|
description: CSRF protection token
|
|
|
|
adminRole:
|
|
type: http
|
|
scheme: bearer
|
|
description: Admin email must be in ACKIFY_ADMIN_EMAILS
|
|
|
|
schemas:
|
|
User:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
description: User unique identifier (OAuth sub claim)
|
|
email:
|
|
type: string
|
|
format: email
|
|
name:
|
|
type: string
|
|
isAdmin:
|
|
type: boolean
|
|
|
|
Document:
|
|
type: object
|
|
properties:
|
|
docId:
|
|
type: string
|
|
example: abc123
|
|
title:
|
|
type: string
|
|
url:
|
|
type: string
|
|
checksum:
|
|
type: string
|
|
checksumAlgorithm:
|
|
type: string
|
|
enum: [SHA-256, SHA-512, MD5]
|
|
description:
|
|
type: string
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
updatedAt:
|
|
type: string
|
|
format: date-time
|
|
createdBy:
|
|
type: string
|
|
|
|
DocumentWithCount:
|
|
allOf:
|
|
- $ref: '#/components/schemas/Document'
|
|
- type: object
|
|
properties:
|
|
signatureCount:
|
|
type: integer
|
|
|
|
Signature:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
format: int64
|
|
docId:
|
|
type: string
|
|
userSub:
|
|
type: string
|
|
userEmail:
|
|
type: string
|
|
format: email
|
|
userName:
|
|
type: string
|
|
signedAt:
|
|
type: string
|
|
format: date-time
|
|
payloadHash:
|
|
type: string
|
|
signature:
|
|
type: string
|
|
description: Ed25519 signature (hex-encoded)
|
|
nonce:
|
|
type: string
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
referer:
|
|
type: string
|
|
prevHash:
|
|
type: string
|
|
docChecksum:
|
|
type: string
|
|
|
|
ExpectedSigner:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
format: int64
|
|
docId:
|
|
type: string
|
|
email:
|
|
type: string
|
|
format: email
|
|
name:
|
|
type: string
|
|
addedAt:
|
|
type: string
|
|
format: date-time
|
|
addedBy:
|
|
type: string
|
|
notes:
|
|
type: string
|
|
|
|
ReminderLog:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
format: int64
|
|
docId:
|
|
type: string
|
|
recipientEmail:
|
|
type: string
|
|
format: email
|
|
sentAt:
|
|
type: string
|
|
format: date-time
|
|
sentBy:
|
|
type: string
|
|
templateUsed:
|
|
type: string
|
|
status:
|
|
type: string
|
|
enum: [sent, failed, bounced, queued]
|
|
errorMessage:
|
|
type: string
|
|
|
|
DocumentStatus:
|
|
type: object
|
|
properties:
|
|
docId:
|
|
type: string
|
|
expectedCount:
|
|
type: integer
|
|
signedCount:
|
|
type: integer
|
|
pendingCount:
|
|
type: integer
|
|
completionPercentage:
|
|
type: number
|
|
format: float
|
|
|
|
DocumentWithSigners:
|
|
allOf:
|
|
- $ref: '#/components/schemas/Document'
|
|
- type: object
|
|
properties:
|
|
expectedSigners:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/ExpectedSigner'
|
|
signatures:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Signature'
|
|
|
|
CreateDocumentRequest:
|
|
type: object
|
|
required:
|
|
- reference
|
|
properties:
|
|
reference:
|
|
type: string
|
|
description: Document URL, path, or custom ID
|
|
title:
|
|
type: string
|
|
description:
|
|
type: string
|
|
|
|
UpdateDocumentMetadataRequest:
|
|
type: object
|
|
properties:
|
|
title:
|
|
type: string
|
|
url:
|
|
type: string
|
|
checksum:
|
|
type: string
|
|
checksumAlgorithm:
|
|
type: string
|
|
enum: [SHA-256, SHA-512, MD5]
|
|
description:
|
|
type: string
|
|
|
|
CreateSignatureRequest:
|
|
type: object
|
|
required:
|
|
- docId
|
|
properties:
|
|
docId:
|
|
type: string
|
|
referer:
|
|
type: string
|
|
description: Source service (Google Docs, GitHub, etc.)
|
|
docChecksum:
|
|
type: string
|
|
description: Document checksum at signing time
|
|
|
|
AddExpectedSignerRequest:
|
|
type: object
|
|
required:
|
|
- emails
|
|
properties:
|
|
emails:
|
|
type: array
|
|
items:
|
|
type: string
|
|
format: email
|
|
notes:
|
|
type: string
|
|
|
|
SendRemindersRequest:
|
|
type: object
|
|
properties:
|
|
emails:
|
|
type: array
|
|
items:
|
|
type: string
|
|
format: email
|
|
description: Specific emails to send to (omit to send to all pending)
|
|
docURL:
|
|
type: string
|
|
description: Custom document URL for email
|
|
locale:
|
|
type: string
|
|
enum: [en, fr, es, de, it]
|
|
default: en
|
|
description: Email language
|