Merged in feature/eula.part1 (pull request #206)

eula support

* eula support

* docs
This commit is contained in:
Jay Brown
2026-01-22 18:17:27 +00:00
parent c10fa98d0a
commit 63c12a2f44
38 changed files with 15646 additions and 230 deletions
+297 -8
View File
@@ -28,6 +28,7 @@ The API is organized into the following service groups:
| ExportService | Operations related to exports |
| AuthService | Operations related to authentication |
| AdminService | Operations related to user management and administration |
| EulaService | Operations related to End User License Agreement management and tracking |
## Authentication and Authorization
@@ -1247,6 +1248,281 @@ Re-enables a previously disabled user in AWS Cognito. Restores authentication ca
---
## EULA Operations (EulaService)
The EULA (End User License Agreement) service manages EULA versions and tracks user consent. It provides both public endpoints for retrieving and agreeing to EULAs, and administrative endpoints for managing EULA versions and viewing compliance reports.
### Public Endpoints
#### `GET /eula`
Returns the current active EULA version. This endpoint is public and does not require authentication.
**Security**: Public (no authentication required)
**Response** (`EulaPublicResponse`):
```json
{
"id": "019580df-ef65-7676-8de9-94435a93337a",
"version": "1.0",
"title": "Terms of Service",
"content": "# Terms of Service\n\nThis is the full EULA content in Markdown format...",
"effective_date": "2025-01-01T00:00:00Z"
}
```
**Responses**:
- `200`: Current EULA version retrieved successfully
- `404`: No current EULA version configured
- `500`: Internal server error
#### `GET /eula/status`
Returns the current user's agreement status for the current EULA version. Used by the frontend to determine if the user needs to be prompted to agree.
**Security**: Requires JWT authentication
**Response** (`EulaStatusResponse`):
```json
{
"has_agreed": true,
"current_version": "1.0",
"current_version_id": "019580df-ef65-7676-8de9-94435a93337a",
"agreed_at": "2025-01-15T10:30:00Z",
"agreed_version": "1.0"
}
```
**Responses**:
- `200`: EULA status retrieved successfully
- `401`: Authentication required
- `404`: No current EULA version configured
- `500`: Internal server error
#### `POST /eula/agree`
Records the user's agreement to the current EULA version. This operation is idempotent - calling it multiple times will not create duplicate records. The IP address is automatically captured from the request.
**Security**: Requires JWT authentication
**Response** (`EulaAgreementResponse`):
```json
{
"id": "019580df-ef65-7676-8de9-94435a93337b",
"eula_version_id": "019580df-ef65-7676-8de9-94435a93337a",
"eula_version": "1.0",
"agreed_at": "2025-01-15T10:30:00Z"
}
```
**Responses**:
- `201`: Agreement recorded successfully (first agreement)
- `200`: User already agreed to current version (returns existing agreement)
- `400`: No current EULA version configured
- `401`: Authentication required
- `500`: Internal server error
### Administrative Endpoints
#### `GET /admin/eula`
Returns a paginated list of all EULA versions.
**Security**: Requires JWT authentication
**Query Parameters**:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| page | integer | 1 | Page number (1-10000) |
| page_size | integer | 20 | Results per page (1-100) |
**Response** (`EulaVersionList`):
```json
{
"versions": [
{
"id": "019580df-ef65-7676-8de9-94435a93337a",
"version": "1.0",
"title": "Terms of Service",
"content": "# Terms of Service...",
"effective_date": "2025-01-01T00:00:00Z",
"is_current": true,
"created_at": "2024-12-01T00:00:00Z",
"created_by": "admin@example.com",
"activated_at": "2025-01-01T00:00:00Z",
"activated_by": "admin@example.com"
}
],
"total": 3,
"has_more": false
}
```
#### `POST /admin/eula`
Creates a new EULA version. The content should be in Markdown format.
**Security**: Requires JWT authentication
**Request Body** (`EulaVersionCreate`):
```json
{
"version": "2.0",
"title": "Updated Terms of Service",
"content": "# Updated Terms of Service\n\nThis is the new EULA content...",
"effective_date": "2025-06-01T00:00:00Z"
}
```
| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| version | string | Yes | Unique version identifier (max 50 chars) |
| title | string | Yes | Human-readable title |
| content | string | Yes | Full EULA text in Markdown format |
| effective_date | datetime | Yes | When this version becomes effective |
**Response** (`201 Created`): `EulaVersion` object
**Error Responses**:
- `400`: Invalid request data
- `401`: Authentication required
- `403`: Insufficient permissions
- `409`: Version string already exists
#### `GET /admin/eula/{version_id}`
Retrieves a specific EULA version by its ID.
**Security**: Requires JWT authentication
**Path Parameters**:
- `version_id`: EULA version UUID
**Response** (`EulaVersion`): Full EULA version object
#### `PATCH /admin/eula/{version_id}`
Updates only the metadata (title and effective date) of an EULA version. Content cannot be modified to maintain audit integrity.
**Security**: Requires JWT authentication
**Request Body** (`EulaVersionUpdate`):
```json
{
"title": "Updated Title",
"effective_date": "2025-07-01T00:00:00Z"
}
```
**Response** (`200 OK`): Updated `EulaVersion` object
#### `POST /admin/eula/{version_id}/activate`
Sets this version as the current active EULA. Uses a database transaction to atomically clear the previous current flag and set the new one.
**Security**: Requires JWT authentication
**Path Parameters**:
- `version_id`: EULA version UUID to activate
**Response** (`200 OK`): Activated `EulaVersion` object with `is_current=true`
**Error Responses**:
- `404`: Version not found
- `409`: Concurrent activation conflict (another admin activated a different version)
#### `GET /admin/eula/agreements`
Returns a paginated list of user agreements. Use filters to view agreements for specific EULA versions or users.
**Security**: Requires JWT authentication
**Query Parameters**:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| page | integer | 1 | Page number (1-10000) |
| page_size | integer | 20 | Results per page (1-100) |
| version_id | uuid | - | Filter by specific EULA version |
| user_id | string | - | Filter by Cognito subject ID |
**Response** (`EulaAgreementList`):
```json
{
"agreements": [
{
"id": "019580df-ef65-7676-8de9-94435a93337b",
"cognito_subject_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"user_email": "user@example.com",
"eula_version_id": "019580df-ef65-7676-8de9-94435a93337a",
"eula_version": "1.0",
"agreed_at": "2025-01-15T10:30:00Z",
"agreed_from_ip": "192.168.1.1"
}
],
"total": 150,
"has_more": true
}
```
#### `GET /admin/eula/compliance`
Returns a comprehensive compliance report showing which users have and have not agreed to a specific EULA version. Defaults to the current active version if no version_id is provided.
**Security**: Requires JWT authentication
**Query Parameters**:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| page | integer | 1 | Page number (1-10000) |
| page_size | integer | 50 | Results per page (1-100) |
| version_id | uuid | - | Specific EULA version (defaults to current) |
| agreed | boolean | - | Filter by agreement status |
**Response** (`EulaComplianceReport`):
```json
{
"version_id": "019580df-ef65-7676-8de9-94435a93337a",
"version": "1.0",
"total_users": 200,
"agreed_count": 150,
"not_agreed_count": 50,
"compliance_percentage": 75.0,
"users": [
{
"cognito_subject_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe",
"has_agreed": true,
"agreed_at": "2025-01-15T10:30:00Z"
}
],
"page": 1,
"page_size": 50,
"has_more": true
}
```
---
## Request/Response Schemas
### Standard Response Headers
@@ -1330,13 +1606,14 @@ The API uses Permit.io for fine-grained authorization. This section documents wh
The following endpoints skip authentication and Permit.io authorization entirely:
| Endpoint | Purpose |
| ---------------- | ------------------------ |
| `GET /home` | Public home/menu page |
| `GET /logout` | Session termination |
| `GET /health` | Health check endpoint |
| `GET /swagger/*` | Swagger UI documentation |
| `GET /metrics` | Prometheus metrics |
| Endpoint | Purpose |
| ---------------- | --------------------------------- |
| `GET /home` | Public home/menu page |
| `GET /logout` | Session termination |
| `GET /health` | Health check endpoint |
| `GET /swagger/*` | Swagger UI documentation |
| `GET /metrics` | Prometheus metrics |
| `GET /eula` | Public EULA version retrieval |
### Special Authentication Endpoints
@@ -1404,6 +1681,17 @@ The following table shows what data is passed to Permit.io for each protected en
| `DELETE` | `/admin/users/{email}` | `admin` | `delete` | Delete user |
| `POST` | `/admin/users/{email}/disable` | `admin` | `post` | Disable user |
| `POST` | `/admin/users/{email}/enable` | `admin` | `post` | Enable user |
| **EULA Operations** |
| `GET` | `/eula` | - | - | Public (no auth) |
| `GET` | `/eula/status` | `eula` | `get` | Get user's EULA status |
| `POST` | `/eula/agree` | `eula` | `post` | Record EULA agreement |
| `GET` | `/admin/eula` | `admin` | `get` | List EULA versions |
| `POST` | `/admin/eula` | `admin` | `post` | Create EULA version |
| `GET` | `/admin/eula/{version_id}` | `admin` | `get` | Get EULA version |
| `PATCH` | `/admin/eula/{version_id}` | `admin` | `patch` | Update EULA metadata |
| `POST` | `/admin/eula/{version_id}/activate` | `admin` | `post` | Activate EULA version |
| `GET` | `/admin/eula/agreements` | `admin` | `get` | List EULA agreements |
| `GET` | `/admin/eula/compliance` | `admin` | `get` | Get compliance report |
### Permit.io Configuration Requirements
@@ -1422,7 +1710,8 @@ To configure Permit.io to work with this API, administrators must create:
| `field-extractions` | Field extraction operations |
| `query` | Query definition management |
| `export` | Export operations |
| `admin` | User administration |
| `admin` | User administration and EULA admin operations |
| `eula` | EULA status and agreement operations |
#### Actions
+93 -1
View File
@@ -63,6 +63,8 @@ erDiagram
collectorVersions ||--|| collectorActiveVersions : "tracks active"
collectorQueries }o--|| queries : "references"
eulaVersions ||--o{ eulaAgreements : "has agreements"
clients {
varchar clientId PK
text name UK
@@ -292,6 +294,28 @@ erDiagram
int addedVersion FK
int removedVersion FK
}
eulaVersions {
uuid id PK
varchar version UK
text title
text content
timestamptz effectiveDate
timestamptz createdAt
varchar createdBy
boolean isCurrent
timestamptz activatedAt
varchar activatedBy
}
eulaAgreements {
uuid id PK
varchar cognitoSubjectId
varchar userEmail
uuid eulaVersionId FK
timestamptz agreedAt
varchar agreedFromIp
}
```
## Core Entities
@@ -497,6 +521,72 @@ CREATE INDEX idx_documentlabels_document_label_time ON documentLabels(documentId
- Multiple applications of same label are allowed (historical tracking)
- Note: Added in migration 110
### EULA System
The EULA (End User License Agreement) system tracks EULA versions and user consent. Added in migration 121.
#### EULA Versions (`eulaVersions`)
Stores EULA version records with content and activation tracking.
```sql
CREATE TABLE eulaVersions (
id uuid PRIMARY KEY DEFAULT uuid_generate_v7(),
version VARCHAR(50) NOT NULL UNIQUE,
title TEXT NOT NULL,
content TEXT NOT NULL,
effectiveDate TIMESTAMPTZ NOT NULL,
createdAt TIMESTAMPTZ NOT NULL DEFAULT NOW(),
createdBy VARCHAR(320) NOT NULL,
isCurrent BOOLEAN NOT NULL DEFAULT FALSE,
activatedAt TIMESTAMPTZ,
activatedBy VARCHAR(320)
);
-- Partial unique index enforces only ONE row can have isCurrent=true at any time
CREATE UNIQUE INDEX idx_eulaversions_one_current ON eulaVersions(isCurrent) WHERE isCurrent = TRUE;
-- Indexes for efficient queries
CREATE INDEX idx_eulaversions_effectivedate ON eulaVersions(effectiveDate DESC);
CREATE INDEX idx_eulaversions_iscurrent ON eulaVersions(isCurrent);
```
**Key Features**:
- UUID v7 primary keys for time-ordered insertion
- Unique version string constraint prevents duplicate versions
- Content stored in Markdown format for flexible rendering
- Partial unique index ensures only one version can be `isCurrent=true` at any time
- Activation tracking with timestamp and admin email
- Effective date supports scheduling future EULA versions
#### EULA Agreements (`eulaAgreements`)
Tracks user consent with audit metadata.
```sql
CREATE TABLE eulaAgreements (
id uuid PRIMARY KEY DEFAULT uuid_generate_v7(),
cognitoSubjectId VARCHAR(256) NOT NULL,
userEmail VARCHAR(320) NOT NULL,
eulaVersionId uuid NOT NULL REFERENCES eulaVersions(id) ON DELETE RESTRICT,
agreedAt TIMESTAMPTZ NOT NULL DEFAULT NOW(),
agreedFromIp VARCHAR(45) NOT NULL,
UNIQUE(cognitoSubjectId, eulaVersionId)
);
-- Indexes for efficient queries
CREATE INDEX idx_eulaagreements_subjectid ON eulaAgreements(cognitoSubjectId);
CREATE INDEX idx_eulaagreements_versionid ON eulaAgreements(eulaVersionId);
CREATE INDEX idx_eulaagreements_version_time ON eulaAgreements(eulaVersionId, agreedAt DESC);
CREATE INDEX idx_eulaagreements_user_time ON eulaAgreements(cognitoSubjectId, agreedAt DESC);
```
**Key Features**:
- Links users (by Cognito subject ID) to EULA versions they've agreed to
- Unique constraint on (cognitoSubjectId, eulaVersionId) prevents duplicate agreements
- Stores user email at time of agreement for audit purposes
- IP address tracking for legal compliance
- ON DELETE RESTRICT prevents deleting EULA versions that have agreements
- Composite indexes optimize common query patterns (agreements by version, user history)
### Queries (`queries`)
Defines data extraction logic with type-specific implementations.
@@ -1675,6 +1765,7 @@ Similar triggers exist for `collectorMinCleanVersions` and `collectorMinTextVers
- **Migration 114**: Added `documentFieldExtractionArrayFields` table with 112 array fields
- **Migration 115**: Added `currentFieldExtractions` and `currentFieldExtractionsWithArrayCount` views
- **Migration 116**: Added `documentUploads.folder_id` column with foreign key constraint
- **Migration 121**: Added `eulaVersions` and `eulaAgreements` tables for EULA management and consent tracking
### Version Compatibility
- Backward-compatible schema changes prioritized
@@ -1728,8 +1819,9 @@ Views are layered for reusability:
- **Core entities**: 5 (clients, documents, batch_uploads, queries, results)
- **Organization tables**: 3 (folders, labels, documentLabels)
- **Field extraction tables**: 3 (documentFieldExtractions, documentFieldExtractionVersions, documentFieldExtractionArrayFields)
- **EULA tables**: 2 (eulaVersions, eulaAgreements)
- **Supporting tables**: 16 (versions, entries, configs, dependencies, etc.)
- **Total tables**: 27
- **Total tables**: 29
### View Count
- **Query management**: 6 views
+6 -1
View File
@@ -315,7 +315,8 @@ Permissions follow the format `resource:action`:
| `folders` | `get`, `post`, `patch`, `delete` | Folder management |
| `query` | `get`, `post`, `patch` | Query definitions |
| `export` | `get`, `post` | Export operations |
| `admin` | `get`, `post`, `patch`, `delete` | User administration |
| `admin` | `get`, `post`, `patch`, `delete` | User and EULA administration |
| `eula` | `get`, `post` | EULA status and agreement |
### Using Permissions in UI
@@ -888,6 +889,9 @@ Content-Type: application/json
| `/client/{id}/document/batch` | POST | Upload ZIP batch |
| `/document/{id}` | GET | Get document details |
| `/clients/{clientId}/folders` | GET | List folders |
| `/eula` | GET | Get current EULA (no auth required) |
| `/eula/status` | GET | Check user's EULA agreement status |
| `/eula/agree` | POST | Record EULA agreement |
### Public Endpoints (No Authentication Required)
@@ -896,6 +900,7 @@ Content-Type: application/json
| `/health` | Health check and version info |
| `/swagger/*` | API documentation |
| `/metrics` | Prometheus metrics |
| `/eula` | Current EULA version (public) |
### TypeScript Type Definitions
+58
View File
@@ -39,6 +39,14 @@ This document provides a comprehensive summary of all REST API endpoints availab
| /admin/users/{email} | GET, HEAD, PATCH, DELETE | AdminService |
| /admin/users/{email}/disable | POST | AdminService |
| /admin/users/{email}/enable | POST | AdminService |
| /eula | GET | EulaService |
| /eula/status | GET | EulaService |
| /eula/agree | POST | EulaService |
| /admin/eula | GET, POST | EulaService |
| /admin/eula/{version_id} | GET, PATCH | EulaService |
| /admin/eula/{version_id}/activate | POST | EulaService |
| /admin/eula/agreements | GET | EulaService |
| /admin/eula/compliance | GET | EulaService |
---
@@ -299,6 +307,56 @@ This document provides a comprehensive summary of all REST API endpoints availab
- **Idempotent**: Safe to call multiple times
- Returns: `{success, email, action, cognito_subject_id, timestamp}` (200 OK)
## EULA Service
### Public Endpoints
- **GET /eula** - Get current EULA version (public, no authentication required)
- Returns: `{id, version, title, content, effective_date}` (200 OK)
- Returns 404 if no current EULA is configured
- **GET /eula/status** - Check user's agreement status
- Requires authentication
- Returns: `{has_agreed, current_version, current_version_id, agreed_at?, agreed_version?}`
- **POST /eula/agree** - Record user agreement to current EULA
- Requires authentication
- IP address automatically captured from request
- **Idempotent**: Returns 200 if already agreed, 201 for new agreement
- Returns: `{id, eula_version_id, eula_version, agreed_at}`
### Administrative Endpoints
- **GET /admin/eula** - List all EULA versions with pagination
- Query Parameters: `page`, `page_size`
- Returns: `{versions[], total, has_more}`
- **POST /admin/eula** - Create a new EULA version
- Request Body: `{version, title, content, effective_date}`
- Content should be in Markdown format
- Returns: Created `EulaVersion` object (201 Created)
- **GET /admin/eula/{version_id}** - Get specific EULA version
- Returns: Full `EulaVersion` object
- **PATCH /admin/eula/{version_id}** - Update EULA metadata
- Request Body: `{title?, effective_date?}`
- Note: Content cannot be modified (audit integrity)
- Returns: Updated `EulaVersion` object
- **POST /admin/eula/{version_id}/activate** - Activate EULA version
- Sets this version as current (atomically clears previous)
- Returns: Activated `EulaVersion` with `is_current=true`
- **GET /admin/eula/agreements** - List EULA agreements
- Query Parameters: `page`, `page_size`, `version_id?`, `user_id?`
- Returns: `{agreements[], total, has_more}`
- **GET /admin/eula/compliance** - Get compliance report
- Query Parameters: `page`, `page_size`, `version_id?`, `agreed?`
- Defaults to current EULA version
- Returns: `{version_id, version, total_users, agreed_count, not_agreed_count, compliance_percentage, users[], page, page_size, has_more}`
## Common Response Codes
All endpoints may return the following standard HTTP response codes:
+821
View File
@@ -0,0 +1,821 @@
# QueryAPI Software Architecture Guide
This document provides a comprehensive guide to the QueryAPI software architecture for new engineers joining the project. It explains the purpose of each architectural layer, how components interact, and what logic belongs where when adding new features.
## Table of Contents
- [Architecture Overview](#architecture-overview)
- [Component Layers](#component-layers)
- [Request Flow](#request-flow)
- [Adding a New API Endpoint](#adding-a-new-api-endpoint)
- [Key Patterns](#key-patterns)
- [Component Diagrams](#component-diagrams)
## Architecture Overview
QueryAPI follows a layered architecture pattern with clear separation of concerns:
```mermaid
flowchart TB
subgraph External["External Layer"]
Client[HTTP Client]
OpenAPI[OpenAPI Spec<br/>serviceAPIs/queryAPI.yaml]
end
subgraph Entry["Entry Point"]
Main[cmd/queryAPI/main.go]
end
subgraph Server["Server Layer"]
Listener[internal/server/api/listener.go]
Echo[Echo HTTP Router]
end
subgraph Middleware["Middleware Layer"]
RateLimit[Rate Limiting]
JWT[JWT Auth Middleware]
TokenValidation[Token Validation]
PermitIO[Permit.io Authorization]
OAPIValidation[OpenAPI Request Validation]
end
subgraph Generated["Generated Layer"]
APIGen[api/queryAPI/api.gen.go<br/>ServerInterface + Types]
RegisterHandlers[RegisterHandlers]
end
subgraph Controllers["Controller Layer"]
Controllers_impl[api/queryAPI/*.go<br/>Controllers struct]
end
subgraph Services["Service Layer"]
ServiceImpl[internal/&lt;domain&gt;/service.go<br/>Business Logic]
end
subgraph Data["Data Access Layer"]
Repository[internal/database/repository/*.sql.go<br/>SQLC Generated]
Queries[internal/database/queries/*.sql<br/>SQL Definitions]
Migrations[internal/database/migrations/*.sql<br/>Schema Changes]
end
subgraph Infrastructure["Infrastructure"]
DB[(PostgreSQL)]
S3[(AWS S3)]
SQS[(AWS SQS)]
end
Client --> Main
OpenAPI -.->|generates| APIGen
Main --> Listener
Listener --> Echo
Echo --> RateLimit --> JWT --> TokenValidation --> PermitIO --> OAPIValidation
OAPIValidation --> RegisterHandlers
RegisterHandlers --> Controllers_impl
Controllers_impl --> ServiceImpl
ServiceImpl --> Repository
Queries -.->|generates| Repository
Repository --> DB
ServiceImpl --> S3
ServiceImpl --> SQS
```
## Component Layers
### 1. Entry Point (`cmd/queryAPI/main.go`)
**Purpose**: Application bootstrap and dependency wiring.
**Responsibilities**:
- Print version information
- Initialize configuration from environment variables
- Create and wire all service dependencies
- Register handlers with the router
- Start the HTTP server
**Key Code Pattern**:
```go
// Create services
cli := client.New(cfg)
doc := document.New(cfg)
// Wire services into Controllers
services := &queryapi.Services{
Client: cli,
Document: doc,
}
cons := queryapi.NewControllers(services, cfg)
// Register routes
queryapi.RegisterHandlers(cfg.Router, cons)
```
**When to modify**: When adding a new domain service that needs to be injected into the API layer.
---
### 2. Server Layer (`internal/server/api/listener.go`)
**Purpose**: HTTP server configuration, middleware setup, and lifecycle management.
**Responsibilities**:
- Create and configure the Echo HTTP router
- Register middleware in the correct order
- Set up health and metrics endpoints
- Initialize background task runners
- Handle graceful shutdown
**Key Components**:
| Component | Purpose |
|-----------|---------|
| `Server` struct | Holds router, port, and cleanup functions |
| `New()` function | Creates server with all middleware configured |
| `Listen()` method | Starts the HTTP server |
| Middleware chain | Rate limiting -> Auth -> Validation |
**Middleware Order** (applied in sequence):
1. Config injection middleware
2. Prometheus metrics
3. Request logging (slog)
4. Recovery (panic handling)
5. CORS
6. Debug logging (when DEBUG=true)
7. Rate limiting
8. Authentication routes registration
9. Test user injection (when DISABLE_AUTH=true)
10. Handler registration
11. OpenAPI request validation
---
### 3. Middleware Layer (`internal/cognitoauth/`)
**Purpose**: Authentication and authorization enforcement.
**Key Files**:
| File | Purpose |
|------|---------|
| `middleware.go` | Main middleware functions (`TokenValidationMiddleware`, `JWTAuthMiddleware`) |
| `handler.go` | OAuth callback and login flow handlers |
| `permitio.go` | Permit.io authorization client |
| `token.go` | JWT token operations (refresh, validation) |
| `validation.go` | Token signature and claims validation |
**Authentication Flow**:
```mermaid
sequenceDiagram
participant C as Client
participant M as Middleware
participant J as JWKS
participant P as Permit.io
C->>M: Request with JWT
M->>M: Check if public path
alt Public Path
M->>C: Allow (skip auth)
else Protected Path
M->>M: Extract token from header/cookie
M->>J: Verify token signature
J-->>M: Token valid
M->>M: Extract user claims
M->>P: Check permission(user, action, resource)
P-->>M: Permitted/Denied
alt Permitted
M->>C: Allow request
else Denied
M->>C: 403 Forbidden
end
end
```
**Path Types**:
- **Public paths**: `/health`, `/swagger/*`, `/metrics`, `/eula` (GET only)
- **Auth-only paths**: `/identity`, `/eula/status`, `/eula/agree` (authenticated but no specific permissions)
- **Protected paths**: All other routes (require both authentication and Permit.io authorization)
---
### 4. Generated API Layer (`api/queryAPI/api.gen.go`)
**Purpose**: Type-safe API interface generated from OpenAPI specification.
**Generated by**: `oapi-codegen` from `serviceAPIs/queryAPI.yaml`
**Key Generated Components**:
| Component | Purpose |
|-----------|---------|
| `ServerInterface` | Interface that controllers must implement |
| Request/Response types | Strongly-typed DTOs (e.g., `ClientCreate`, `DocClient`) |
| `RegisterHandlers()` | Connects routes to handler methods |
| `ServerInterfaceWrapper` | Wraps handlers with parameter extraction |
| Parameter types | Path/query parameter types (e.g., `ClientID`) |
**Example Interface Method**:
```go
// From ServerInterface
CreateClient(ctx echo.Context) error
GetClient(ctx echo.Context, id ClientID) error
```
**Important**: Never edit `api.gen.go` directly. Modify `serviceAPIs/queryAPI.yaml` and run `task generate`.
---
### 5. Controller Layer (`api/queryAPI/*.go`)
**Purpose**: HTTP request handling and response formatting.
**Key Files**:
| File | Domain |
|------|--------|
| `controllers.go` | `Controllers` struct and constructor |
| `client.go` | Client endpoints |
| `documents.go` | Document endpoints |
| `folders.go` | Folder endpoints |
| `labels.go` | Label endpoints |
| `collector.go` | Collector endpoints |
| `export.go` | Export endpoints |
| `authHandlers.go` | Authentication-related endpoints |
| `adminHandlers.go` | User administration endpoints |
**Responsibilities**:
- Bind and validate request bodies
- Call appropriate service methods
- Transform service results to API response types
- Return appropriate HTTP status codes
- Handle errors with proper HTTP error responses
**Controller Pattern**:
```go
func (s *Controllers) GetClient(ctx echo.Context, id ClientID) error {
// 1. Call service layer
client, err := s.svc.Client.Get(ctx.Request().Context(), id)
if err != nil {
// 2. Handle error
return echo.NewHTTPError(http.StatusBadRequest,
fmt.Sprintf("Unable to get client: %s", err))
}
// 3. Transform to API type and return
return ctx.JSON(http.StatusOK, DocClient{
Id: client.ID,
Name: client.Name,
CanSync: client.CanSync,
})
}
```
**What belongs here**:
- Request parsing and binding
- Input validation (beyond OpenAPI validation)
- Calling service methods
- Response transformation
- HTTP status code decisions
**What does NOT belong here**:
- Business logic
- Database operations
- External service calls
- Complex data transformations
---
### 6. Service Layer (`internal/<domain>/`)
**Purpose**: Business logic and domain operations.
**Structure per domain**:
```
internal/client/
├── service.go # Service struct and constructor
├── create.go # Create operation
├── get.go # Read operations
├── list.go # List operations
├── status.go # Status checks
└── *_test.go # Unit tests
```
**Service Pattern**:
```go
// Service struct with config dependency
type Service struct {
cfg serviceconfig.ConfigProvider
}
// Constructor
func New(cfg serviceconfig.ConfigProvider) *Service {
return &Service{cfg}
}
// Business operation
func (s *Service) Get(ctx context.Context, id string) (*Client, error) {
// Access database through config
client, err := s.cfg.GetDBQueries().GetClient(ctx, id)
if err != nil {
return nil, err
}
return parseFullClient(client), nil
}
```
**What belongs here**:
- Business logic and rules
- Input normalization and validation
- Orchestration of multiple repository calls
- Transaction management
- Domain-specific transformations
- Business error handling
**Accessing Infrastructure**:
```go
// Database queries
s.cfg.GetDBQueries().GetClient(ctx, id)
// Database transactions
s.cfg.ExecuteDBTransaction(ctx, func(ctx context.Context, q *repository.Queries) error {
// Multiple operations in a transaction
return nil
})
// Object storage
s.cfg.GetStoreClient() // S3 client
s.cfg.GetBucket() // Bucket name
// Queue operations
s.cfg.GetQueueClient()
```
---
### 7. Repository Layer (`internal/database/repository/`)
**Purpose**: Type-safe database access generated from SQL.
**Generated by**: SQLC from `internal/database/queries/*.sql`
**Key Files**:
| File | Purpose |
|------|---------|
| `db.go` | `Queries` struct and `DBTX` interface |
| `models.go` | Generated Go types from database schema |
| `client.sql.go` | Generated client query methods |
| `document.sql.go` | Generated document query methods |
| etc. | One file per SQL source file |
**SQL Query Definition** (`internal/database/queries/client.sql`):
```sql
-- name: GetClient :one
SELECT * FROM fullClients WHERE clientId = $1;
-- name: CreateClient :exec
INSERT INTO clients (clientId, name) VALUES ($1, $2);
-- name: ListClients :many
SELECT * FROM fullClients ORDER BY name;
```
**Generated Go Code**:
```go
func (q *Queries) GetClient(ctx context.Context, clientid string) (*Fullclient, error)
func (q *Queries) CreateClient(ctx context.Context, arg *CreateClientParams) error
func (q *Queries) ListClients(ctx context.Context) ([]*Fullclient, error)
```
**Transaction Support**:
```go
// Create queries with transaction
q.WithTx(tx)
```
---
### 8. Database Migrations (`internal/database/migrations/`)
**Purpose**: Version-controlled schema evolution.
**Naming Convention**: `YYYYMMDDHHMMSS_description.up.sql` / `.down.sql`
**Examples**:
- `00000000000003_clients.up.sql` - Create clients table
- `00000000000109_create_folders_table.up.sql` - Add folders feature
**Migration Tool**: golang-migrate
---
### 9. Configuration Layer (`internal/serviceconfig/`)
**Purpose**: Environment-based configuration with dependency injection.
**Key Interface** (`ConfigProvider`):
```go
type ConfigProvider interface {
// Database access
GetDBQueries() *repository.Queries
GetDBPool() *pgxpool.Pool
ExecuteDBTransaction(ctx, func) error
// Logging
GetLogger() *slog.Logger
// AWS services
GetStoreClient() objectstore.Client // S3
GetQueueClient() queue.Client // SQS
GetBucket() string
// Auth
GetAuthConfig() auth.ConfigProvider
}
```
**Configuration Composition**:
```go
type QueryAPIConfig struct {
api.BaseConfig // Server config
clientsync.ClientSyncConfig // Queue config
objectstore.ObjectStoreConfig // S3 config
BackgroundRunner *backgroundtask.Runner
}
```
---
## Request Flow
Here's how a request flows through the system:
```mermaid
sequenceDiagram
participant C as HTTP Client
participant E as Echo Router
participant MW as Middleware Chain
participant V as OpenAPI Validator
participant W as ServerInterfaceWrapper
participant H as Controller Handler
participant S as Service
participant R as Repository
participant DB as PostgreSQL
C->>E: POST /client
E->>MW: Apply middleware
MW->>MW: Rate limit check
MW->>MW: JWT validation
MW->>MW: Permit.io authorization
MW->>V: Validate request body
V->>W: Route to wrapper
W->>W: Extract path/query params
W->>H: CreateClient(ctx)
H->>H: ctx.Bind(&req)
H->>S: client.Create(ctx, params)
S->>S: Normalize input
S->>R: CreateClient(ctx, dbParams)
R->>DB: INSERT INTO clients...
DB-->>R: Success
R-->>S: nil error
S-->>H: clientID, nil
H->>H: Build response
H-->>C: 201 Created + JSON body
```
---
## Adding a New API Endpoint
Follow these steps to add a new endpoint (e.g., `GET /client/{id}/summary`):
### Step 1: Define the API Contract
Edit `serviceAPIs/queryAPI.yaml`:
```yaml
paths:
/client/{id}/summary:
parameters:
- $ref: "#/components/parameters/ClientID"
get:
operationId: getClientSummary
tags:
- ClientService
summary: Get client summary
security:
- jwtAuth: []
responses:
"200":
description: Client summary
content:
application/json:
schema:
$ref: "#/components/schemas/ClientSummary"
components:
schemas:
ClientSummary:
type: object
required:
- id
- documentCount
properties:
id:
type: string
documentCount:
type: integer
```
### Step 2: Generate Code
```bash
task generate
```
This updates:
- `api/queryAPI/api.gen.go` - Adds `GetClientSummary` to `ServerInterface` and `ClientSummary` type
### Step 3: Add Database Query (if needed)
Edit `internal/database/queries/client.sql`:
```sql
-- name: GetClientDocumentCount :one
SELECT COUNT(*) as count FROM documents WHERE clientId = $1;
```
Run: `task generate`
### Step 4: Add Service Method
Create or edit `internal/client/summary.go`:
```go
package client
import "context"
type Summary struct {
ID string
DocumentCount int64
}
func (s *Service) GetSummary(ctx context.Context, id string) (*Summary, error) {
// Validate client exists
client, err := s.Get(ctx, id)
if err != nil {
return nil, err
}
// Get document count
count, err := s.cfg.GetDBQueries().GetClientDocumentCount(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get document count: %w", err)
}
return &Summary{
ID: client.ID,
DocumentCount: count,
}, nil
}
```
### Step 5: Implement Handler
Edit `api/queryAPI/client.go`:
```go
func (s *Controllers) GetClientSummary(ctx echo.Context, id ClientID) error {
summary, err := s.svc.Client.GetSummary(ctx.Request().Context(), id)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest,
fmt.Sprintf("Unable to get client summary: %s", err))
}
return ctx.JSON(http.StatusOK, ClientSummary{
Id: summary.ID,
DocumentCount: int32(summary.DocumentCount),
})
}
```
### Step 6: Add Tests
Create `internal/client/summary_test.go`:
```go
func TestService_GetSummary(t *testing.T) {
// Use testcontainers for real database testing
cfg := test.NewTestConfig(t)
svc := client.New(cfg)
// Create test client
clientID, err := svc.Create(ctx, client.CreateParams{...})
require.NoError(t, err)
// Test summary
summary, err := svc.GetSummary(ctx, clientID)
require.NoError(t, err)
assert.Equal(t, clientID, summary.ID)
assert.Equal(t, int64(0), summary.DocumentCount)
}
```
### Step 7: Verify
```bash
task lint # Check for issues
task test:unit # Run unit tests
task fullsuite:ci # Run full test suite
```
---
## Key Patterns
### Dependency Injection via ConfigProvider
Services receive all dependencies through the `ConfigProvider` interface:
```go
type Service struct {
cfg serviceconfig.ConfigProvider
}
func (s *Service) DoSomething(ctx context.Context) error {
// Database
s.cfg.GetDBQueries().SomeQuery(ctx, ...)
// Logging
s.cfg.GetLogger().Info("Operation completed")
// S3
s.cfg.GetStoreClient().PutObject(...)
return nil
}
```
### Transaction Handling
Use `ExecuteDBTransaction` for operations requiring atomicity:
```go
err := s.cfg.ExecuteDBTransaction(ctx, func(ctx context.Context, q *repository.Queries) error {
if err := q.CreateClient(ctx, params); err != nil {
return err
}
if err := q.CreateRootFolder(ctx, folderParams); err != nil {
return err
}
return nil
})
```
### Error Handling
Services return errors, controllers convert to HTTP errors:
```go
// Service - return domain errors
if client == nil {
return nil, errors.New("client not found")
}
// Controller - convert to HTTP error
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
```
### Input Normalization
Normalize and validate inputs in the service layer:
```go
func (s *Service) normalizeCreate(params *CreateParams) error {
// Trim whitespace
params.Name = strings.TrimSpace(params.Name)
// Validate format
if !isValidID(params.ID) {
return errors.New("invalid ID format")
}
return nil
}
```
---
## Component Diagrams
### Domain Package Structure
```mermaid
flowchart LR
subgraph "internal/client/"
S[service.go<br/>Service struct]
C[create.go<br/>Create operation]
G[get.go<br/>Read operations]
L[list.go<br/>List operations]
ST[status.go<br/>Status checks]
end
subgraph "Repository"
R[repository/client.sql.go]
end
S --> C
S --> G
S --> L
S --> ST
C --> R
G --> R
L --> R
ST --> R
```
### Configuration Composition
```mermaid
classDiagram
class ConfigProvider {
<<interface>>
GetDBQueries()
GetLogger()
GetStoreClient()
}
class BaseConfig {
Port int
BaseURL string
CognitoConfig
DBConfig
AWSConfig
}
class QueryAPIConfig {
BaseConfig
ClientSyncConfig
ObjectStoreConfig
}
ConfigProvider <|.. BaseConfig
BaseConfig <|-- QueryAPIConfig
```
### Service Layer Interactions
```mermaid
flowchart TB
subgraph Controllers
CH[Client Handler]
DH[Document Handler]
end
subgraph Services
CS[Client Service]
DS[Document Service]
US[Upload Service]
end
subgraph Repository
CR[Client Queries]
DR[Document Queries]
end
subgraph Infrastructure
DB[(PostgreSQL)]
S3[(S3)]
end
CH --> CS
DH --> DS
DH --> US
CS --> CR
DS --> DR
US --> DR
US --> S3
CR --> DB
DR --> DB
```
---
## Quick Reference
| Layer | Location | Responsibility |
|-------|----------|----------------|
| Entry Point | `cmd/queryAPI/main.go` | Bootstrap, wire dependencies |
| Server | `internal/server/api/listener.go` | HTTP server, middleware setup |
| Middleware | `internal/cognitoauth/` | Auth, authorization |
| Generated | `api/queryAPI/api.gen.go` | Interface, types (don't edit) |
| Controllers | `api/queryAPI/*.go` | Request handling, response formatting |
| Services | `internal/<domain>/` | Business logic |
| Repository | `internal/database/repository/` | Database access (generated) |
| Queries | `internal/database/queries/*.sql` | SQL definitions |
| Migrations | `internal/database/migrations/` | Schema changes |
| Config | `internal/serviceconfig/` | Environment config, DI |
---
## See Also
- [02-service-architecture.md](./02-service-architecture.md) - Overall system architecture
- [06-code-organization.md](./06-code-organization.md) - Project structure details
- [03-api-documentation.md](./03-api-documentation.md) - API reference
- [04-data-architecture.md](./04-data-architecture.md) - Database schema