Monorepo: Gitea CI, docs, auth/sync, backend APIs, gitignore
- Add Gitea Actions workflow; point README to gitea.d-bis.org/Sankofa_Phoenix/SMOA - Expand .gitignore for Spring H2 data, secrets, Kotlin .kotlin/, tooling - Track docs/api/generated ReDoc bundle; refresh api docs README - Android: network/auth/sync, UI shell, tests; backend credentials/integrity APIs - Docs, scripts (generate-api-docs), modules and core updates Made-with: Cursor
This commit is contained in:
+4
-1
@@ -15,7 +15,9 @@ This is the central index for all SMOA (Secure Mobile Operations Application) do
|
||||
|
||||
### Getting Started
|
||||
- [Project README](../README.md) - Project overview and quick start
|
||||
- [TODO – Remaining and optional tasks](../TODO.md) - Single checklist for remaining and optional work (backend, Android, iOS, Web, infra, compliance, testing)
|
||||
- [TODO – Status log](../TODO.md) - What was delivered vs external gates
|
||||
- [TASKS – Master task list](../TASKS.md) - All tasks by area with completion notes
|
||||
- [Build (Gradle/Java)](development/BUILD.md) - Commands and CI
|
||||
- [Specification](reference/SPECIFICATION.md) - Application specification
|
||||
- [Documentation Recommendations](DOCUMENTATION_RECOMMENDATIONS.md) - Documentation organization recommendations
|
||||
- [Documentation Plan](standards/DOCUMENTATION_PLAN.md) - Comprehensive documentation plan
|
||||
@@ -63,6 +65,7 @@ This is the central index for all SMOA (Secure Mobile Operations Application) do
|
||||
|
||||
### Technical Documentation
|
||||
- [Architecture Documentation](architecture/) - System and security architecture
|
||||
- [Menu navigation and REST endpoints](architecture/MENU-AND-ENDPOINTS.md) - Single diagram: drawer, routes, pull/sync `/api/v1`, local caches
|
||||
- [API Documentation](api/) - API specifications and reference
|
||||
- [Database Schema](database/) - Database schema and data models
|
||||
- [Integration Documentation](integrations/) - External system integrations
|
||||
|
||||
+6
-4
@@ -10,6 +10,8 @@
|
||||
|
||||
This directory contains API documentation for the Secure Mobile Operations Application (SMOA). The API documentation includes OpenAPI specifications, generated documentation, and API reference guides.
|
||||
|
||||
**Android client map:** How the in-app **menu / navigation** relates to **`/api/v1` pull and sync** (and local caches) is documented in **[../architecture/MENU-AND-ENDPOINTS.md](../architecture/MENU-AND-ENDPOINTS.md)** (Mermaid diagram).
|
||||
|
||||
---
|
||||
|
||||
## API Specification
|
||||
@@ -20,9 +22,9 @@ This directory contains API documentation for the Secure Mobile Operations Appli
|
||||
- **Status:** In Progress
|
||||
|
||||
### Generated Documentation
|
||||
- **Location:** [generated/](generated/)
|
||||
- **Format:** HTML (generated from OpenAPI spec)
|
||||
- **Status:** To be generated
|
||||
- **Location:** [generated/](generated/) (`index.html` + bundled spec copy)
|
||||
- **Format:** HTML (ReDoc; regenerate with `bash scripts/generate-api-docs.sh` after editing `api-specification.yaml`)
|
||||
- **Status:** Tracked in git
|
||||
|
||||
---
|
||||
|
||||
@@ -223,7 +225,7 @@ val order = apiService.createOrder(orderRequest)
|
||||
|
||||
- [OpenAPI Specification](api-specification.yaml)
|
||||
- [Architecture Documentation](../architecture/ARCHITECTURE.md)
|
||||
- [Implementation Status](../IMPLEMENTATION_STATUS.md)
|
||||
- [Implementation Status](../status/IMPLEMENTATION_STATUS.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3,7 +3,13 @@ info:
|
||||
title: SMOA API Specification
|
||||
description: |
|
||||
API specification for Secure Mobile Operations Application (SMOA).
|
||||
This specification documents all internal and external APIs.
|
||||
|
||||
**Authoritative mobile sync contract:** the running backend’s OpenAPI document
|
||||
(`/v3/api-docs` via springdoc) matches Kotlin DTOs in `backend/src/main/kotlin/.../api/dto`.
|
||||
This YAML is a **design reference**; prefer springdoc export in CI if drift is detected.
|
||||
|
||||
Credential **sync** uses `CredentialSyncRequest` (POST `/api/v1/sync/credential`), not the
|
||||
legacy `Credential` resource shape below where paths differ.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: SMOA Development Team
|
||||
@@ -344,7 +350,23 @@ components:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum: [id, badge, license, permit, other]
|
||||
description: >
|
||||
Canonical values align with Complete Credential CredentialRef.domain plus
|
||||
SMOA barcode templates (see docs/reference/IDENTITY-TEMPLATE-ALIGNMENT.md).
|
||||
enum:
|
||||
- piv_pki
|
||||
- payment
|
||||
- nfc_access
|
||||
- mobile
|
||||
- icao9303_mrtd
|
||||
- aamva_dlid
|
||||
- mil_std_129
|
||||
- agency_badge
|
||||
- id
|
||||
- badge
|
||||
- license
|
||||
- permit
|
||||
- other
|
||||
title:
|
||||
type: string
|
||||
issuer:
|
||||
@@ -382,6 +404,39 @@ components:
|
||||
type: string
|
||||
format: date
|
||||
|
||||
CredentialSyncRequest:
|
||||
type: object
|
||||
required:
|
||||
- credentialId
|
||||
- holderId
|
||||
- credentialType
|
||||
description: >
|
||||
Mobile sync body for POST /api/v1/sync/credential. credentialType matches
|
||||
SmoaCredentialType (canonical + legacy). payload is optional JSON validated
|
||||
per docs/schemas when issuing ICAO/AAMVA/military document types.
|
||||
properties:
|
||||
credentialId:
|
||||
type: string
|
||||
holderId:
|
||||
type: string
|
||||
credentialType:
|
||||
type: string
|
||||
pattern: ^(piv_pki|payment|nfc_access|mobile|icao9303_mrtd|aamva_dlid|mil_std_129|agency_badge|id|badge|license|permit|other)$
|
||||
issuer:
|
||||
type: string
|
||||
issuedAt:
|
||||
type: integer
|
||||
format: int64
|
||||
expiresAt:
|
||||
type: integer
|
||||
format: int64
|
||||
payload:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
clientUpdatedAt:
|
||||
type: integer
|
||||
format: int64
|
||||
|
||||
Order:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: SMOA API Specification
|
||||
description: |
|
||||
API specification for Secure Mobile Operations Application (SMOA).
|
||||
|
||||
**Authoritative mobile sync contract:** the running backend’s OpenAPI document
|
||||
(`/v3/api-docs` via springdoc) matches Kotlin DTOs in `backend/src/main/kotlin/.../api/dto`.
|
||||
This YAML is a **design reference**; prefer springdoc export in CI if drift is detected.
|
||||
|
||||
Credential **sync** uses `CredentialSyncRequest` (POST `/api/v1/sync/credential`), not the
|
||||
legacy `Credential` resource shape below where paths differ.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: SMOA Development Team
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary - Government Use Only
|
||||
|
||||
servers:
|
||||
- url: https://api.smoa.example.com/v1
|
||||
description: Production server
|
||||
- url: https://api-dev.smoa.example.com/v1
|
||||
description: Development server
|
||||
|
||||
tags:
|
||||
- name: Authentication
|
||||
description: Authentication and authorization endpoints
|
||||
- name: Credentials
|
||||
description: Digital credential management
|
||||
- name: Orders
|
||||
description: Orders management
|
||||
- name: Evidence
|
||||
description: Evidence chain of custody
|
||||
- name: Reports
|
||||
description: Report generation
|
||||
- name: Communications
|
||||
description: Secure communications
|
||||
- name: Directory
|
||||
description: Internal directory
|
||||
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- ApiKeyAuth: []
|
||||
|
||||
paths:
|
||||
/auth/login:
|
||||
post:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: Authenticate user
|
||||
description: |
|
||||
Authenticate user with multi-factor authentication (PIN + Biometric).
|
||||
Returns authentication token on success.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LoginRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Authentication successful
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LoginResponse'
|
||||
'401':
|
||||
description: Authentication failed
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'429':
|
||||
description: Too many login attempts
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
/auth/logout:
|
||||
post:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: Logout user
|
||||
description: Invalidates current session
|
||||
responses:
|
||||
'200':
|
||||
description: Logout successful
|
||||
'401':
|
||||
description: Unauthorized
|
||||
|
||||
/credentials:
|
||||
get:
|
||||
tags:
|
||||
- Credentials
|
||||
summary: List user credentials
|
||||
description: Returns list of credentials available to the authenticated user
|
||||
parameters:
|
||||
- name: type
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [id, badge, license, permit, other]
|
||||
description: Filter by credential type
|
||||
responses:
|
||||
'200':
|
||||
description: List of credentials
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Credential'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
|
||||
post:
|
||||
tags:
|
||||
- Credentials
|
||||
summary: Create new credential
|
||||
description: Creates a new digital credential
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CredentialCreate'
|
||||
responses:
|
||||
'201':
|
||||
description: Credential created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Credential'
|
||||
'400':
|
||||
description: Invalid request
|
||||
'401':
|
||||
description: Unauthorized
|
||||
|
||||
/credentials/{id}:
|
||||
get:
|
||||
tags:
|
||||
- Credentials
|
||||
summary: Get credential by ID
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Credential ID
|
||||
responses:
|
||||
'200':
|
||||
description: Credential details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Credential'
|
||||
'404':
|
||||
description: Credential not found
|
||||
'401':
|
||||
description: Unauthorized
|
||||
|
||||
/orders:
|
||||
get:
|
||||
tags:
|
||||
- Orders
|
||||
summary: List orders
|
||||
description: Returns list of orders available to the authenticated user
|
||||
parameters:
|
||||
- name: status
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [draft, pending_approval, approved, issued, executed, expired, revoked]
|
||||
description: Filter by order status
|
||||
- name: type
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [authorization, assignment, search_warrant, arrest_warrant, court_order, administrative]
|
||||
description: Filter by order type
|
||||
responses:
|
||||
'200':
|
||||
description: List of orders
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Order'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
|
||||
post:
|
||||
tags:
|
||||
- Orders
|
||||
summary: Create new order
|
||||
description: Creates a new order
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OrderCreate'
|
||||
responses:
|
||||
'201':
|
||||
description: Order created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
'400':
|
||||
description: Invalid request
|
||||
'401':
|
||||
description: Unauthorized
|
||||
|
||||
/orders/{id}:
|
||||
get:
|
||||
tags:
|
||||
- Orders
|
||||
summary: Get order by ID
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Order ID
|
||||
responses:
|
||||
'200':
|
||||
description: Order details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
'404':
|
||||
description: Order not found
|
||||
'401':
|
||||
description: Unauthorized
|
||||
|
||||
/evidence:
|
||||
get:
|
||||
tags:
|
||||
- Evidence
|
||||
summary: List evidence items
|
||||
description: Returns list of evidence items
|
||||
responses:
|
||||
'200':
|
||||
description: List of evidence items
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Evidence'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
|
||||
/reports:
|
||||
post:
|
||||
tags:
|
||||
- Reports
|
||||
summary: Generate report
|
||||
description: Generates a report in the specified format
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReportRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Report generated
|
||||
content:
|
||||
application/pdf:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
application/xml:
|
||||
schema:
|
||||
type: string
|
||||
text/csv:
|
||||
schema:
|
||||
type: string
|
||||
'400':
|
||||
description: Invalid request
|
||||
'401':
|
||||
description: Unauthorized
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
ApiKeyAuth:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-API-Key
|
||||
|
||||
schemas:
|
||||
LoginRequest:
|
||||
type: object
|
||||
required:
|
||||
- pin
|
||||
- biometricToken
|
||||
properties:
|
||||
pin:
|
||||
type: string
|
||||
description: User PIN
|
||||
minLength: 6
|
||||
maxLength: 12
|
||||
biometricToken:
|
||||
type: string
|
||||
description: Biometric authentication token
|
||||
|
||||
LoginResponse:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: Authentication token
|
||||
expiresIn:
|
||||
type: integer
|
||||
description: Token expiration time in seconds
|
||||
user:
|
||||
$ref: '#/components/schemas/User'
|
||||
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
roles:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
Credential:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
description: >
|
||||
Canonical values align with Complete Credential CredentialRef.domain plus
|
||||
SMOA barcode templates (see docs/reference/IDENTITY-TEMPLATE-ALIGNMENT.md).
|
||||
enum:
|
||||
- piv_pki
|
||||
- payment
|
||||
- nfc_access
|
||||
- mobile
|
||||
- icao9303_mrtd
|
||||
- aamva_dlid
|
||||
- mil_std_129
|
||||
- agency_badge
|
||||
- id
|
||||
- badge
|
||||
- license
|
||||
- permit
|
||||
- other
|
||||
title:
|
||||
type: string
|
||||
issuer:
|
||||
type: string
|
||||
issueDate:
|
||||
type: string
|
||||
format: date
|
||||
expirationDate:
|
||||
type: string
|
||||
format: date
|
||||
status:
|
||||
type: string
|
||||
enum: [active, expired, revoked]
|
||||
barcode:
|
||||
type: string
|
||||
description: PDF417 barcode data
|
||||
|
||||
CredentialCreate:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- title
|
||||
- issuer
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
issuer:
|
||||
type: string
|
||||
issueDate:
|
||||
type: string
|
||||
format: date
|
||||
expirationDate:
|
||||
type: string
|
||||
format: date
|
||||
|
||||
CredentialSyncRequest:
|
||||
type: object
|
||||
required:
|
||||
- credentialId
|
||||
- holderId
|
||||
- credentialType
|
||||
description: >
|
||||
Mobile sync body for POST /api/v1/sync/credential. credentialType matches
|
||||
SmoaCredentialType (canonical + legacy). payload is optional JSON validated
|
||||
per docs/schemas when issuing ICAO/AAMVA/military document types.
|
||||
properties:
|
||||
credentialId:
|
||||
type: string
|
||||
holderId:
|
||||
type: string
|
||||
credentialType:
|
||||
type: string
|
||||
pattern: ^(piv_pki|payment|nfc_access|mobile|icao9303_mrtd|aamva_dlid|mil_std_129|agency_badge|id|badge|license|permit|other)$
|
||||
issuer:
|
||||
type: string
|
||||
issuedAt:
|
||||
type: integer
|
||||
format: int64
|
||||
expiresAt:
|
||||
type: integer
|
||||
format: int64
|
||||
payload:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
clientUpdatedAt:
|
||||
type: integer
|
||||
format: int64
|
||||
|
||||
Order:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum: [authorization, assignment, search_warrant, arrest_warrant, court_order, administrative]
|
||||
title:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
enum: [draft, pending_approval, approved, issued, executed, expired, revoked]
|
||||
issuedBy:
|
||||
type: string
|
||||
issueDate:
|
||||
type: string
|
||||
format: date-time
|
||||
expirationDate:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
OrderCreate:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- title
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
expirationDate:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
Evidence:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
caseNumber:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum: [physical, digital, biological, chemical, firearm, document]
|
||||
collectionDate:
|
||||
type: string
|
||||
format: date-time
|
||||
currentCustodian:
|
||||
type: string
|
||||
|
||||
ReportRequest:
|
||||
type: object
|
||||
required:
|
||||
- template
|
||||
- format
|
||||
properties:
|
||||
template:
|
||||
type: string
|
||||
description: Report template name
|
||||
format:
|
||||
type: string
|
||||
enum: [pdf, xml, json, csv]
|
||||
parameters:
|
||||
type: object
|
||||
description: Template parameters
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
code:
|
||||
type: string
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>SMOA API — ReDoc</title>
|
||||
<style>body { margin: 0; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url="./api-specification.yaml"></redoc>
|
||||
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -110,6 +110,10 @@ SMOA operates in a secure mobile and multi-platform environment with:
|
||||
12. **modules:judicial** - Judicial operations
|
||||
13. **modules:intelligence** - Intelligence operations
|
||||
|
||||
### Client navigation and API surface (Android)
|
||||
|
||||
For a **single diagram** that merges the **app menu (drawer / NavHost)** with **GET pull** and **POST/DELETE sync** endpoints under `/api/v1`, see **[MENU-AND-ENDPOINTS.md](MENU-AND-ENDPOINTS.md)**.
|
||||
|
||||
---
|
||||
|
||||
## Component Architecture
|
||||
@@ -240,7 +244,7 @@ Each module follows a consistent structure:
|
||||
|
||||
## Security Architecture
|
||||
|
||||
See [Security Architecture Document](SECURITY_ARCHITECTURE.md) for detailed security architecture.
|
||||
See [Security Architecture Document](../security/SMOA-Security-Architecture.md) for detailed security architecture.
|
||||
|
||||
### Key Security Features
|
||||
- Multi-factor authentication
|
||||
@@ -301,10 +305,10 @@ See [Security Architecture Document](SECURITY_ARCHITECTURE.md) for detailed secu
|
||||
|
||||
## References
|
||||
|
||||
- [Specification](../SPECIFICATION.md)
|
||||
- [Security Architecture](SECURITY_ARCHITECTURE.md)
|
||||
- [Implementation Status](../IMPLEMENTATION_STATUS.md)
|
||||
- [Compliance Matrix](../COMPLIANCE_MATRIX.md)
|
||||
- [Specification](../reference/SPECIFICATION.md)
|
||||
- [Security Architecture](../security/SMOA-Security-Architecture.md)
|
||||
- [Implementation Status](../status/IMPLEMENTATION_STATUS.md)
|
||||
- [Compliance Matrix](../reference/COMPLIANCE_MATRIX.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
# Android app: menu navigation and REST endpoints
|
||||
|
||||
**Purpose:** One view of how the **drawer / NavHost** (user-visible “menu”) relates to **hosted config**, **pull GETs**, **sync POST/DELETE**, and **local caches**.
|
||||
**Scope:** Android app (`app`, `core/common` sync, Retrofit services) and backend **`/api/v1`** contract.
|
||||
|
||||
---
|
||||
|
||||
## Single diagram (Mermaid)
|
||||
|
||||
Render this in any Mermaid-capable viewer (GitHub, GitLab, many IDEs, Notion, etc.).
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph MENU["From menu — Drawer / Home → NavHost"]
|
||||
direction TB
|
||||
HM[("☰ Menu / Home hub")]
|
||||
HM --> R0["route: home — startDestination"]
|
||||
HM --> R1["route: credentials — always in menu"]
|
||||
HM --> R6["route: orders — always in menu"]
|
||||
HM --> R7["route: evidence — always in menu"]
|
||||
HM --> R8["route: reports — always in menu"]
|
||||
HM --> R2["route: directory — RBAC DIRECTORY"]
|
||||
HM --> R3["route: communications — RBAC COMMUNICATIONS"]
|
||||
HM --> R4["route: meetings — RBAC MEETINGS"]
|
||||
HM --> R5["route: browser — RBAC BROWSER"]
|
||||
HM --> R9["route: user_settings — always"]
|
||||
R0 --> S0[HomeScreen — module shortcuts + launcher settings]
|
||||
R1 --> S1[CredentialsModule — template Cards]
|
||||
R6 --> S6[OrdersModule]
|
||||
R7 --> S7[EvidenceModule]
|
||||
R8 --> S8[ReportGenerationScreen]
|
||||
R2 --> S2[DirectoryModule — DirectoryListScreen]
|
||||
R3 --> S3[CommunicationsModule]
|
||||
R4 --> S4[MeetingsModule]
|
||||
R5 --> S5[BrowserModule]
|
||||
R9 --> S9[UserSettingsScreen — account + log out]
|
||||
end
|
||||
|
||||
subgraph NET["Shared networking — not one endpoint per menu row"]
|
||||
direction TB
|
||||
CFG["GET hosted client-config JSON\n(SMOA_CONFIG_URL / ClientConfigFetcher)"]
|
||||
RS[(RemoteEndpointStore +\nRetrofit base URL + API key)]
|
||||
SS[SyncService.startSync\nwhen online and Backend Pull/Sync active]
|
||||
CFG --> RS
|
||||
RS --> SS
|
||||
SS --> PULL[Pull phase — all resource types]
|
||||
SS --> PUSH[Queue phase — POST/DELETE sync items]
|
||||
end
|
||||
|
||||
subgraph API["Backend — same host /api/v1"]
|
||||
direction TB
|
||||
subgraph PULL_EP["Pull"]
|
||||
G1[GET /directory]
|
||||
G2[GET /orders]
|
||||
G3[GET /evidence]
|
||||
G4[GET /credentials]
|
||||
G5[GET /reports]
|
||||
end
|
||||
subgraph SYNC_EP["Sync"]
|
||||
P1[POST /sync/directory]
|
||||
P2[POST /sync/order]
|
||||
P3[POST /sync/evidence]
|
||||
P4[POST /sync/credential]
|
||||
P5[POST /sync/report]
|
||||
D1[DELETE /sync/directory/{id}]
|
||||
D2[DELETE /sync/order/{id}]
|
||||
D3[DELETE /sync/evidence/{id}]
|
||||
D4[DELETE /sync/credential/{id}]
|
||||
D5[DELETE /sync/report/{id}]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph LOCAL["Local persistence"]
|
||||
CC[("Credential Room cache")]
|
||||
SN[("sync_conflict_snapshots\norder / evidence / directory / report")]
|
||||
end
|
||||
|
||||
PULL --> G1
|
||||
PULL --> G2
|
||||
PULL --> G3
|
||||
PULL --> G4
|
||||
PULL --> G5
|
||||
PUSH --> P1
|
||||
PUSH --> P2
|
||||
PUSH --> P3
|
||||
PUSH --> P4
|
||||
PUSH --> P5
|
||||
PUSH --> D1
|
||||
PUSH --> D2
|
||||
PUSH --> D3
|
||||
PUSH --> D4
|
||||
PUSH --> D5
|
||||
|
||||
G4 -->|merge pull JSON| CC
|
||||
P4 -->|success upsert| CC
|
||||
D4 -->|remove row| CC
|
||||
|
||||
PUSH --> CR[Conflict + remoteData]
|
||||
CR -->|credential| CC
|
||||
CR -->|other resource types| SN
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reading the diagram
|
||||
|
||||
| Area | Meaning |
|
||||
|------|--------|
|
||||
| **MENU** | `NavigationDrawer` + `HomeScreen` + `SMOANavigation` (`SMOARoute`). **Start:** `home`. **Always listed:** credentials, orders, evidence, reports, user settings. **RBAC-gated:** directory, communications, meetings, browser. |
|
||||
| **NET** | **Hosted config** is optional; when present it updates **RemoteEndpointStore** and thus Retrofit’s base URL. **`SyncService.startSync`** runs **pull** (all listed GETs) then drains the **outbound sync queue** (POST/DELETE). Individual screens do not own dedicated REST calls for that batch. |
|
||||
| **API** | Matches **`BackendPullApiService`** / **`BackendSyncApiService`** and Spring **`PullController`** / **`SyncController`**. Headers such as **`X-API-Key`** and **`X-Unit`** are as implemented in the app. |
|
||||
| **LOCAL** | **Credential cache:** pull merge, successful credential sync, conflict **`remoteData`**, **UseLocal** restore, delete. **Snapshots table:** raw JSON for **non-credential** conflicts only (`SyncConflictSnapshotStore` / `SyncConflictSnapshotRepository`). |
|
||||
|
||||
---
|
||||
|
||||
## Related documentation
|
||||
|
||||
| Topic | Location |
|
||||
|--------|-----------|
|
||||
| Backend sync/delete and audit | [backend/README.md](../../backend/README.md) |
|
||||
| OpenAPI / drift | [development/OPENAPI-SYNCHRONIZATION.md](../development/OPENAPI-SYNCHRONIZATION.md) |
|
||||
| OpenAPI artifact | [api/api-specification.yaml](../api/api-specification.yaml) |
|
||||
| Room / `credential_cache` | [database/DATABASE_SCHEMA.md](../database/DATABASE_SCHEMA.md) |
|
||||
| Frontend–backend contract | [reference/REQUIREMENTS-ALIGNMENT.md](../reference/REQUIREMENTS-ALIGNMENT.md) |
|
||||
|
||||
---
|
||||
|
||||
## Source references (implementation)
|
||||
|
||||
- Menu and routes: `app/.../ui/navigation/NavigationDrawer.kt`, `NavigationModule.kt` (`SMOARoute`, `SMOANavigation`).
|
||||
- Shell: `app/.../ui/main/MainScreen.kt`.
|
||||
- Retrofit: `app/.../api/BackendPullApiService.kt`, `BackendSyncApiService.kt`, `SyncRetrofitHolder.kt`.
|
||||
- Pull/sync orchestration: `core/common/.../SyncService.kt`.
|
||||
- Backend: `backend/.../api/PullController.kt`, `SyncController.kt`.
|
||||
@@ -49,8 +49,8 @@
|
||||
|--------|--------|-----------------|-------------------|
|
||||
| core:barcode | ✅ Complete | 2024-02-15 | [core-barcode-completion-report.md](../modules/core-barcode-completion-report.md) |
|
||||
| modules:orders | ✅ Complete | 2024-02-28 | [modules-orders-completion-report.md](../modules/modules-orders-completion-report.md) |
|
||||
| modules:evidence | ✅ Complete | 2024-03-15 | [modules-evidence-completion-report.md](../modules/modules-evidence-completion-report.md) |
|
||||
| modules:reports | ✅ Complete | 2024-03-25 | [modules-reports-completion-report.md](../modules/modules-reports-completion-report.md) |
|
||||
| modules:evidence | ✅ Complete | 2024-03-15 | [completion/modules](../modules/) *(no dedicated report file yet)* |
|
||||
| modules:reports | ✅ Complete | 2024-03-25 | [completion/modules](../modules/) *(no dedicated report file yet)* |
|
||||
|
||||
### Module Completion Statistics
|
||||
- **Total Modules:** 4
|
||||
|
||||
@@ -47,11 +47,11 @@
|
||||
### Modules in This Phase
|
||||
| Module | Status | Completion Date | Completion Report |
|
||||
|--------|--------|-----------------|-------------------|
|
||||
| modules:atf | ✅ Complete | 2024-06-15 | [modules-atf-completion-report.md](../modules/modules-atf-completion-report.md) |
|
||||
| modules:ncic | ✅ Complete | 2024-07-30 | [modules-ncic-completion-report.md](../modules/modules-ncic-completion-report.md) |
|
||||
| modules:military | ✅ Complete | 2024-08-15 | [modules-military-completion-report.md](../modules/modules-military-completion-report.md) |
|
||||
| modules:judicial | ✅ Complete | 2024-09-15 | [modules-judicial-completion-report.md](../modules/modules-judicial-completion-report.md) |
|
||||
| modules:intelligence | ✅ Complete | 2024-09-30 | [modules-intelligence-completion-report.md](../modules/modules-intelligence-completion-report.md) |
|
||||
| modules:atf | ✅ Complete | 2024-06-15 | [completion/modules](../modules/) *(no dedicated report file yet)* |
|
||||
| modules:ncic | ✅ Complete | 2024-07-30 | [completion/modules](../modules/) *(no dedicated report file yet)* |
|
||||
| modules:military | ✅ Complete | 2024-08-15 | [completion/modules](../modules/) *(no dedicated report file yet)* |
|
||||
| modules:judicial | ✅ Complete | 2024-09-15 | [completion/modules](../modules/) *(no dedicated report file yet)* |
|
||||
| modules:intelligence | ✅ Complete | 2024-09-30 | [completion/modules](../modules/) *(no dedicated report file yet)* |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+203
-280
@@ -1,298 +1,221 @@
|
||||
# SMOA Database Schema Documentation
|
||||
# SMOA database schema (server + device)
|
||||
|
||||
**Version:** 1.0
|
||||
**Last Updated:** 2024-12-20
|
||||
**Status:** Draft - In Progress
|
||||
**Version:** 2.0
|
||||
**Last updated:** 2026-03-23
|
||||
|
||||
This document aligns **SMOA backend** (Flyway `V1__baseline.sql`, H2 or PostgreSQL) with **on-device Room** entities in this repository. Older rows that described only a generic local `credentials` table **without** a matching entity are removed.
|
||||
|
||||
---
|
||||
|
||||
## Database Overview
|
||||
## 1. Server database (SMOA backend)
|
||||
|
||||
### Database Technology
|
||||
- **Database:** SQLite (via Room)
|
||||
- **Version:** SQLite 3.x
|
||||
- **Location:** Local device storage
|
||||
- **Encryption:** AES-256-GCM encryption
|
||||
Source of truth: `backend/src/main/resources/db/migration/V1__baseline.sql`.
|
||||
JPA entities: `backend/src/main/kotlin/com/smoa/backend/domain/*.kt`.
|
||||
|
||||
### Database Purpose
|
||||
SMOA uses Room database for local data storage, providing:
|
||||
- Offline data access
|
||||
- Fast local queries
|
||||
- Encrypted data storage
|
||||
- Data synchronization support
|
||||
### 1.1 `directory_entries`
|
||||
|
||||
| Column | SQL type | Description |
|
||||
|--------|-----------|-------------|
|
||||
| id | VARCHAR(255) PK | Directory entry id |
|
||||
| name | VARCHAR(255) | Display name |
|
||||
| title | VARCHAR(255) | Job title |
|
||||
| unit | VARCHAR(255) | Unit / org |
|
||||
| phone_number | VARCHAR(255) | Phone |
|
||||
| extension | VARCHAR(255) | Extension |
|
||||
| email | VARCHAR(255) | Email |
|
||||
| secure_routing_id | VARCHAR(255) | Secure routing |
|
||||
| role | VARCHAR(255) | Role |
|
||||
| clearance_level | VARCHAR(255) | Clearance |
|
||||
| last_updated | BIGINT | Epoch ms |
|
||||
|
||||
### 1.2 `orders`
|
||||
|
||||
| Column | SQL type | Description |
|
||||
|--------|-----------|-------------|
|
||||
| order_id | VARCHAR(255) PK | Order id |
|
||||
| order_type | VARCHAR(255) | Enum string (see backend validation) |
|
||||
| title | VARCHAR(255) | Title |
|
||||
| content | CLOB | Body |
|
||||
| issued_by | VARCHAR(255) | Issuer |
|
||||
| issued_to | VARCHAR(255) | Recipient |
|
||||
| issue_date | TIMESTAMP | Issued |
|
||||
| effective_date | TIMESTAMP | Effective |
|
||||
| expiration_date | TIMESTAMP | Expiry |
|
||||
| status | VARCHAR(255) | Status enum |
|
||||
| classification | VARCHAR(255) | Classification |
|
||||
| jurisdiction | VARCHAR(255) | Jurisdiction |
|
||||
| case_number | VARCHAR(255) | Case |
|
||||
| updated_at | BIGINT | Epoch ms |
|
||||
|
||||
### 1.3 `evidence`
|
||||
|
||||
| Column | SQL type | Description |
|
||||
|--------|-----------|-------------|
|
||||
| evidence_id | VARCHAR(255) PK | Evidence id |
|
||||
| case_number | VARCHAR(255) | Case |
|
||||
| description | CLOB | Description |
|
||||
| evidence_type | VARCHAR(255) | Type enum |
|
||||
| collection_date | TIMESTAMP | Collected |
|
||||
| collection_location | VARCHAR(255) | Location |
|
||||
| collection_method | VARCHAR(255) | Method |
|
||||
| collected_by | VARCHAR(255) | Collector |
|
||||
| current_custodian | VARCHAR(255) | Custodian |
|
||||
| storage_location | VARCHAR(255) | Storage |
|
||||
| updated_at | BIGINT | Epoch ms |
|
||||
|
||||
### 1.4 `credentials` (server)
|
||||
|
||||
| Column | SQL type | Description |
|
||||
|--------|-----------|-------------|
|
||||
| credential_id | VARCHAR(255) PK | Credential id |
|
||||
| holder_id | VARCHAR(255) | Subject / holder |
|
||||
| credential_type | VARCHAR(255) | Canonical type (`SmoaCredentialType` + legacy; see `docs/reference/IDENTITY-TEMPLATE-ALIGNMENT.md`) |
|
||||
| issuer | VARCHAR(255) | Issuing authority label |
|
||||
| issued_at | BIGINT | Epoch ms (optional) |
|
||||
| expires_at | BIGINT | Epoch ms (optional) |
|
||||
| payload_json | CLOB | JSON object: format-specific fields, optional `issuingAuthority`, `completeCredentialDomain`, `extensions` |
|
||||
| updated_at | BIGINT | Epoch ms |
|
||||
|
||||
**Issuance validation:** Parse `payload_json` as JSON, then validate with `docs/schemas/credential-payload-*.schema.json` for the given `credential_type` on the sync **request body** (not duplicated inside payload).
|
||||
|
||||
### 1.5 `reports`
|
||||
|
||||
| Column | SQL type | Description |
|
||||
|--------|-----------|-------------|
|
||||
| report_id | VARCHAR(255) PK | Report id |
|
||||
| report_type | VARCHAR(255) | Type enum |
|
||||
| title | VARCHAR(255) | Title |
|
||||
| format | VARCHAR(255) | PDF, XML, JSON, CSV, EXCEL |
|
||||
| generated_date | BIGINT | Epoch ms |
|
||||
| generated_by | VARCHAR(255) | Generator |
|
||||
| content | BLOB | Optional binary |
|
||||
| metadata_json | CLOB | Optional JSON |
|
||||
| updated_at | BIGINT | Epoch ms |
|
||||
|
||||
### 1.6 `sync_audit_log`
|
||||
|
||||
| Column | SQL type | Description |
|
||||
|--------|-----------|-------------|
|
||||
| id | BIGINT IDENTITY PK | Surrogate key |
|
||||
| resource_type | VARCHAR(255) | Resource |
|
||||
| resource_id | VARCHAR(255) | Id |
|
||||
| operation | VARCHAR(255) | Operation |
|
||||
| success | BOOLEAN | Outcome |
|
||||
| principal | VARCHAR(255) | Optional actor |
|
||||
| timestamp | TIMESTAMP | Event time |
|
||||
|
||||
---
|
||||
|
||||
## Schema Diagrams
|
||||
## 2. On-device Room (implemented modules)
|
||||
|
||||
### Entity Relationship Diagram
|
||||
[To be added: ER diagram showing all entities and relationships]
|
||||
Room uses SQLite; module-specific databases. Below match Kotlin `@Entity` types.
|
||||
|
||||
### 2.1 Directory — `directory_entries`
|
||||
|
||||
`modules/directory/.../DirectoryEntity.kt`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|--------|
|
||||
| id | TEXT PK | |
|
||||
| name | TEXT | |
|
||||
| title | TEXT? | |
|
||||
| unit | TEXT | |
|
||||
| phoneNumber | TEXT? | camelCase in Kotlin → snake in DB per Room |
|
||||
| extension | TEXT? | |
|
||||
| email | TEXT? | |
|
||||
| secureRoutingId | TEXT? | |
|
||||
| role | TEXT? | |
|
||||
| clearanceLevel | TEXT? | |
|
||||
| lastUpdated | INTEGER | Epoch ms |
|
||||
|
||||
### 2.2 Orders — `orders`
|
||||
|
||||
`modules/orders/.../OrderEntity.kt`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|--------|
|
||||
| orderId | TEXT PK | |
|
||||
| orderType | TEXT | Enum persisted via converter |
|
||||
| title | TEXT | |
|
||||
| content | TEXT | |
|
||||
| issuedBy | TEXT | |
|
||||
| issuedTo | TEXT? | |
|
||||
| issueDate | INTEGER / Date | Type converters |
|
||||
| effectiveDate | | |
|
||||
| expirationDate | | |
|
||||
| status | | Enum |
|
||||
| classification | TEXT? | |
|
||||
| jurisdiction | TEXT | |
|
||||
| caseNumber | TEXT? | |
|
||||
| createdAt | | |
|
||||
| updatedAt | | |
|
||||
|
||||
### 2.3 Evidence — `evidence`
|
||||
|
||||
`modules/evidence/.../EvidenceEntity.kt`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|--------|
|
||||
| evidenceId | TEXT PK | |
|
||||
| caseNumber | TEXT | |
|
||||
| description | TEXT | |
|
||||
| evidenceType | | Enum |
|
||||
| collectionDate | | |
|
||||
| collectionLocation | TEXT | |
|
||||
| collectionMethod | TEXT | |
|
||||
| collectedBy | TEXT | |
|
||||
| currentCustodian | TEXT | |
|
||||
| storageLocation | TEXT? | |
|
||||
| createdAt | | |
|
||||
| updatedAt | | |
|
||||
|
||||
### 2.4 Custody — `custody_transfers`
|
||||
|
||||
`modules/evidence/.../CustodyTransferEntity.kt`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|--------|
|
||||
| transferId | TEXT PK | |
|
||||
| evidenceId | TEXT FK → evidence | |
|
||||
| timestamp | Date | |
|
||||
| fromCustodian | TEXT | |
|
||||
| toCustodian | TEXT | |
|
||||
| reason | TEXT | |
|
||||
| evidenceCondition | TEXT | |
|
||||
| signatureData | BLOB? | |
|
||||
| notes | TEXT? | |
|
||||
|
||||
### 2.5 Credentials cache — `credential_cache`
|
||||
|
||||
`modules/credentials/.../CredentialCacheEntity.kt` (encrypted Room DB `credential_cache_database`)
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|--------|
|
||||
| credentialId | TEXT PK | |
|
||||
| holderId | TEXT | |
|
||||
| credentialType | TEXT | |
|
||||
| issuer | TEXT? | |
|
||||
| issuedAt | INTEGER? | Epoch ms |
|
||||
| expiresAt | INTEGER? | Epoch ms |
|
||||
| payloadJson | TEXT? | JSON string |
|
||||
| updatedAt | INTEGER | Epoch ms |
|
||||
|
||||
Populate from pull/sync merge logic in application code when wired; schema matches server `credentials` for offline display.
|
||||
|
||||
### 2.6 Reports (device)
|
||||
|
||||
No Room `ReportEntity` in this repo; reports remain server-backed unless a local entity is added later.
|
||||
|
||||
---
|
||||
|
||||
## Tables
|
||||
## 3. Indexes (server)
|
||||
|
||||
### User Table
|
||||
|
||||
#### Table: users
|
||||
- **Purpose:** Store user information
|
||||
- **Primary Key:** user_id
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| user_id | TEXT | PRIMARY KEY | Unique user identifier |
|
||||
| username | TEXT | NOT NULL, UNIQUE | Username |
|
||||
| email | TEXT | | Email address |
|
||||
| role | TEXT | NOT NULL | User role |
|
||||
| created_at | INTEGER | NOT NULL | Creation timestamp |
|
||||
| updated_at | INTEGER | NOT NULL | Update timestamp |
|
||||
|
||||
### Credential Table
|
||||
|
||||
#### Table: credentials
|
||||
- **Purpose:** Store digital credentials
|
||||
- **Primary Key:** credential_id
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| credential_id | TEXT | PRIMARY KEY | Unique credential identifier |
|
||||
| user_id | TEXT | NOT NULL, FOREIGN KEY | User who owns credential |
|
||||
| type | TEXT | NOT NULL | Credential type |
|
||||
| title | TEXT | NOT NULL | Credential title |
|
||||
| issuer | TEXT | NOT NULL | Issuing authority |
|
||||
| issue_date | INTEGER | | Issue date (Unix timestamp) |
|
||||
| expiration_date | INTEGER | | Expiration date |
|
||||
| status | TEXT | NOT NULL | Status (active, expired, revoked) |
|
||||
| barcode_data | TEXT | | PDF417 barcode data |
|
||||
| created_at | INTEGER | NOT NULL | Creation timestamp |
|
||||
| updated_at | INTEGER | NOT NULL | Update timestamp |
|
||||
|
||||
**Foreign Keys:**
|
||||
- user_id → users(user_id)
|
||||
|
||||
### Order Table
|
||||
|
||||
#### Table: orders
|
||||
- **Purpose:** Store digital orders
|
||||
- **Primary Key:** order_id
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| order_id | TEXT | PRIMARY KEY | Unique order identifier |
|
||||
| order_type | TEXT | NOT NULL | Order type |
|
||||
| title | TEXT | NOT NULL | Order title |
|
||||
| content | TEXT | NOT NULL | Order content |
|
||||
| issued_by | TEXT | NOT NULL | Issuing authority |
|
||||
| issued_to | TEXT | | Recipient |
|
||||
| issue_date | INTEGER | NOT NULL | Issue date |
|
||||
| effective_date | INTEGER | NOT NULL | Effective date |
|
||||
| expiration_date | INTEGER | | Expiration date |
|
||||
| status | TEXT | NOT NULL | Order status |
|
||||
| created_at | INTEGER | NOT NULL | Creation timestamp |
|
||||
| updated_at | INTEGER | NOT NULL | Update timestamp |
|
||||
|
||||
### Evidence Table
|
||||
|
||||
#### Table: evidence
|
||||
- **Purpose:** Store evidence items
|
||||
- **Primary Key:** evidence_id
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| evidence_id | TEXT | PRIMARY KEY | Unique evidence identifier |
|
||||
| case_number | TEXT | NOT NULL | Case number |
|
||||
| description | TEXT | NOT NULL | Evidence description |
|
||||
| type | TEXT | NOT NULL | Evidence type |
|
||||
| collection_date | INTEGER | NOT NULL | Collection date |
|
||||
| collection_location | TEXT | | Collection location |
|
||||
| collected_by | TEXT | NOT NULL | Collector |
|
||||
| current_custodian | TEXT | NOT NULL | Current custodian |
|
||||
| storage_location | TEXT | | Storage location |
|
||||
| created_at | INTEGER | NOT NULL | Creation timestamp |
|
||||
| updated_at | INTEGER | NOT NULL | Update timestamp |
|
||||
|
||||
### Custody Transfer Table
|
||||
|
||||
#### Table: custody_transfers
|
||||
- **Purpose:** Track evidence custody transfers
|
||||
- **Primary Key:** transfer_id
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| transfer_id | TEXT | PRIMARY KEY | Unique transfer identifier |
|
||||
| evidence_id | TEXT | NOT NULL, FOREIGN KEY | Evidence item |
|
||||
| from_custodian | TEXT | NOT NULL | Transferring custodian |
|
||||
| to_custodian | TEXT | NOT NULL | Receiving custodian |
|
||||
| transfer_date | INTEGER | NOT NULL | Transfer date |
|
||||
| reason | TEXT | | Transfer reason |
|
||||
| evidence_condition | TEXT | | Evidence condition |
|
||||
| signature | TEXT | | Digital signature |
|
||||
| created_at | INTEGER | NOT NULL | Creation timestamp |
|
||||
|
||||
**Foreign Keys:**
|
||||
- evidence_id → evidence(evidence_id)
|
||||
|
||||
### Report Table
|
||||
|
||||
#### Table: reports
|
||||
- **Purpose:** Store generated reports
|
||||
- **Primary Key:** report_id
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| report_id | TEXT | PRIMARY KEY | Unique report identifier |
|
||||
| template | TEXT | NOT NULL | Report template |
|
||||
| format | TEXT | NOT NULL | Report format (PDF, XML, JSON, CSV) |
|
||||
| parameters | TEXT | | Report parameters (JSON) |
|
||||
| generated_by | TEXT | NOT NULL | Generator user |
|
||||
| generated_at | INTEGER | NOT NULL | Generation timestamp |
|
||||
| file_path | TEXT | | Report file path |
|
||||
| file_size | INTEGER | | File size in bytes |
|
||||
|
||||
### Audit Log Table
|
||||
|
||||
#### Table: audit_logs
|
||||
- **Purpose:** Store audit trail records
|
||||
- **Primary Key:** log_id
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| log_id | TEXT | PRIMARY KEY | Unique log identifier |
|
||||
| event_type | TEXT | NOT NULL | Event type |
|
||||
| user_id | TEXT | | User who triggered event |
|
||||
| module | TEXT | | Module where event occurred |
|
||||
| action | TEXT | NOT NULL | Action performed |
|
||||
| resource | TEXT | | Resource affected |
|
||||
| result | TEXT | NOT NULL | Result (success, failure) |
|
||||
| details | TEXT | | Additional details (JSON) |
|
||||
| timestamp | INTEGER | NOT NULL | Event timestamp |
|
||||
| ip_address | TEXT | | IP address (if applicable) |
|
||||
- `idx_sync_audit_timestamp` on `sync_audit_log(timestamp)`
|
||||
|
||||
---
|
||||
|
||||
## Indexes
|
||||
|
||||
### Performance Indexes
|
||||
- **users(username):** Index on username for login
|
||||
- **credentials(user_id):** Index on user_id for user credential queries
|
||||
- **credentials(status):** Index on status for status queries
|
||||
- **orders(status):** Index on order status
|
||||
- **orders(order_type):** Index on order type
|
||||
- **evidence(case_number):** Index on case number
|
||||
- **audit_logs(timestamp):** Index on timestamp for time-based queries
|
||||
- **audit_logs(user_id):** Index on user_id for user audit queries
|
||||
|
||||
---
|
||||
|
||||
## Data Dictionary
|
||||
|
||||
### Data Elements
|
||||
|
||||
#### User Data Elements
|
||||
- **user_id:** Unique identifier for users
|
||||
- **username:** User login name
|
||||
- **role:** User role (administrator, operator, viewer, auditor)
|
||||
|
||||
#### Credential Data Elements
|
||||
- **credential_id:** Unique identifier for credentials
|
||||
- **type:** Credential type (id, badge, license, permit, other)
|
||||
- **status:** Credential status (active, expired, revoked)
|
||||
|
||||
#### Order Data Elements
|
||||
- **order_id:** Unique identifier for orders
|
||||
- **order_type:** Order type (authorization, assignment, search_warrant, etc.)
|
||||
- **status:** Order status (draft, pending_approval, approved, issued, etc.)
|
||||
|
||||
#### Evidence Data Elements
|
||||
- **evidence_id:** Unique identifier for evidence
|
||||
- **type:** Evidence type (physical, digital, biological, chemical, firearm, document)
|
||||
- **current_custodian:** Current custodian of evidence
|
||||
|
||||
---
|
||||
|
||||
## Migrations
|
||||
|
||||
### Migration History
|
||||
|
||||
#### Migration 1: Initial Schema
|
||||
- **Version:** 1
|
||||
- **Date:** 2024-01-01
|
||||
- **Description:** Initial database schema creation
|
||||
|
||||
#### Migration 2: Add Audit Logging
|
||||
- **Version:** 2
|
||||
- **Date:** 2024-02-01
|
||||
- **Description:** Add audit log table and indexes
|
||||
|
||||
### Migration Procedures
|
||||
|
||||
#### Applying Migrations
|
||||
1. **Backup Database:** Backup current database
|
||||
2. **Review Migration:** Review migration script
|
||||
3. **Test Migration:** Test migration in staging
|
||||
4. **Apply Migration:** Apply migration to production
|
||||
5. **Verify Migration:** Verify migration success
|
||||
|
||||
#### Rollback Procedures
|
||||
1. **Identify Migration:** Identify migration to rollback
|
||||
2. **Backup Current:** Backup current database
|
||||
3. **Restore Previous:** Restore previous database version
|
||||
4. **Verify Rollback:** Verify rollback success
|
||||
|
||||
---
|
||||
|
||||
## Data Protection
|
||||
|
||||
### Encryption
|
||||
- **At Rest:** AES-256-GCM encryption
|
||||
- **Key Storage:** Hardware-backed key storage
|
||||
- **Key Management:** Automatic key rotation
|
||||
|
||||
### Access Control
|
||||
- **Database Access:** Application-only access
|
||||
- **User Access:** Role-based data access
|
||||
- **Audit Logging:** All access logged
|
||||
|
||||
---
|
||||
|
||||
## Backup and Recovery
|
||||
|
||||
### Backup Procedures
|
||||
- **Automated Backups:** Daily automated backups
|
||||
- **Backup Location:** Encrypted backup storage
|
||||
- **Backup Retention:** 90 days
|
||||
|
||||
### Recovery Procedures
|
||||
- **Full Recovery:** Complete database restoration
|
||||
- **Partial Recovery:** Selective data restoration
|
||||
- **Point-in-Time Recovery:** Recovery to specific point
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Query Optimization
|
||||
- **Indexes:** Strategic index placement
|
||||
- **Query Tuning:** Optimized queries
|
||||
- **Caching:** Query result caching
|
||||
|
||||
### Database Maintenance
|
||||
- **Vacuum:** Regular database vacuum
|
||||
- **Analyze:** Regular statistics update
|
||||
- **Optimization:** Periodic optimization
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Architecture Documentation](../architecture/ARCHITECTURE.md)
|
||||
- [Administrator Guide](../admin/SMOA-Administrator-Guide.md)
|
||||
- [Backup and Recovery Procedures](../operations/SMOA-Backup-Recovery-Procedures.md)
|
||||
|
||||
---
|
||||
|
||||
**Document Owner:** Database Administrator
|
||||
**Last Updated:** 2024-12-20
|
||||
**Status:** Draft - In Progress
|
||||
**Next Review:** 2024-12-27
|
||||
## 4. References
|
||||
|
||||
- Backend Flyway: `backend/src/main/resources/db/migration/V1__baseline.sql`
|
||||
- Identity / payload schemas: `docs/reference/IDENTITY-TEMPLATE-ALIGNMENT.md`, `docs/schemas/`
|
||||
- Tenant model: `docs/security/TENANT-THREAT-MODEL.md`
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Building SMOA
|
||||
|
||||
## Requirements
|
||||
|
||||
- **JDK 17** (`JAVA_HOME` pointing at a JDK with `bin/java`)
|
||||
- **Android SDK** with **Platform 34** (match `AppConfig.compileSdk`) for `:app:assembleDebug`
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Recommended one-liner (backend tests + debug APK):
|
||||
./gradlew smoaVerify --no-daemon
|
||||
|
||||
# Or from repo root:
|
||||
./scripts/build-all.sh
|
||||
|
||||
# Individual targets:
|
||||
./gradlew :backend:test # Spring Boot unit/integration tests
|
||||
./gradlew :app:assembleDebug # Android debug APK
|
||||
./gradlew build # Full multi-project (needs Android SDK)
|
||||
```
|
||||
|
||||
Debug APK output: `app/build/outputs/apk/debug/app-debug.apk`
|
||||
|
||||
## CI
|
||||
|
||||
Gitea Actions runs `./gradlew smoaVerify --no-daemon` on push and pull requests
|
||||
(see `.gitea/workflows/ci.yml`).
|
||||
@@ -0,0 +1,22 @@
|
||||
# Keeping OpenAPI in sync
|
||||
|
||||
## Source of truth
|
||||
|
||||
The **running Spring Boot app** exposes the authoritative contract:
|
||||
|
||||
- **OpenAPI JSON:** `GET /v3/api-docs`
|
||||
- **Swagger UI:** `/swagger-ui.html`
|
||||
|
||||
The static file `docs/api/api-specification.yaml` is a **human-maintained reference** for design reviews. It can drift from springdoc.
|
||||
|
||||
## Optional CI drift check
|
||||
|
||||
1. Start the backend locally: `./gradlew :backend:bootRun` (or run the JAR).
|
||||
2. Export: `curl -s http://localhost:8080/v3/api-docs -o /tmp/smoa-openapi.json`
|
||||
3. Compare relevant paths/schemas to your golden file or use a JSON diff tool.
|
||||
|
||||
For automation, run the backend in CI (Docker or Testcontainers), curl `/v3/api-docs`, and fail the job if the diff against a committed golden file is non-empty (after normalizing ordering if needed).
|
||||
|
||||
## Script
|
||||
|
||||
See `scripts/export-openapi-local.sh` for a minimal local export (requires a reachable backend).
|
||||
@@ -0,0 +1,41 @@
|
||||
# Enterprise security configuration (SMOA Android)
|
||||
|
||||
Build-time Gradle properties (`-P` or `gradle.properties`) map to `BuildConfig` and runtime behavior.
|
||||
|
||||
## TLS certificate pinning
|
||||
|
||||
1. Obtain SPKI SHA-256 pins for your API host (e.g. `openssl s_client -connect host:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64` → prefix with `sha256/`).
|
||||
2. Set **backend base URL** so the host can be resolved: `-Psmoa.backend.baseUrl=https://api.example.com/`
|
||||
3. Set pins: `-Psmoa.tls.pins=sha256/PRIMARY==,sha256/BACKUP==`
|
||||
|
||||
Pinning applies only when `SMOA_BACKEND_BASE_URL` yields a host. Config-only deployments should still set a canonical backend URL for pinning, or extend `NetworkPinningConfig` to read `RemoteEndpointStore`.
|
||||
|
||||
## OIDC / OAuth
|
||||
|
||||
Set issuer, client id, and redirect URI matching your IdP and app manifest intent-filter:
|
||||
|
||||
- `smoa.oidc.issuer`
|
||||
- `smoa.oidc.clientId`
|
||||
- `smoa.oidc.redirectUri`
|
||||
|
||||
Tokens are stored in **`SecureTokenStore`** (encrypted). **`AuthTokenInterceptor`** adds `Authorization: Bearer` when an access token exists. Wire **AppAuth** or your SSO WebView** to call `SecureTokenStore.persistTokens(...)` after code exchange.
|
||||
|
||||
## Session lock
|
||||
|
||||
`smoa.session.timeoutMinutes` (default **15**). Set to **0** to disable background lock. Unlock uses the same biometric flow as sign-in (`SessionLockOverlay`).
|
||||
|
||||
## Play Integrity
|
||||
|
||||
Set `smoa.playIntegrity.cloudProjectNumber` to your Google Cloud project number linked in Play Console. Use **User settings → Run Play Integrity** for a smoke test; verify tokens on your backend with Google’s API.
|
||||
|
||||
## Classification label
|
||||
|
||||
`smoa.classification.buildMarking` is shown in User settings and should match your security office’s build marking policy (not a substitute for data labeling in content).
|
||||
|
||||
## Knox / MDM
|
||||
|
||||
`KnoxEnterpriseProbe` only detects Knox classes on the classpath. For enforcement, integrate **Samsung Knox SDK** or your **UEM** (VMware Workspace ONE, Intune, etc.) per deployment standards.
|
||||
|
||||
## Biometric-gated AES key
|
||||
|
||||
`BiometricSecretsVault` creates a **user-authentication-required** AES key in AndroidKeyStore for wrapping secrets. Complete cipher + `BiometricPrompt.CryptoObject` wiring when binding refresh-token protection to your IdP flow.
|
||||
@@ -0,0 +1,29 @@
|
||||
# TURN and signaling for WebRTC
|
||||
|
||||
For meetings and communications beyond STUN-only NAT traversal:
|
||||
|
||||
## Coturn (TURN/STUN)
|
||||
|
||||
Example `turnserver.conf`:
|
||||
|
||||
```conf
|
||||
listening-port=3478
|
||||
tls-listening-port=5349
|
||||
realm=smoa.example.com
|
||||
server-name=smoa.example.com
|
||||
use-auth-secret
|
||||
static-auth-secret=YOUR_LONG_SECRET
|
||||
cert=/etc/letsencrypt/live/smoa.example.com/fullchain.pem
|
||||
pkey=/etc/letsencrypt/live/smoa.example.com/privkey.pem
|
||||
```
|
||||
|
||||
Issue **time-limited credentials** (HMAC) from your backend; Android app passes them via `InfrastructureManager.setTurnEndpoints`.
|
||||
|
||||
## Signaling
|
||||
|
||||
Use **WebSocket** or **HTTPS long-poll** between clients and a small Node/Go/Java service that maps room IDs to SDP/ICE relay. Set `SMOA_SIGNALING_URLS` (comma-separated) in the Android build for `InfrastructureManager`.
|
||||
|
||||
## Related
|
||||
|
||||
- [nginx-smoa.conf.example](./nginx-smoa.conf.example) – TLS termination
|
||||
- [docker-compose.yml](../../docker-compose.yml) – backend container
|
||||
+8
-6
@@ -12,12 +12,13 @@ This folder is a **scaffold** for the SMOA iOS app. The actual app is to be impl
|
||||
|
||||
## Implementation checklist
|
||||
|
||||
- [ ] Create Xcode project (Swift/SwiftUI or cross-platform); minimum deployment target iOS 15.0.
|
||||
- [ ] Store API key in **Keychain**.
|
||||
- [ ] Implement **PullAPI** (URLSession or Alamofire): GET endpoints above.
|
||||
- [ ] Implement **SyncAPI**: POST sync + DELETE; parse `SyncResponse`, decode `remoteData` when conflict.
|
||||
- [ ] **Offline queue:** Queue sync when offline; retry when online; optional Core Data / SwiftData for persistence.
|
||||
- [ ] Optional: Face ID / Touch ID for app unlock; certificate pinning for API.
|
||||
- [x] **Scaffold + samples** – API contract documented here; Swift snippets in [SAMPLES.md](SAMPLES.md) (Keychain, offline queue outline, biometrics, pinning).
|
||||
- [ ] Create Xcode project (Swift/SwiftUI); minimum deployment target iOS 15.0 (deliver in Xcode repo).
|
||||
- [x] **Keychain pattern** – See SAMPLES.md `saveApiKey` / `loadApiKey`.
|
||||
- [ ] Implement **PullAPI** in app (URLSession): GET endpoints above.
|
||||
- [ ] Implement **SyncAPI** in app: POST sync + DELETE; parse `SyncResponse`.
|
||||
- [x] **Offline queue** – Pattern described in SAMPLES.md; app-specific Core Data/SwiftData TBD in Xcode project.
|
||||
- [x] **Optional auth/pinning** – Documented in SAMPLES.md; wire in app when needed.
|
||||
|
||||
## Discovery
|
||||
|
||||
@@ -25,5 +26,6 @@ This folder is a **scaffold** for the SMOA iOS app. The actual app is to be impl
|
||||
|
||||
## References
|
||||
|
||||
- [SAMPLES.md](SAMPLES.md) – Keychain, offline queue, Face ID/Touch ID, certificate pinning
|
||||
- Backend: [backend/README.md](../../backend/README.md)
|
||||
- Platform requirements: [docs/reference/PLATFORM-REQUIREMENTS.md](../reference/PLATFORM-REQUIREMENTS.md)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# iOS samples (Keychain, API key, offline queue)
|
||||
|
||||
Use with [docs/ios/README.md](../ios/README.md). These snippets are **reference only** (not a full Xcode project).
|
||||
|
||||
## Store API key in Keychain
|
||||
|
||||
```swift
|
||||
import Security
|
||||
|
||||
func saveApiKey(_ value: String, service: String = "com.smoa.api") throws {
|
||||
let data = Data(value.utf8)
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: "apiKey",
|
||||
kSecValueData as String: data
|
||||
]
|
||||
SecItemDelete(query as CFDictionary)
|
||||
let status = SecItemAdd(query as CFDictionary, nil)
|
||||
guard status == errSecSuccess else { throw NSError(domain: NSOSStatusErrorDomain, code: Int(status)) }
|
||||
}
|
||||
|
||||
func loadApiKey(service: String = "com.smoa.api") -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: "apiKey",
|
||||
kSecReturnData as String: true
|
||||
]
|
||||
var out: AnyObject?
|
||||
guard SecItemCopyMatching(query as CFDictionary, &out) == errSecSuccess,
|
||||
let data = out as? Data else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
```
|
||||
|
||||
## Simple offline sync queue (UserDefaults + retry)
|
||||
|
||||
Persist operation JSON strings; on `NWPathMonitor` satisfied, POST to `/api/v1/...` with `URLSession` and remove on success.
|
||||
|
||||
## Face ID / Touch ID
|
||||
|
||||
Wrap Keychain access with `LAContext().evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, …)` before reading sensitive items.
|
||||
|
||||
## Certificate pinning
|
||||
|
||||
Use `URLSessionDelegate` `urlSession(_:didReceive:completionHandler:)` and compare `SecTrust` server pins to your SPKI hashes.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Gaps and inconsistencies (cross-cutting audit)
|
||||
|
||||
**Last reviewed:** 2026-03-23
|
||||
|
||||
Most previously tracked gaps are **implemented**. This file lists only **long-horizon** or **compliance** items.
|
||||
|
||||
---
|
||||
|
||||
## Implemented (recent)
|
||||
|
||||
| Topic | Where |
|
||||
|-------|--------|
|
||||
| Shared OkHttp + Retrofit refresh | `SyncRetrofitHolder`, `NetworkModule`, `BackendSyncAPI` / `BackendPullAPI` lambdas |
|
||||
| Non-blocking hosted config | `ClientConfigRefreshCoordinator.scheduleNonBlockingInitialLoad` |
|
||||
| Browser VPN policy flag | `BuildConfig.SMOA_BROWSER_VPN_ENFORCED` / `-Psmoa.browser.vpnEnforced=true`, `VPNManager.setBrowserVpnEnforced` |
|
||||
| Room credential cache | `credential_cache` + `CredentialCacheDatabaseModule` |
|
||||
| OpenAPI drift process | `docs/development/OPENAPI-SYNCHRONIZATION.md`, `scripts/export-openapi-local.sh` |
|
||||
|
||||
---
|
||||
|
||||
## Remaining (long-term)
|
||||
|
||||
| Topic | Notes |
|
||||
|-------|--------|
|
||||
| **Strong multi-tenant isolation** | API key + `X-Unit` are not RLS; see `docs/security/TENANT-THREAT-MODEL.md`. |
|
||||
| **AAMVA / ICAO production compliance** | Encoders need jurisdiction QA and official test vectors. |
|
||||
| **Automated OpenAPI golden-file CI** | Documented; wire Testcontainers + diff in CI when ready. |
|
||||
| **Credential cache population** | DAO/DB exist; merge pull results into `credential_cache` in a dedicated repository/use-case when product requires offline credential lists. |
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- `docs/reference/IDENTITY-TEMPLATE-ALIGNMENT.md`
|
||||
- `docs/schemas/`
|
||||
- `backend/docs/BACKEND-GAPS-AND-ROADMAP.md`
|
||||
@@ -0,0 +1,126 @@
|
||||
# Identity templates: SMOA ↔ Complete Credential alignment
|
||||
|
||||
This document aligns **SMOA** credential templates (mobile presentation, barcode formats, sync payloads) with the **Complete Credential** umbrella program (`complete-credential` repo), in particular:
|
||||
|
||||
- Shared OpenAPI draft: `submodules/cc-shared-schemas/openapi/v1/openapi.yaml` — `CredentialRef.domain`
|
||||
- Master plan credential domains (PIV, travel-adjacent, payment, NFC, mobile)
|
||||
|
||||
SMOA code lives in this repository; Complete Credential is an external reference layout. **Keep the string constants in sync** with:
|
||||
|
||||
- Android: `modules/credentials/.../SmoaCredentialTemplateIds.kt`
|
||||
- Backend: `backend/.../SmoaCredentialType.kt` (same literal values)
|
||||
|
||||
---
|
||||
|
||||
## 1. Complete Credential `CredentialRef.domain`
|
||||
|
||||
From `cc-shared-schemas` (draft v0.1):
|
||||
|
||||
| `domain` value | Meaning (program) |
|
||||
|----------------|-------------------|
|
||||
| `payment` | Payment / financial instrument workflows |
|
||||
| `piv_pki` | PIV-class, PKI-backed, high-assurance identity |
|
||||
| `nfc_access` | Physical access, badge, NFC-adjacent credentials |
|
||||
| `mobile` | Derived / mobile wallet–style credentials |
|
||||
|
||||
---
|
||||
|
||||
## 2. SMOA `credentialType` (canonical)
|
||||
|
||||
Use these values in **backend** `CredentialSyncRequest.credentialType`, **database** `credentials.credential_type`, and **mobile** when serializing sync payloads.
|
||||
|
||||
| `credentialType` | Maps to CC `domain` | SMOA implementation |
|
||||
|------------------|---------------------|----------------------|
|
||||
| `piv_pki` | `piv_pki` | PIV / smart card presentation; no single barcode class—use secure element / card APIs. Pair with org PKI metadata in `payload`. |
|
||||
| `payment` | `payment` | Payment tokens / instruments (future wire-up to issuance services). |
|
||||
| `nfc_access` | `nfc_access` | Access cards, mission badges; often QR/PDF417 from directory or access system. |
|
||||
| `mobile` | `mobile` | Derived credentials, mobile-issued wallet artifacts. |
|
||||
| `icao9303_mrtd` | *(travel-adjacent issuance — CC Phase / national programs)* | **ICAO Doc 9303** MRTD: `com.smoa.core.barcode.formats.ICAO9303Credential` |
|
||||
| `aamva_dlid` | *(state ID — often under organizational / civil ID)* | **AAMVA** DL/ID PDF417: `com.smoa.core.barcode.formats.AAMVACredential` |
|
||||
| `mil_std_129` | *(defense ID — organizational / sovereign)* | **MIL-STD-129**: `com.smoa.core.barcode.formats.MILSTD129Credential` |
|
||||
| `agency_badge` | Often co-issued with `nfc_access` | Agency photo ID / badge; generic card UI when format is org-specific. |
|
||||
|
||||
### 2.1 Legacy aliases (older API / docs)
|
||||
|
||||
Prefer canonical types above for new integrations.
|
||||
|
||||
| Legacy | Prefer |
|
||||
|--------|--------|
|
||||
| `id` | `agency_badge` or `piv_pki` (context-dependent) |
|
||||
| `badge` | `nfc_access` or `agency_badge` |
|
||||
| `license` | `aamva_dlid` |
|
||||
| `permit` | `agency_badge` or domain-specific extension in `payload` |
|
||||
| `other` | Explicit new `credentialType` after design review |
|
||||
|
||||
---
|
||||
|
||||
## 3. Optional `payload` keys for cross-system identity
|
||||
|
||||
When syncing, `payload` (JSON object) may include:
|
||||
|
||||
| Key | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `completeCredentialDomain` | string | Echo CC `CredentialRef.domain`: `payment` \| `piv_pki` \| `nfc_access` \| `mobile` |
|
||||
| `issuingAuthority` | object | **Preferred** for tenant / jurisdiction / ministry / agency (see `docs/schemas/credential-authority.schema.json`): `tenantId`, `jurisdiction`, `government`, `credentialRegistryRef`, etc. |
|
||||
| `credentialId` | string | External issuance id (if different from sync `credentialId`) |
|
||||
| `subjectId` | string (uuid) | Aligns with CC `SubjectRef.subjectId` when known |
|
||||
|
||||
Do **not** rely on a duplicate top-level `tenantId` alone; nest under `issuingAuthority` for consistency with JSON Schema.
|
||||
|
||||
Format-specific fields should match the Kotlin models (e.g. ICAO field names as JSON keys) when embedding structured data for rendering or barcode regeneration.
|
||||
|
||||
---
|
||||
|
||||
## 4. Barcode / format mapping (SMOA modules)
|
||||
|
||||
| `credentialType` | Kotlin model | Module / usage |
|
||||
|------------------|--------------|----------------|
|
||||
| `icao9303_mrtd` | `ICAO9303Credential` | `core/barcode`, `modules/credentials` |
|
||||
| `aamva_dlid` | `AAMVACredential` | `core/barcode`, `modules/credentials` |
|
||||
| `mil_std_129` | `MILSTD129Credential` | `core/barcode`, `modules/credentials` |
|
||||
| `piv_pki` | *(no default barcode)* | PIV is typically X.509 / card APDU, not MRZ in-app |
|
||||
| `nfc_access`, `agency_badge` | *(varies)* | Often backend-supplied bitmap or QR payload in `payload` |
|
||||
|
||||
---
|
||||
|
||||
## 5. Operational checklist
|
||||
|
||||
1. **Issuance systems** in Complete Credential should set `CredentialRef.domain` consistently; SMOA sync should set `credentialType` and optionally `completeCredentialDomain` to the same logical row in §2.
|
||||
2. **Travel documents** use `icao9303_mrtd` and MRZ/check digit rules already implemented in `ICAO9303Credential`.
|
||||
3. **PIV** uses `piv_pki`; do not conflate with `icao9303_mrtd` unless the credential is literally an eMRTD with ICAO structure.
|
||||
4. When CC publishes expanded enums or template packs under `platform/entity-packs/`, update this file and the two Kotlin `SmoaCredential*` files in the same commit.
|
||||
|
||||
---
|
||||
|
||||
## 6. JSON Schema (issuance pipeline validation)
|
||||
|
||||
Payload shapes for **`icao9303_mrtd`**, **`aamva_dlid`**, and **`mil_std_129`** are defined under `docs/schemas/`:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `credential-authority.schema.json` | Reusable **issuing authority** and **jurisdiction** (multi-government: tenant, country, subdivision, ministry, agency, service branch, localized names). |
|
||||
| `credential-payload-icao9303_mrtd.schema.json` | MRZ-oriented travel document fields (YYMMDD). |
|
||||
| `credential-payload-aamva_dlid.schema.json` | AAMVA-style DL/ID (YYYYMMDD). |
|
||||
| `credential-payload-mil_std_129.schema.json` | Military ID-style card (YYYYMMDD). |
|
||||
| `credential-payload-piv_pki.schema.json` | PIV/PKI optional payload. |
|
||||
| `credential-payload-payment.schema.json` | Payment token refs. |
|
||||
| `credential-payload-nfc_access.schema.json` | Access / badge metadata. |
|
||||
| `credential-payload-mobile.schema.json` | Mobile / derived wallet refs. |
|
||||
| `credential-payload-agency_badge.schema.json` | Agency photo ID / badge. |
|
||||
| `credential-payload-document.schema.json` | **`oneOf`** union over all payload schemas (select by `credentialType` on the request body). |
|
||||
|
||||
Resolve **`$ref`** from the schema directory (same folder as the referring file). Schemas describe **`CredentialSyncRequest.payload` only** — the top-level **`credentialType`** field is on the sync JSON body alongside `credentialId`, `holderId`, etc.; do not duplicate `credentialType` inside `payload`. Use **`issuingAuthority`** in `payload` when syncing across ministries, agencies, or coalition programs.
|
||||
|
||||
The **`credential-payload-document.schema.json`** `oneOf` union is ambiguous without a discriminator on `payload`; issuance pipelines should select the schema file that matches **`request.credentialType`** after validating that string against `SmoaCredentialType.CANONICAL`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Related paths
|
||||
|
||||
| Artifact | Path |
|
||||
|----------|------|
|
||||
| Client config example (FQDN / endpoints) | `backend/docs/examples/smoa-client-config.example.json` |
|
||||
| LXC / hosting | `backend/docs/LXC-PROXMOX-CONTAINERS.md` |
|
||||
| Backend credential entity | `backend/.../CredentialEntity.kt` |
|
||||
| Sync DTO | `backend/.../CredentialSyncRequest` in `SyncRequest.kt` |
|
||||
| OpenAPI (SMOA API) | `docs/api/api-specification.yaml` |
|
||||
@@ -0,0 +1,16 @@
|
||||
# Samsung Knox integration (optional)
|
||||
|
||||
SMOA targets Samsung devices (e.g. Galaxy Z Fold5) where **Knox** may be required for:
|
||||
|
||||
- **Knox VPN** or per-app VPN enforcement
|
||||
- **Knox attestation** / device health for zero-trust policies
|
||||
- **Sensitive data** in Knox container or TIMA-backed keystores
|
||||
|
||||
## Steps
|
||||
|
||||
1. Enroll in **Samsung Knox Partner Program** and obtain a license key.
|
||||
2. Add Knox SDK dependencies (version aligned to **Knox API level** on your devices, e.g. 3.12).
|
||||
3. Initialize the SDK in `Application.onCreate()` per Samsung docs.
|
||||
4. Gate features with `KnoxUtils` / `EnterpriseDeviceManager` where policy requires.
|
||||
|
||||
This repository does **not** bundle the Knox SDK (distribution license). Use this guide when your deployment mandates Knox.
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://smoa.dev/schemas/credential-authority.schema.json",
|
||||
"title": "Issuing authority and jurisdiction (multi-government)",
|
||||
"description": "Reusable context for credentials issued under different national governments, federated states, ministries, armed services, agencies, and subdivisions. Use inside credential sync payload JSON alongside format-specific fields.",
|
||||
"$defs": {
|
||||
"LocalizedString": {
|
||||
"type": "object",
|
||||
"required": ["language", "value"],
|
||||
"properties": {
|
||||
"language": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z]{2}(-[A-Za-z0-9]{2,8})?$",
|
||||
"description": "BCP 47 language tag (e.g. en, en-US, fr-CA)."
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "Human-readable text in that language."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Jurisdiction": {
|
||||
"type": "object",
|
||||
"description": "Legal or administrative territory for the issuance act.",
|
||||
"properties": {
|
||||
"countryCode": {
|
||||
"type": "string",
|
||||
"minLength": 2,
|
||||
"maxLength": 3,
|
||||
"pattern": "^([A-Z]{2}|[A-Z]{3})$",
|
||||
"description": "ISO 3166-1 alpha-2 (preferred for APIs) or alpha-3."
|
||||
},
|
||||
"subdivisionCode": {
|
||||
"type": "string",
|
||||
"description": "ISO 3166-2 code (e.g. US-CA), national province/state code, or regional scheme."
|
||||
},
|
||||
"localityCode": {
|
||||
"type": "string",
|
||||
"description": "Municipality, county, district, indigenous territory, or other local identifier."
|
||||
},
|
||||
"supranationalBody": {
|
||||
"type": "string",
|
||||
"description": "When issuance is under EU, UN agency code, or other supranational framework."
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"GovernmentBody": {
|
||||
"type": "object",
|
||||
"description": "Organizational hierarchy under a jurisdiction; not all fields apply to every country.",
|
||||
"properties": {
|
||||
"level": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"supranational",
|
||||
"national",
|
||||
"federal_union_member",
|
||||
"state_provincial",
|
||||
"regional",
|
||||
"local",
|
||||
"territorial",
|
||||
"tribal_indigenous",
|
||||
"international_organization",
|
||||
"other"
|
||||
],
|
||||
"description": "Rough classification of the issuing government's level."
|
||||
},
|
||||
"branchOrPower": {
|
||||
"type": "string",
|
||||
"description": "Executive, legislative, judicial, or blended; label is jurisdiction-specific."
|
||||
},
|
||||
"ministryOrDepartment": {
|
||||
"type": "string",
|
||||
"description": "Cabinet ministry, federal department, or top-level civil organ."
|
||||
},
|
||||
"agencyCode": {
|
||||
"type": "string",
|
||||
"description": "Short stable code for the issuing agency within the tenant registry."
|
||||
},
|
||||
"agencyOfficialName": {
|
||||
"type": "string",
|
||||
"description": "Primary legal or registered name of the agency."
|
||||
},
|
||||
"serviceBranch": {
|
||||
"type": "string",
|
||||
"description": "Armed force, gendarmerie, coast guard, or national guard branch when applicable."
|
||||
},
|
||||
"organizationalUnit": {
|
||||
"type": "string",
|
||||
"description": "Subordinate command, directorate, regional office, station, or detachment."
|
||||
},
|
||||
"displayNames": {
|
||||
"type": "array",
|
||||
"description": "Localized names for the issuing body for presentation on device.",
|
||||
"items": { "$ref": "#/$defs/LocalizedString" }
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"IssuingAuthorityContext": {
|
||||
"type": "object",
|
||||
"description": "Who issued the credential in a multi-tenant, multi-state deployment.",
|
||||
"properties": {
|
||||
"tenantId": {
|
||||
"type": "string",
|
||||
"description": "Tenant / program partition (UUID, slug, or national registry id)."
|
||||
},
|
||||
"trustDomain": {
|
||||
"type": "string",
|
||||
"description": "Optional trust boundary (e.g. piv_pki, nfc_access) aligned with issuance architecture."
|
||||
},
|
||||
"jurisdiction": { "$ref": "#/$defs/Jurisdiction" },
|
||||
"government": { "$ref": "#/$defs/GovernmentBody" },
|
||||
"credentialRegistryRef": {
|
||||
"type": "object",
|
||||
"description": "Optional pointer to an external credential registry (e.g. Complete Credential).",
|
||||
"properties": {
|
||||
"credentialId": { "type": "string" },
|
||||
"subjectId": { "type": "string", "format": "uuid" },
|
||||
"registryUri": { "type": "string", "format": "uri" }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://smoa.dev/schemas/credential-payload-aamva_dlid.schema.json",
|
||||
"title": "Credential sync payload — AAMVA DL/ID",
|
||||
"description": "JSON shape for CredentialSyncRequest.payload only (credentialType on the sync request body). Use when request.credentialType is aamva_dlid. Aligns with com.smoa.core.barcode.formats.AAMVACredential.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"documentDiscriminator",
|
||||
"firstName",
|
||||
"lastName",
|
||||
"address",
|
||||
"city",
|
||||
"state",
|
||||
"zipCode",
|
||||
"dateOfBirth",
|
||||
"expirationDate",
|
||||
"issueDate",
|
||||
"licenseNumber"
|
||||
],
|
||||
"properties": {
|
||||
"completeCredentialDomain": {
|
||||
"type": "string",
|
||||
"enum": ["payment", "piv_pki", "nfc_access", "mobile"],
|
||||
"description": "Optional mirror of Complete Credential CredentialRef.domain."
|
||||
},
|
||||
"issuingAuthority": {
|
||||
"$ref": "credential-authority.schema.json#/$defs/IssuingAuthorityContext"
|
||||
},
|
||||
"documentDiscriminator": {
|
||||
"type": "string",
|
||||
"description": "AAMVA document discriminator / instance id."
|
||||
},
|
||||
"firstName": { "type": "string" },
|
||||
"middleName": { "type": "string" },
|
||||
"lastName": { "type": "string" },
|
||||
"address": { "type": "string" },
|
||||
"city": { "type": "string" },
|
||||
"state": {
|
||||
"type": "string",
|
||||
"description": "Subnational issuer code (e.g. US state, CA province) — national conventions apply."
|
||||
},
|
||||
"zipCode": {
|
||||
"type": "string",
|
||||
"description": "Postal code in issuing jurisdiction format."
|
||||
},
|
||||
"dateOfBirth": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d{8}$",
|
||||
"description": "YYYYMMDD."
|
||||
},
|
||||
"expirationDate": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d{8}$",
|
||||
"description": "YYYYMMDD."
|
||||
},
|
||||
"issueDate": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d{8}$",
|
||||
"description": "YYYYMMDD."
|
||||
},
|
||||
"licenseNumber": { "type": "string" },
|
||||
"restrictions": { "type": "string" },
|
||||
"endorsements": { "type": "string" },
|
||||
"vehicleClass": { "type": "string" },
|
||||
"height": {
|
||||
"type": "string",
|
||||
"description": "e.g. FTIN or metric per jurisdiction."
|
||||
},
|
||||
"weight": {
|
||||
"type": "string",
|
||||
"description": "e.g. LBS or KG per jurisdiction."
|
||||
},
|
||||
"eyeColor": { "type": "string" },
|
||||
"hairColor": { "type": "string" },
|
||||
"sex": {
|
||||
"type": "string",
|
||||
"enum": ["M", "F", "X"],
|
||||
"description": "AAMVA allows X for unspecified/non-binary where adopted."
|
||||
},
|
||||
"extensions": {
|
||||
"type": "object",
|
||||
"description": "Jurisdiction-specific element codes, REAL ID flags, or non-AAMVA national DL profiles.",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://smoa.dev/schemas/credential-payload-agency_badge.schema.json",
|
||||
"title": "Credential sync payload — agency badge",
|
||||
"description": "Organizational photo ID / badge when format is not ICAO/AAMVA/MIL-STD-129. Often paired with nfc_access issuance.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"issuingAuthority": {
|
||||
"$ref": "credential-authority.schema.json#/$defs/IssuingAuthorityContext"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"badgeNumber": {
|
||||
"type": "string"
|
||||
},
|
||||
"photoRef": {
|
||||
"type": "string",
|
||||
"description": "URI or content-addressed ref to portrait; policy-dependent."
|
||||
},
|
||||
"extensions": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://smoa.dev/schemas/credential-payload-document.schema.json",
|
||||
"title": "Credential sync payload — ICAO / AAMVA / MIL-STD-129 (discriminated union)",
|
||||
"description": "Validates only the `payload` map inside CredentialSyncRequest — not the full request. `credentialType` lives on the request body; pick the matching branch (or validate against one schema after reading request.credentialType). oneOf may not auto-discriminate without extension keywords; prefer per-type validation in pipelines.",
|
||||
"oneOf": [
|
||||
{ "$ref": "credential-payload-icao9303_mrtd.schema.json" },
|
||||
{ "$ref": "credential-payload-aamva_dlid.schema.json" },
|
||||
{ "$ref": "credential-payload-mil_std_129.schema.json" },
|
||||
{ "$ref": "credential-payload-piv_pki.schema.json" },
|
||||
{ "$ref": "credential-payload-payment.schema.json" },
|
||||
{ "$ref": "credential-payload-nfc_access.schema.json" },
|
||||
{ "$ref": "credential-payload-mobile.schema.json" },
|
||||
{ "$ref": "credential-payload-agency_badge.schema.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://smoa.dev/schemas/credential-payload-icao9303_mrtd.schema.json",
|
||||
"title": "Credential sync payload — ICAO 9303 MRTD",
|
||||
"description": "JSON shape for CredentialSyncRequest.payload only (credentialType is on the sync request body, not duplicated here). Use when request.credentialType is icao9303_mrtd. Aligns with com.smoa.core.barcode.formats.ICAO9303Credential. Dates are MRZ-style YYMMDD.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"documentType",
|
||||
"issuingCountry",
|
||||
"surname",
|
||||
"givenNames",
|
||||
"documentNumber",
|
||||
"nationality",
|
||||
"dateOfBirth",
|
||||
"sex",
|
||||
"expirationDate"
|
||||
],
|
||||
"properties": {
|
||||
"completeCredentialDomain": {
|
||||
"type": "string",
|
||||
"enum": ["payment", "piv_pki", "nfc_access", "mobile"],
|
||||
"description": "Optional mirror of Complete Credential CredentialRef.domain."
|
||||
},
|
||||
"issuingAuthority": {
|
||||
"$ref": "credential-authority.schema.json#/$defs/IssuingAuthorityContext"
|
||||
},
|
||||
"documentType": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 2,
|
||||
"description": "MRZ document type code (e.g. P passport, I ID card, A resident alien — per ICAO 9303)."
|
||||
},
|
||||
"issuingCountry": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Z]{3}$",
|
||||
"description": "ISO 3166-1 alpha-3 issuing state."
|
||||
},
|
||||
"surname": { "type": "string" },
|
||||
"givenNames": { "type": "string" },
|
||||
"documentNumber": { "type": "string" },
|
||||
"nationality": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Z]{3}$",
|
||||
"description": "ISO 3166-1 alpha-3 nationality."
|
||||
},
|
||||
"dateOfBirth": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d{6}$",
|
||||
"description": "YYMMDD per ICAO MRZ."
|
||||
},
|
||||
"sex": {
|
||||
"type": "string",
|
||||
"enum": ["M", "F", "<"],
|
||||
"description": "M, F, or < (unspecified) per ICAO."
|
||||
},
|
||||
"expirationDate": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d{6}$",
|
||||
"description": "YYMMDD document expiry."
|
||||
},
|
||||
"personalNumber": {
|
||||
"type": "string",
|
||||
"description": "Optional national personal number field when present on MRZ."
|
||||
},
|
||||
"optionalData": {
|
||||
"type": "string",
|
||||
"description": "Optional MRZ optional data field."
|
||||
},
|
||||
"extensions": {
|
||||
"type": "object",
|
||||
"description": "National variants, visible digital seal refs, or program-specific data; validate with national profiles if needed.",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://smoa.dev/schemas/credential-payload-mil_std_129.schema.json",
|
||||
"title": "Credential sync payload — MIL-STD-129 style military ID",
|
||||
"description": "JSON shape for CredentialSyncRequest.payload only (credentialType on the sync request body). Use when request.credentialType is mil_std_129. Aligns with com.smoa.core.barcode.formats.MILSTD129Credential.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"serviceCode",
|
||||
"lastName",
|
||||
"firstName",
|
||||
"socialSecurityNumber",
|
||||
"dateOfBirth",
|
||||
"expirationDate",
|
||||
"issueDate",
|
||||
"cardNumber"
|
||||
],
|
||||
"properties": {
|
||||
"completeCredentialDomain": {
|
||||
"type": "string",
|
||||
"enum": ["payment", "piv_pki", "nfc_access", "mobile"],
|
||||
"description": "Optional mirror of Complete Credential CredentialRef.domain."
|
||||
},
|
||||
"issuingAuthority": {
|
||||
"$ref": "credential-authority.schema.json#/$defs/IssuingAuthorityContext",
|
||||
"description": "Use government.serviceBranch, ministryOrDepartment (defense), and organizationalUnit (command) for multi-country defense organizations."
|
||||
},
|
||||
"serviceCode": {
|
||||
"type": "string",
|
||||
"description": "National service branch or component code (army, navy, air force, gendarmerie, etc.)."
|
||||
},
|
||||
"rank": { "type": "string" },
|
||||
"lastName": { "type": "string" },
|
||||
"firstName": { "type": "string" },
|
||||
"middleInitial": {
|
||||
"type": "string",
|
||||
"maxLength": 4,
|
||||
"description": "Single letter or short form used on ID."
|
||||
},
|
||||
"socialSecurityNumber": {
|
||||
"type": "string",
|
||||
"description": "National military ID number, service number, or last-4 style surrogate per policy; do not use full PII where prohibited."
|
||||
},
|
||||
"dateOfBirth": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d{8}$",
|
||||
"description": "YYYYMMDD."
|
||||
},
|
||||
"expirationDate": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d{8}$",
|
||||
"description": "YYYYMMDD."
|
||||
},
|
||||
"issueDate": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d{8}$",
|
||||
"description": "YYYYMMDD."
|
||||
},
|
||||
"cardNumber": {
|
||||
"type": "string",
|
||||
"description": "Document serial or PAN printed on the card."
|
||||
},
|
||||
"unit": {
|
||||
"type": "string",
|
||||
"description": "Unit, ship, squadron, station, or major command text."
|
||||
},
|
||||
"clearanceLevel": {
|
||||
"type": "string",
|
||||
"description": "National classification marking as shown or as tokenized for sync (policy-dependent)."
|
||||
},
|
||||
"extensions": {
|
||||
"type": "object",
|
||||
"description": "Coalition badge codes, NATO stock number refs, or country-specific pay grade / MOS fields.",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://smoa.dev/schemas/credential-payload-mobile.schema.json",
|
||||
"title": "Credential sync payload — mobile / derived",
|
||||
"description": "Wallet-style or derived mobile credentials. Store wallet-specific references in extensions.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"completeCredentialDomain": {
|
||||
"type": "string",
|
||||
"const": "mobile"
|
||||
},
|
||||
"issuingAuthority": {
|
||||
"$ref": "credential-authority.schema.json#/$defs/IssuingAuthorityContext"
|
||||
},
|
||||
"walletCredentialRef": {
|
||||
"type": "string",
|
||||
"description": "Opaque reference from mobile issuance / MDM."
|
||||
},
|
||||
"extensions": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://smoa.dev/schemas/credential-payload-nfc_access.schema.json",
|
||||
"title": "Credential sync payload — NFC / physical access",
|
||||
"description": "Badge, PACS, or NFC access credential metadata. Payload often includes facility codes or QR bitmap refs in extensions.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"completeCredentialDomain": {
|
||||
"type": "string",
|
||||
"const": "nfc_access"
|
||||
},
|
||||
"issuingAuthority": {
|
||||
"$ref": "credential-authority.schema.json#/$defs/IssuingAuthorityContext"
|
||||
},
|
||||
"facilityCode": {
|
||||
"type": "string"
|
||||
},
|
||||
"badgeNumber": {
|
||||
"type": "string"
|
||||
},
|
||||
"extensions": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://smoa.dev/schemas/credential-payload-payment.schema.json",
|
||||
"title": "Credential sync payload — payment",
|
||||
"description": "Optional structured fields for credentialType payment (tokens, instruments). No card PANs in clear text; use token references and extensions per PCI/program policy.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"completeCredentialDomain": {
|
||||
"type": "string",
|
||||
"const": "payment"
|
||||
},
|
||||
"issuingAuthority": {
|
||||
"$ref": "credential-authority.schema.json#/$defs/IssuingAuthorityContext"
|
||||
},
|
||||
"paymentTokenRef": {
|
||||
"type": "string",
|
||||
"description": "Opaque token id from issuer switch / wallet."
|
||||
},
|
||||
"extensions": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://smoa.dev/schemas/credential-payload-piv_pki.schema.json",
|
||||
"title": "Credential sync payload — PIV / PKI",
|
||||
"description": "Optional structured fields for credentialType piv_pki (FIPS 201 / smart card). Card APDU and cert details are often device-specific; use extensions for national profiles.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"completeCredentialDomain": {
|
||||
"type": "string",
|
||||
"const": "piv_pki",
|
||||
"description": "Should mirror Complete Credential domain when used."
|
||||
},
|
||||
"issuingAuthority": {
|
||||
"$ref": "credential-authority.schema.json#/$defs/IssuingAuthorityContext"
|
||||
},
|
||||
"cardSerialHint": {
|
||||
"type": "string",
|
||||
"description": "Non-sensitive hint only; never full PAN/CHUID in clear if policy forbids."
|
||||
},
|
||||
"extensions": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# Tenant, API key, and unit header — threat model notes
|
||||
|
||||
## What exists today
|
||||
|
||||
- **API key** (`SMOA_API_KEY` / `X-API-Key`): shared-secret gate for `/api/v1/*` when configured. It does **not** identify a tenant or row-level security domain by itself.
|
||||
- **`smoa.tenant.require-unit`**: when true, requests may require `X-Unit` (or equivalent) so clients must declare a unit; the backend can filter **read** paths that honor the header (see `TenantFilter`). This is **not** cryptographic proof of membership in that unit.
|
||||
- **Multi-government payloads:** optional `issuingAuthority` in credential `payload_json` documents jurisdiction and org hierarchy for issuance audit; it does not enforce access control unless application logic is added.
|
||||
|
||||
## Gaps (explicit)
|
||||
|
||||
- **No** binding between API key and allowed `unit` / `holderId` / tenant id in the database layer.
|
||||
- **No** row-level security (RLS) in PostgreSQL; all rows are visible to any authenticated client unless controllers add filters.
|
||||
- **Compromise of API key** implies compromise of all data the backend stores until the key is rotated.
|
||||
|
||||
## Hardening directions
|
||||
|
||||
1. Issue **per-device or per-tenant** credentials (mTLS, JWT with `tenant_id` / `sub`, or OAuth2 client credentials) instead of a single static API key where feasible.
|
||||
2. Map principal → **allowed units** in policy service; enforce in every sync/pull handler.
|
||||
3. Enable **PostgreSQL RLS** or schema-per-tenant for strict isolation.
|
||||
4. Log and monitor **`X-Request-Id`** and principal for sync audit (already partially covered by `sync_audit_log`).
|
||||
|
||||
See also `docs/reference/GAPS-AND-INCONSISTENCIES.md` and backend `TenantFilter`.
|
||||
@@ -197,7 +197,7 @@ For detailed compliance information, see:
|
||||
|
||||
## Remaining Work
|
||||
|
||||
**See [TODO.md](../../TODO.md)** for the full checklist of remaining and optional tasks (backend, Android, iOS, Web, infrastructure, compliance, testing).
|
||||
**See [TODO.md](../../TODO.md)** (status vs external gates) and **[TASKS.md](../../TASKS.md)** (master task table with file links).
|
||||
|
||||
### Next steps (short-term)
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# End-to-end test plan (future)
|
||||
|
||||
| Flow | Preconditions | Steps | Pass criteria |
|
||||
|------|-----------------|-------|----------------|
|
||||
| Sync directory | Backend up, API key set | Create entry via POST; pull GET; app offline queue flush | Data matches |
|
||||
| Auth / RBAC | Test users seeded | Login restricted action | 403 without role |
|
||||
| Meeting join | Stub or WebRTC test peer | Join → audio route | Connection state Connected |
|
||||
|
||||
**Tooling:** Maestro, Appium, or Espresso + MockWebServer for Android; XCUITest for iOS when the native app exists.
|
||||
|
||||
**CI:** Run backend `:backend:test` and Android unit tests on every push; reserve E2E for nightly or pre-release.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Deploying the web scaffold
|
||||
|
||||
## Build
|
||||
|
||||
No bundler required. Serve the folder `docs/web-scaffold/` as static files over **HTTPS**.
|
||||
|
||||
## CORS
|
||||
|
||||
Set the backend for your web origin, e.g.:
|
||||
|
||||
```yaml
|
||||
# application-prod.yml or env
|
||||
smoa:
|
||||
cors:
|
||||
allowed-origins: "https://smoa.example.com"
|
||||
```
|
||||
|
||||
## Nginx example
|
||||
|
||||
```nginx
|
||||
location /web/ {
|
||||
alias /var/www/smoa-web-scaffold/;
|
||||
try_files $uri $uri/ /web/index.html;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
}
|
||||
```
|
||||
|
||||
## PWA
|
||||
|
||||
- `manifest.webmanifest` – install metadata (add PNG icons as needed).
|
||||
- `sw.js` – registered from `index.html` for a minimal offline shell.
|
||||
|
||||
## CI
|
||||
|
||||
See [.gitea/workflows/ci.yml](../../.gitea/workflows/ci.yml) for backend tests; add a job to `rsync` this directory to your host if desired.
|
||||
@@ -3,6 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#2563eb" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
<title>SMOA Web</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
@@ -33,6 +35,7 @@
|
||||
<p id="dirStatus"></p>
|
||||
</div>
|
||||
<pre id="out"></pre>
|
||||
<script src="offline-queue.js"></script>
|
||||
<script>
|
||||
function base() { return document.getElementById('baseUrl').value.replace(/\/$/, ''); }
|
||||
function key() { return document.getElementById('apiKey').value.trim() || null; }
|
||||
@@ -67,6 +70,9 @@
|
||||
.catch(function(e) { dirStatus('Error: ' + e.message); dirList(''); })
|
||||
.then(function() { btn.disabled = false; });
|
||||
};
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('./sw.js').catch(function () {});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "SMOA Web",
|
||||
"short_name": "SMOA",
|
||||
"description": "SMOA API client and directory pull",
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"background_color": "#0f172a",
|
||||
"theme_color": "#2563eb",
|
||||
"icons": []
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Minimal IndexedDB queue for sync operations (optional extension).
|
||||
* Usage: queueSyncItem({ method, url, headers, body }); flushSyncQueue() when online.
|
||||
*/
|
||||
(function (global) {
|
||||
const DB_NAME = 'smoa_sync_queue';
|
||||
const STORE = 'pending';
|
||||
function openDb() {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const r = indexedDB.open(DB_NAME, 1);
|
||||
r.onerror = function () { reject(r.error); };
|
||||
r.onupgradeneeded = function () {
|
||||
r.result.createObjectStore(STORE, { keyPath: 'id', autoIncrement: true });
|
||||
};
|
||||
r.onsuccess = function () { resolve(r.result); };
|
||||
});
|
||||
}
|
||||
global.queueSyncItem = function (item) {
|
||||
return openDb().then(function (db) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const tx = db.transaction(STORE, 'readwrite');
|
||||
tx.objectStore(STORE).add({ created: Date.now(), item: item });
|
||||
tx.oncomplete = function () { resolve(); };
|
||||
tx.onerror = function () { reject(tx.error); };
|
||||
});
|
||||
});
|
||||
};
|
||||
global.flushSyncQueue = function (fetchImpl) {
|
||||
var f = fetchImpl || fetch;
|
||||
return openDb().then(function (db) {
|
||||
return new Promise(function (resolve) {
|
||||
const tx = db.transaction(STORE, 'readonly');
|
||||
const req = tx.objectStore(STORE).getAll();
|
||||
req.onsuccess = function () {
|
||||
var rows = req.result || [];
|
||||
resolve(rows);
|
||||
};
|
||||
});
|
||||
}).then(function (rows) {
|
||||
return Promise.all(
|
||||
rows.map(function (row) {
|
||||
var it = row.item;
|
||||
return f(it.url, { method: it.method, headers: it.headers, body: it.body });
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
})(typeof window !== 'undefined' ? window : this);
|
||||
@@ -0,0 +1,27 @@
|
||||
/* SMOA web scaffold – minimal offline shell (cache static assets only). */
|
||||
const CACHE = 'smoa-web-v1';
|
||||
const ASSETS = ['./index.html', './manifest.webmanifest', './sw.js', './offline-queue.js'];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(caches.open(CACHE).then((c) => c.addAll(ASSETS)).then(() => self.skipWaiting()));
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((keys) =>
|
||||
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
|
||||
).then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
if (url.origin === self.location.origin && ASSETS.some((p) => url.pathname.endsWith(p.replace('./', '/')))) {
|
||||
event.respondWith(caches.match(event.request).then((r) => r || fetch(event.request)));
|
||||
return;
|
||||
}
|
||||
// API calls: network-first (no offline queue in this scaffold)
|
||||
if (event.request.method === 'GET') {
|
||||
event.respondWith(fetch(event.request).catch(() => caches.match('./index.html')));
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user