[AZ-189] [AZ-190] [AZ-191] [AZ-192] [AZ-193] [AZ-194] [AZ-195] Add e2e blackbox test suite

Made-with: Cursor
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-04-16 06:25:36 +03:00
parent 1b38e888e1
commit d320d6dd59
98 changed files with 6883 additions and 1 deletions
+144
View File
@@ -0,0 +1,144 @@
# Codebase Discovery
## Directory Tree
```
Azaion.AdminApi.sln
├── Azaion.AdminApi/ # ASP.NET Core Minimal API (entry point)
│ ├── Program.cs # App configuration + all endpoint definitions
│ └── BusinessExceptionHandler.cs
├── Azaion.Common/ # Shared library (entities, configs, DB, requests, extensions)
│ ├── Configs/
│ │ ├── ConnectionStrings.cs
│ │ ├── JwtConfig.cs
│ │ └── ResourcesConfig.cs
│ ├── Database/
│ │ ├── AzaionDb.cs
│ │ ├── AzaionDbShemaHolder.cs
│ │ └── DbFactory.cs
│ ├── Entities/
│ │ ├── User.cs # User, UserConfig, UserQueueOffsets
│ │ └── RoleEnum.cs
│ ├── Extensions/
│ │ ├── EnumExtensions.cs
│ │ ├── QueryableExtensions.cs
│ │ ├── StreamExtensions.cs
│ │ └── StringExtensions.cs
│ ├── Requests/
│ │ ├── GetResourceRequest.cs # also CheckResourceRequest + validator
│ │ ├── LoginRequest.cs
│ │ ├── RegisterUserRequest.cs # also RegisterUserValidator
│ │ ├── SetHWRequest.cs # also SetHWRequestValidator
│ │ └── SetUserQueueOffsetsRequest.cs
│ └── BusinessException.cs # also ExceptionEnum
├── Azaion.Services/ # Business logic layer
│ ├── AuthService.cs # JWT token creation, current user resolution
│ ├── UserService.cs # User CRUD, hardware validation, role management
│ ├── ResourcesService.cs # File storage, encrypted resource delivery
│ ├── Security.cs # Hashing, AES encryption/decryption
│ └── Cache.cs # In-memory caching wrapper over LazyCache
├── Azaion.Test/ # xUnit test project
│ ├── UserServiceTest.cs
│ └── SecurityTest.cs
├── env/ # Infrastructure provisioning scripts
│ ├── db/ # PostgreSQL setup + schema DDL
│ ├── api/ # API deployment scripts
│ ├── rabbit/ # RabbitMQ setup (not used by this API)
│ └── cdn/ # MinIO object storage setup
├── docker.test/ # Placeholder test Dockerfile
├── .woodpecker/ # Woodpecker CI pipeline
│ └── build-arm.yml
├── Dockerfile # Multi-stage build targeting .NET 10.0
├── deploy.cmd # Docker build + push to docker.azaion.com
└── .dockerignore
```
## Tech Stack
| Category | Technology | Version / Details |
|----------|-----------|-------------------|
| Language | C# | .NET 10.0 |
| Framework | ASP.NET Core Minimal API | net10.0 SDK |
| Database | PostgreSQL | via linq2db 5.4.1 + Npgsql 10.0.1 |
| Auth | JWT Bearer | Microsoft.AspNetCore.Authentication.JwtBearer 10.0.3 |
| Token Generation | System.IdentityModel.Tokens.Jwt | 7.1.2 |
| Validation | FluentValidation | 11.3.0 (API) / 11.10.0 (Common) |
| Caching | LazyCache | 2.4.0 |
| Logging | Serilog | 4.1.0 (Console + File sinks) |
| API Docs | Swagger / Swashbuckle | 10.1.4 |
| Encryption | AES-256-CBC | System.Security.Cryptography (built-in) |
| Password Hashing | SHA-384 | System.Security.Cryptography (built-in) |
| Serialization | Newtonsoft.Json | 13.0.1 |
| Testing | xUnit 2.9.2 + FluentAssertions 6.12.2 | Microsoft.NET.Test.Sdk 17.11.1 |
| CI/CD | Woodpecker CI | ARM64 build pipeline |
| Container | Docker | Multi-stage, .NET 10.0 base images |
| Container Registry | docker.azaion.com | Private registry |
## Dependency Graph
### Project-Level
```mermaid
graph TD
Common["Azaion.Common"]
Services["Azaion.Services"]
AdminApi["Azaion.AdminApi"]
Test["Azaion.Test"]
Services --> Common
AdminApi --> Common
AdminApi --> Services
Test --> Services
```
### Module-Level (Topological Order)
**Tier 0 — Leaf modules (no internal dependencies):**
1. `Common/Extensions/EnumExtensions`
2. `Common/Extensions/StringExtensions`
3. `Common/Extensions/StreamExtensions`
4. `Common/Extensions/QueryableExtensions`
5. `Common/Entities/RoleEnum`
6. `Common/Configs/ConnectionStrings`
7. `Common/Configs/JwtConfig`
8. `Common/Configs/ResourcesConfig`
9. `Common/Requests/LoginRequest`
**Tier 1 — Depends on Tier 0:**
10. `Common/Entities/User` → RoleEnum
11. `Common/BusinessException` → EnumExtensions
12. `Common/Requests/RegisterUserRequest` → RoleEnum, BusinessException
13. `Common/Requests/GetResourceRequest` → BusinessException
14. `Common/Requests/SetHWRequest` → BusinessException
**Tier 2 — Depends on Tier 0-1:**
15. `Common/Requests/SetUserQueueOffsetsRequest` → User
16. `Common/Database/AzaionDb` → User
17. `Common/Database/AzaionDbSchemaHolder` → User, RoleEnum, StringExtensions
18. `Common/Database/DbFactory` → AzaionDb, AzaionDbSchemaHolder, ConnectionStrings
**Tier 3 — Services (depends on Common):**
19. `Services/Security` → (standalone cryptographic utilities)
20. `Services/Cache` → (standalone caching wrapper)
21. `Services/UserService` → DbFactory, User, BusinessException, Security, Cache, QueryableExtensions, Requests
22. `Services/AuthService` → JwtConfig, User, IUserService
23. `Services/ResourcesService` → ResourcesConfig, BusinessException, Security (EncryptTo)
**Tier 4 — API + Exception Handler:**
24. `AdminApi/BusinessExceptionHandler` → BusinessException
25. `AdminApi/Program` → all services, configs, entities, requests (entry point)
**Tier 5 — Tests:**
26. `Test/SecurityTest` → Security, StreamExtensions
27. `Test/UserServiceTest` → UserService, DbFactory, Cache, ConnectionStrings
## Entry Points
- `Azaion.AdminApi/Program.cs` — single API entry point, top-level statements
## Test Structure
- Framework: xUnit
- Location: `Azaion.Test/`
- Tests: `SecurityTest` (encrypt/decrypt round-trip), `UserServiceTest` (hardware hash check against live DB)
- Coverage: minimal — only Security encryption and one UserService integration test
+115
View File
@@ -0,0 +1,115 @@
# Verification Log
## Summary
| Metric | Count |
|--------|-------|
| Total entities verified | 87 |
| Entities flagged | 0 |
| Corrections applied | 3 |
| Remaining gaps | 0 |
| Completeness score | 27/27 modules (100%) |
## Entity Verification
All class names, method signatures, interfaces, enum values, and endpoints referenced in documentation were cross-referenced against actual source code. No hallucinated entities found.
### Classes & Interfaces Verified
- `User`, `UserConfig`, `UserQueueOffsets`, `RoleEnum`
- `BusinessException`, `ExceptionEnum` (all 10 values) ✓
- `IDbFactory`, `DbFactory`, `AzaionDb`, `AzaionDbSchemaHolder`
- `IUserService`, `UserService` (all 10 methods) ✓
- `IAuthService`, `AuthService` (2 methods) ✓
- `IResourcesService`, `ResourcesService` (5 methods) ✓
- `ICache`, `MemoryCache` (2 methods) ✓
- `Security` (5 methods) ✓
- `BusinessExceptionHandler`
- All config POCOs (`ConnectionStrings`, `JwtConfig`, `ResourcesConfig`) ✓
- All request DTOs + validators ✓
### Endpoints Verified
All 17 endpoints in `Program.cs` match documentation. Routes, HTTP methods, and authorization requirements confirmed.
## Corrections Applied
### Correction 1: `apiUploaderPolicy` is unused dead code
**Document**: `components/05_admin_api/description.md`, `architecture.md`
**Finding**: `apiUploaderPolicy` is defined in `Program.cs` (lines 55-61) and registered via `AddPolicy`, but it is **never applied to any endpoint**. No `RequireAuthorization(apiUploaderPolicy)` call exists in the codebase. It is effectively dead code.
**Action**: Added note in architecture.md and admin API component spec.
### Correction 2: `BusinessExceptionHandler` cross-project namespace
**Document**: `modules/admin_api_business_exception_handler.md`
**Finding**: `BusinessExceptionHandler.cs` resides in the `Azaion.AdminApi` project but declares `namespace Azaion.Common;`. This is an unusual cross-project namespace usage — the class lives in the API project but belongs to the Common namespace.
**Action**: Noted in module doc.
### Correction 3: Missing appsettings discovery
**Document**: `deployment/environment_strategy.md`
**Finding**: `appsettings.json` was not initially read during discovery. It reveals:
- `ResourcesConfig` defaults: `ResourcesFolder: "Content"`, `SuiteInstallerFolder: "suite"`, `SuiteStageInstallerFolder: "suite-stage"`
- `JwtConfig`: Issuer `"AzaionApi"`, Audience `"Annotators/OrangePi/Admins"`, TokenLifetimeHours `4`
- `ConnectionStrings` and `JwtConfig.Secret` are NOT in appsettings — must be provided via environment variables (correct practice for secrets)
**Action**: Updated environment strategy doc.
## Observations (not errors)
### `hardware_hash` column mismatch
The `users` table DDL includes `hardware_hash varchar(120)` but the application code does not map or use this column. The `User` entity has no `HardwareHash` property. The application stores raw hardware in `hardware` and computes hashes at runtime. The DDL column appears to be a leftover from an earlier design.
### No UNIQUE constraint on `email`
The DDL (`env/db/02_structure.sql`) does not include a UNIQUE constraint on the `email` column. Uniqueness is enforced at the application level in `UserService.RegisterUser` (check-then-insert pattern), which is susceptible to race conditions.
### Test credentials in source
`UserServiceTest.cs` contains hardcoded PostgreSQL credentials for a remote database server. These should be in a test configuration file or environment variables.
### JWT Audience reveals system scope
The JWT audience value `"Annotators/OrangePi/Admins"` reveals that the system serves annotators (operators), OrangePi/CompanionPC devices, and administrators — consistent with the `RoleEnum` definitions.
## Completeness Check
| Module (source file) | Module Doc | Component |
|---------------------|-----------|-----------|
| Common/Extensions/EnumExtensions.cs | ✓ | Common Helpers |
| Common/Extensions/StringExtensions.cs | ✓ | Common Helpers |
| Common/Extensions/StreamExtensions.cs | ✓ | Common Helpers |
| Common/Extensions/QueryableExtensions.cs | ✓ | Common Helpers |
| Common/Entities/RoleEnum.cs | ✓ | 01 Data Layer |
| Common/Entities/User.cs | ✓ | 01 Data Layer |
| Common/Configs/ConnectionStrings.cs | ✓ | 01 Data Layer |
| Common/Configs/JwtConfig.cs | ✓ | 01 Data Layer |
| Common/Configs/ResourcesConfig.cs | ✓ | 01 Data Layer |
| Common/Database/AzaionDb.cs | ✓ | 01 Data Layer |
| Common/Database/AzaionDbSchemaHolder.cs | ✓ | 01 Data Layer |
| Common/Database/DbFactory.cs | ✓ | 01 Data Layer |
| Common/BusinessException.cs | ✓ | Common Helpers |
| Common/Requests/LoginRequest.cs | ✓ | 02 User Management |
| Common/Requests/RegisterUserRequest.cs | ✓ | 02 User Management |
| Common/Requests/GetResourceRequest.cs | ✓ | 04 Resource Management |
| Common/Requests/SetHWRequest.cs | ✓ | 02 User Management |
| Common/Requests/SetUserQueueOffsetsRequest.cs | ✓ | 02 User Management |
| Services/Security.cs | ✓ | 03 Auth & Security |
| Services/Cache.cs | ✓ | 01 Data Layer |
| Services/UserService.cs | ✓ | 02 User Management |
| Services/AuthService.cs | ✓ | 03 Auth & Security |
| Services/ResourcesService.cs | ✓ | 04 Resource Management |
| AdminApi/BusinessExceptionHandler.cs | ✓ | 05 Admin API |
| AdminApi/Program.cs | ✓ | 05 Admin API |
| Test/SecurityTest.cs | ✓ | Tests |
| Test/UserServiceTest.cs | ✓ | Tests |
All 27 modules are covered. No gaps.
## Consistency Check
- Component docs agree with architecture doc ✓
- Flow diagrams match component interfaces ✓
- Data model matches entity definitions ✓
- Deployment docs match Dockerfile and CI config ✓
+117
View File
@@ -0,0 +1,117 @@
# Azaion Admin API — Documentation Report
## Executive Summary
The Azaion Admin API is a .NET 10.0 Minimal API serving as the backend management service for the Azaion Suite annotation platform. The codebase was fully documented bottom-up: 27 source modules across 4 .NET projects, assembled into 5 logical components, with system-level architecture, 7 flows, data model, and deployment documentation produced. The system manages users, authenticates via JWT, binds software to hardware fingerprints, and distributes AES-encrypted resources.
## Problem Statement
The Azaion annotation platform requires centralized control over who can use its desktop software, on which physical machines, and with what permissions. The Admin API solves this by providing user lifecycle management with role-based access, hardware fingerprint binding to prevent unauthorized redistribution, and per-user encrypted resource delivery. It serves both human administrators (via an admin web panel) and desktop client applications.
## Architecture Overview
The API follows a layered architecture: Data Layer (PostgreSQL via linq2db) → Services (UserService, AuthService, ResourcesService) → Minimal API endpoints. Components communicate via direct DI injection within a single process.
**Technology stack**: C# / .NET 10.0, ASP.NET Core Minimal API, PostgreSQL (linq2db 5.4.1 + Npgsql), JWT Bearer auth, AES-256-CBC encryption, Serilog logging, Swagger docs.
**Deployment**: Single Docker container on self-hosted Linux server, ARM64, Woodpecker CI, private registry.
## Component Summary
| # | Component | Purpose | Dependencies |
|---|-----------|---------|-------------|
| 01 | Data Layer | DB access, ORM mapping, entities, configs, caching | None (leaf) |
| 02 | User Management | User CRUD, hardware binding, role management | 01, 03 |
| 03 | Auth & Security | JWT tokens, password hashing, AES encryption | 01 |
| 04 | Resource Management | File upload/download, encrypted delivery | 01, 03 |
| 05 | Admin API | 17 HTTP endpoints, middleware, DI composition | 01, 02, 03, 04 |
**Implementation order**:
1. Phase 1: Data Layer (01), Auth & Security (03) — no dependencies, can be parallel
2. Phase 2: User Management (02), Resource Management (04) — depend on Phase 1
3. Phase 3: Admin API (05) — composition root, depends on all
## System Flows
| Flow | Description | Key Components |
|------|-------------|---------------|
| F1: User Login | Email/password validation → JWT token | API, User Mgmt, Auth |
| F2: User Registration | Admin creates user with role | API, User Mgmt |
| F3: Encrypted Resource Download | Hardware check → key derivation → AES encrypt → stream | API, Auth, User Mgmt, Resource Mgmt |
| F4: Hardware Check | First-time binding or hash comparison | API, Auth, User Mgmt |
| F5: Resource Upload | File save to server filesystem | API, Resource Mgmt |
| F6: Installer Download | Latest `AzaionSuite.Iterative*` file delivery | API, Auth, Resource Mgmt |
| F7: User Management CRUD | Role change, enable/disable, delete | API, User Mgmt |
## Risk Summary
| Level | Count | Key Risks |
|-------|-------|-----------|
| High | 2 | SHA-384 password hashing without per-user salt; no path traversal protection on resource endpoints |
| Medium | 4 | No UNIQUE constraint on email; hardcoded credentials in test; no rate limiting on login; encryption in memory limits file size |
| Low | 3 | `apiUploaderPolicy` dead code; `hardware_hash` unused DB column; no health check endpoint |
## Test Coverage
| Component | Existing Tests | Coverage |
|-----------|---------------|----------|
| Data Layer | 0 | None |
| User Management | 1 (integration, live DB) | `CheckHardwareHash` only |
| Auth & Security | 2 (encrypt/decrypt) | AES round-trip only |
| Resource Management | 0 | None |
| Admin API | 0 | None |
**Overall acceptance criteria coverage**: 2 / 28 ACs covered by existing tests (7%)
## Key Decisions (from code analysis)
| # | Decision | Rationale | Alternatives Rejected |
|---|----------|-----------|----------------------|
| 1 | Minimal API over Controllers | Small endpoint count, minimal boilerplate | MVC Controllers |
| 2 | Read/Write DB connection separation | Enforce privilege levels | Single connection with app-level guards |
| 3 | Per-user resource encryption | Bind resources to specific user + hardware | Server-side access control only |
| 4 | Hardware fingerprint binding | Prevent software redistribution | License keys, online activation |
| 5 | linq2db over EF Core | Lightweight, no migration overhead | Entity Framework Core |
## Open Questions
| # | Question | Impact | Assigned To |
|---|----------|--------|-------------|
| 1 | Should `email` column have a UNIQUE constraint? | Race condition in registration could create duplicates | DBA / Developer |
| 2 | Is the `hardware_hash` DB column intentionally unused? | Dead column wastes space, confuses schema readers | Developer |
| 3 | Should password hashing be upgraded to bcrypt/Argon2? | Current SHA-384 without salt is weak against rainbow tables | Security review |
| 4 | Should path traversal protection be added to resource endpoints? | `dataFolder` parameter allows arbitrary filesystem access | Security review |
| 5 | Should `apiUploaderPolicy` be removed or applied? | Dead code that suggests incomplete feature | Product owner |
## Artifact Index
| File | Description |
|------|-------------|
| `_docs/00_problem/problem.md` | Problem statement |
| `_docs/00_problem/restrictions.md` | Technical and operational constraints |
| `_docs/00_problem/acceptance_criteria.md` | 28 acceptance criteria derived from code |
| `_docs/00_problem/input_data/data_parameters.md` | DB schema, API schemas, config schemas |
| `_docs/00_problem/security_approach.md` | Authentication, authorization, encryption details |
| `_docs/01_solution/solution.md` | Solution description with architecture table |
| `_docs/02_document/00_discovery.md` | Codebase discovery (tree, tech stack, dependency graph) |
| `_docs/02_document/modules/` | 27 individual module documentation files |
| `_docs/02_document/components/01_data_layer/description.md` | Data Layer component spec |
| `_docs/02_document/components/02_user_management/description.md` | User Management component spec |
| `_docs/02_document/components/03_auth_and_security/description.md` | Auth & Security component spec |
| `_docs/02_document/components/04_resource_management/description.md` | Resource Management component spec |
| `_docs/02_document/components/05_admin_api/description.md` | Admin API component spec |
| `_docs/02_document/common-helpers/01_helper_extensions.md` | Extensions helper doc |
| `_docs/02_document/common-helpers/02_helper_business_exception.md` | BusinessException helper doc |
| `_docs/02_document/architecture.md` | System architecture (8 sections, 5 ADRs) |
| `_docs/02_document/system-flows.md` | 7 system flows with sequence diagrams |
| `_docs/02_document/data_model.md` | Data model with ERD, ORM mapping, observations |
| `_docs/02_document/deployment/containerization.md` | Docker multi-stage build |
| `_docs/02_document/deployment/ci_cd_pipeline.md` | Woodpecker CI pipeline |
| `_docs/02_document/deployment/environment_strategy.md` | Environment config strategy |
| `_docs/02_document/deployment/observability.md` | Logging and monitoring status |
| `_docs/02_document/diagrams/components.md` | Component relationship diagram (Mermaid) |
| `_docs/02_document/diagrams/flows/flow_login.md` | Login flow diagram |
| `_docs/02_document/diagrams/flows/flow_encrypted_resource_download.md` | Encrypted download flow diagram |
| `_docs/02_document/diagrams/flows/flow_hardware_check.md` | Hardware check flowchart |
| `_docs/02_document/04_verification_log.md` | Verification results (87 entities, 3 corrections) |
| `_docs/02_document/FINAL_report.md` | This report |
+163
View File
@@ -0,0 +1,163 @@
# Azaion Admin API — Architecture
## 1. System Context
**Problem being solved**: Azaion Suite requires a centralized admin API to manage users, assign roles, bind hardware to user accounts, and securely distribute encrypted software resources (DLLs, AI models, installers) to authorized devices.
**System boundaries**:
- **Inside**: User management, authentication (JWT), role-based authorization, file-based resource storage with per-user AES encryption, hardware fingerprint validation.
- **Outside**: Client applications (Azaion Suite desktop app, admin web panel at admin.azaion.com), PostgreSQL database, server filesystem for resource storage.
**External systems**:
| System | Integration Type | Direction | Purpose |
|--------|-----------------|-----------|---------|
| PostgreSQL | Database (linq2db) | Both | User data persistence |
| Server filesystem | File I/O | Both | Resource file storage and retrieval |
| Azaion Suite client | REST API | Inbound | Resource download, hardware check, login |
| Admin web panel (admin.azaion.com) | REST API | Inbound | User management, resource upload |
## 2. Technology Stack
| Layer | Technology | Version | Rationale |
|-------|-----------|---------|-----------|
| Language | C# | .NET 10.0 | Modern, cross-platform, strong typing |
| Framework | ASP.NET Core Minimal API | 10.0 | Lightweight, minimal boilerplate |
| Database | PostgreSQL | (server-side) | Open-source, robust relational DB |
| ORM | linq2db | 5.4.1 | Lightweight, LINQ-native, no migrations overhead |
| Cache | LazyCache (in-memory) | 2.4.0 | Simple async caching for user lookups |
| Auth | JWT Bearer | 10.0.3 | Stateless token authentication |
| Validation | FluentValidation | 11.3.0 / 11.10.0 | Declarative request validation |
| Logging | Serilog | 4.1.0 | Structured logging (console + file) |
| API Docs | Swashbuckle (Swagger) | 10.1.4 | OpenAPI specification |
| Serialization | Newtonsoft.Json | 13.0.1 | JSON for DB field mapping and responses |
| Container | Docker | .NET 10.0 images | Multi-stage build, ARM64 support |
| CI/CD | Woodpecker CI | — | Branch-based ARM64 builds |
| Registry | docker.azaion.com | — | Private container registry |
## 3. Deployment Model
**Environments**: Development (local), Production (Linux server)
**Infrastructure**:
- Self-hosted Linux server (evidenced by `env/` provisioning scripts for Debian/Ubuntu)
- Docker containerization with private registry (`docker.azaion.com`, `localhost:5000`)
- No orchestration (single container deployment via `deploy.cmd`)
**Environment-specific configuration**:
| Config | Development | Production |
|--------|-------------|------------|
| Database | Local PostgreSQL (port 4312) | Remote PostgreSQL (same custom port) |
| Secrets | Environment variables (`ASPNETCORE_*`) | Environment variables |
| Logging | Console + file | Console + rolling file (`logs/log.txt`) |
| Swagger | Enabled | Disabled |
| CORS | Same as prod | `admin.azaion.com` |
## 4. Data Model Overview
**Core entities**:
| Entity | Description | Owned By Component |
|--------|-------------|--------------------|
| User | System user with email, password hash, hardware binding, role, config | 01 Data Layer |
| UserConfig | JSON-serialized per-user configuration (queue offsets) | 01 Data Layer |
| RoleEnum | Authorization role hierarchy (None → ApiAdmin) | 01 Data Layer |
| ExceptionEnum | Business error code catalog | Common Helpers |
**Key relationships**:
- User → RoleEnum: each user has exactly one role
- User → UserConfig: optional 1:1 JSON field containing queue offsets
**Data flow summary**:
- Client → API → UserService → PostgreSQL: user CRUD operations
- Client → API → ResourcesService → Filesystem: resource upload/download
- Client → API → Security → ResourcesService: encrypted resource retrieval (key derived from user credentials + hardware)
## 5. Integration Points
### Internal Communication
| From | To | Protocol | Pattern | Notes |
|------|----|----------|---------|-------|
| Admin API | User Management | Direct DI call | Request-Response | Scoped service injection |
| Admin API | Auth & Security | Direct DI call | Request-Response | Scoped service injection |
| Admin API | Resource Management | Direct DI call | Request-Response | Scoped service injection |
| User Management | Data Layer | Direct DI call | Request-Response | Singleton DbFactory |
| Auth & Security | User Management | Direct DI call | Request-Response | IUserService.GetByEmail |
### External Integrations
| External System | Protocol | Auth | Rate Limits | Failure Mode |
|----------------|----------|------|-------------|--------------|
| PostgreSQL | TCP (Npgsql) | Username/password | None configured | Exception propagation |
| Filesystem | OS I/O | OS-level permissions | None | Exception propagation |
## 6. Non-Functional Requirements
| Requirement | Target | Measurement | Priority |
|------------|--------|-------------|----------|
| Max upload size | 200 MB | Kestrel MaxRequestBodySize | High |
| File encryption | AES-256-CBC | Per-resource | High |
| Password hashing | SHA-384 | Per-user | Medium |
| Cache TTL | 4 hours | User entity cache | Low |
No explicit availability, latency, throughput, or recovery targets found in the codebase.
## 7. Security Architecture
**Authentication**: JWT Bearer tokens (HMAC-SHA256 signed, validated for issuer/audience/lifetime/signing key).
**Authorization**: Role-based (RBAC) via ASP.NET Core authorization policies:
- `apiAdminPolicy` — requires `ApiAdmin` role
- `apiUploaderPolicy` — requires `ResourceUploader` or `ApiAdmin` (defined but never applied to any endpoint)
- General `[Authorize]` — any authenticated user
**Data protection**:
- At rest: Resources encrypted with AES-256-CBC using per-user derived key (email + password + hardware hash)
- In transit: HTTPS (assumed, not enforced in code)
- Secrets management: Environment variables (`ASPNETCORE_*` prefix)
**Audit logging**: No explicit audit trail. Serilog logs business exceptions (WARN) and general events (INFO).
## 8. Key Architectural Decisions
### ADR-001: Minimal API over Controllers
**Context**: API has ~17 endpoints with simple request/response patterns.
**Decision**: Use ASP.NET Core Minimal API with top-level statements instead of MVC controllers.
**Consequences**: All endpoints in a single `Program.cs`. Simple for small APIs but could become unwieldy as endpoints grow.
### ADR-002: Read/Write Database Connection Separation
**Context**: Needed different privilege levels for read vs. write operations.
**Decision**: `DbFactory` maintains two connection strings — a read-only one (`AzaionDb`) and an admin one (`AzaionDbAdmin`) — with separate `Run` and `RunAdmin` methods.
**Consequences**: Write operations are explicitly gated through `RunAdmin`. Prevents accidental writes through the reader connection. Requires maintaining two DB users with different privileges.
### ADR-003: Per-User Resource Encryption
**Context**: Resources (DLLs, AI models) must be delivered only to authorized hardware.
**Decision**: Resources are encrypted at download time using AES-256-CBC with a key derived from the user's email, password, and hardware hash. The client must know all three to decrypt.
**Consequences**: Strong per-user binding. However, encryption happens in memory (MemoryStream), which limits practical file sizes. Key derivation is deterministic — same inputs always produce the same key.
### ADR-004: Hardware Fingerprint Binding
**Context**: Resources should only be usable on a specific physical machine.
**Decision**: On first resource access, the user's hardware fingerprint string is stored. Subsequent accesses compare the hash of the provided hardware against the stored value.
**Consequences**: Ties resources to a single device. Hardware changes require admin intervention to reset. The raw hardware string is stored in the DB; only the hash is compared.
### ADR-005: linq2db over Entity Framework
**Context**: Needed a lightweight ORM for PostgreSQL.
**Decision**: Use linq2db instead of Entity Framework Core.
**Consequences**: No migration framework — schema managed via SQL scripts (`env/db/`). Lighter runtime footprint. Manual mapping configuration in `AzaionDbSchemaHolder`.
@@ -0,0 +1,17 @@
# Common Helper: Extensions
Shared utility extensions used across multiple components.
## Modules
- `EnumExtensions` — enum description/attribute extraction (used by BusinessException)
- `StringExtensions` — PascalCase → snake_case conversion (used by AzaionDbSchemaHolder)
- `StreamExtensions` — Stream → string conversion (used by SecurityTest)
- `QueryableExtensions` — conditional LINQ Where filter (used by UserService)
## Consumers
| Helper | Used By Components |
|--------|-------------------|
| `EnumExtensions.GetDescriptions` | Data Layer (BusinessException) |
| `StringExtensions.ToSnakeCase` | Data Layer (AzaionDbSchemaHolder) |
| `StreamExtensions.ConvertToString` | Tests |
| `QueryableExtensions.WhereIf` | User Management (UserService) |
@@ -0,0 +1,25 @@
# Common Helper: BusinessException
Domain exception type with catalog of business error codes (`ExceptionEnum`).
## Error Codes
| Code | Value | Message |
|------|-------|---------|
| NoEmailFound | 10 | No such email found |
| EmailExists | 20 | Email already exists |
| WrongPassword | 30 | Passwords do not match |
| PasswordLengthIncorrect | 32 | Password should be at least 8 characters |
| EmailLengthIncorrect | 35 | Email is empty or invalid |
| WrongEmail | 37 | (no description) |
| HardwareIdMismatch | 40 | Hardware mismatch |
| BadHardware | 45 | Hardware should be not empty |
| WrongResourceName | 50 | Wrong resource file name |
| NoFileProvided | 60 | No file provided |
## Consumers
| Component | Usage |
|-----------|-------|
| User Management | Throws for auth/validation errors |
| Resource Management | Throws for missing files |
| Admin API | BusinessExceptionHandler catches and serializes |
| Request Validators | Reference error codes in `.WithErrorCode()` |
@@ -0,0 +1,173 @@
# Data Layer
## 1. High-Level Overview
**Purpose**: Provides database access, ORM mapping, entity definitions, configuration binding, and in-memory caching for the entire application.
**Architectural Pattern**: Repository/Factory — `DbFactory` creates short-lived `AzaionDb` connections with a read/write separation pattern.
**Upstream dependencies**: None (leaf component).
**Downstream consumers**: User Management, Authentication & Security, Resource Management.
## 2. Internal Interfaces
### Interface: IDbFactory
| Method | Input | Output | Async | Error Types |
|--------|-------|--------|-------|-------------|
| `Run<T>` | `Func<AzaionDb, Task<T>>` | `T` | Yes | `ArgumentException` (empty conn string) |
| `Run` | `Func<AzaionDb, Task>` | void | Yes | `ArgumentException` |
| `RunAdmin` | `Func<AzaionDb, Task>` | void | Yes | `ArgumentException` |
### Interface: ICache
| Method | Input | Output | Async | Error Types |
|--------|-------|--------|-------|-------------|
| `GetFromCacheAsync<T>` | `string key, Func<Task<T>>, TimeSpan?` | `T` | Yes | None |
| `Invalidate` | `string key` | void | No | None |
### Entities
```
User:
Id: Guid (PK)
Email: string (required)
PasswordHash: string (required)
Hardware: string? (optional)
Role: RoleEnum (required)
CreatedAt: DateTime (required)
LastLogin: DateTime? (optional)
UserConfig: UserConfig? (optional, JSON-serialized)
IsEnabled: bool (required)
UserConfig:
QueueOffsets: UserQueueOffsets? (optional)
UserQueueOffsets:
AnnotationsOffset: ulong
AnnotationsConfirmOffset: ulong
AnnotationsCommandsOffset: ulong
RoleEnum: None=0, Operator=10, Validator=20, CompanionPC=30, Admin=40, ResourceUploader=50, ApiAdmin=1000
```
### Configuration POCOs
```
ConnectionStrings:
AzaionDb: string — read-only connection string
AzaionDbAdmin: string — admin (read/write) connection string
JwtConfig:
Issuer: string
Audience: string
Secret: string
TokenLifetimeHours: double
ResourcesConfig:
ResourcesFolder: string
SuiteInstallerFolder: string
SuiteStageInstallerFolder: string
```
## 3. External API Specification
N/A — internal component.
## 4. Data Access Patterns
### Queries
| Query | Frequency | Hot Path | Index Needed |
|-------|-----------|----------|--------------|
| `SELECT * FROM users WHERE email = ?` | High | Yes | Yes (email) |
| `SELECT * FROM users` with optional filters | Medium | No | No |
| `UPDATE users SET ... WHERE email = ?` | Medium | No | No |
| `INSERT INTO users` | Low | No | No |
| `DELETE FROM users WHERE email = ?` | Low | No | No |
### Caching Strategy
| Data | Cache Type | TTL | Invalidation |
|------|-----------|-----|-------------|
| User by email | In-memory (LazyCache) | 4 hours | On hardware update, queue offset update, hardware check |
### Storage Estimates
| Table | Est. Row Count (1yr) | Row Size | Total Size | Growth Rate |
|-------|---------------------|----------|------------|-------------|
| `users` | 1001000 | ~500 bytes | ~500 KB | Low |
### Data Management
**Seed data**: Default admin user (`admin@azaion.com`, `ApiAdmin` role) and uploader user (`uploader@azaion.com`, `ResourceUploader` role) — see `env/db/02_structure.sql`.
**Rollback**: Standard PostgreSQL transactions; linq2db creates a new connection per `Run`/`RunAdmin` call.
## 5. Implementation Details
**State Management**: Stateless factory pattern. `DbFactory` is a singleton holding pre-built `DataOptions`. Cache state is in-memory per process.
**Key Dependencies**:
| Library | Version | Purpose |
|---------|---------|---------|
| linq2db | 5.4.1 | ORM for PostgreSQL access |
| Npgsql | 10.0.1 | PostgreSQL ADO.NET provider |
| LazyCache | 2.4.0 | In-memory cache with async support |
| Newtonsoft.Json | 13.0.1 | JSON serialization for UserConfig |
**Error Handling Strategy**:
- `DbFactory.LoadOptions` throws `ArgumentException` on empty connection strings (fail-fast at startup).
- Database exceptions from linq2db/Npgsql propagate unhandled to callers.
## 6. Extensions and Helpers
| Helper | Purpose | Used By |
|--------|---------|---------|
| `StringExtensions.ToSnakeCase` | PascalCase → snake_case column mapping | AzaionDbSchemaHolder |
| `EnumExtensions.GetDescriptions` | Enum → description dictionary | BusinessException |
| `QueryableExtensions.WhereIf` | Conditional LINQ filters | UserService |
## 7. Caveats & Edge Cases
**Known limitations**:
- No connection pooling configuration exposed; relies on Npgsql defaults.
- `AzaionDbSchemaHolder` mapping schema is static — adding new entities requires code changes.
- Cache TTL (4 hours) is hardcoded, not configurable.
**Potential race conditions**:
- Cache invalidation after write: there's a small window where stale data could be served between the DB write and cache invalidation.
**Performance bottlenecks**:
- `DbFactory` creates a new connection per operation. For high-throughput scenarios, connection reuse or batching would be needed.
## 8. Dependency Graph
**Must be implemented after**: None (leaf component).
**Can be implemented in parallel with**: Security & Cryptography (no dependency).
**Blocks**: User Management, Authentication, Resource Management, Admin API.
## 9. Logging Strategy
| Log Level | When | Example |
|-----------|------|---------|
| INFO | SQL trace | `SELECT * FROM users WHERE email = @p1` (via linq2db TraceLevel.Info) |
**Log format**: Plaintext SQL output to console.
**Log storage**: Console (via `Console.WriteLine` in `DbFactory.LoadOptions` trace callback).
## Modules Covered
- `Common/Configs/ConnectionStrings`
- `Common/Configs/JwtConfig`
- `Common/Configs/ResourcesConfig`
- `Common/Entities/User`
- `Common/Entities/RoleEnum`
- `Common/Database/AzaionDb`
- `Common/Database/AzaionDbSchemaHolder`
- `Common/Database/DbFactory`
- `Services/Cache`
@@ -0,0 +1,127 @@
# User Management
## 1. High-Level Overview
**Purpose**: Full user lifecycle management — registration, credential validation, hardware binding, role changes, account enable/disable, and deletion.
**Architectural Pattern**: Service layer — stateless business logic operating on the Data Layer through `IDbFactory`.
**Upstream dependencies**: Data Layer (IDbFactory, ICache, User entity), Security & Cryptography (hashing).
**Downstream consumers**: Admin API (endpoint handlers), Authentication (GetByEmail).
## 2. Internal Interfaces
### Interface: IUserService
| Method | Input | Output | Async | Error Types |
|--------|-------|--------|-------|-------------|
| `RegisterUser` | `RegisterUserRequest, CancellationToken` | void | Yes | `BusinessException(EmailExists)` |
| `ValidateUser` | `LoginRequest, CancellationToken` | `User` | Yes | `BusinessException(NoEmailFound, WrongPassword)` |
| `GetByEmail` | `string? email, CancellationToken` | `User?` | Yes | `ArgumentNullException` |
| `UpdateHardware` | `string email, string? hardware, CancellationToken` | void | Yes | None |
| `UpdateQueueOffsets` | `string email, UserQueueOffsets, CancellationToken` | void | Yes | None |
| `GetUsers` | `string? searchEmail, RoleEnum? searchRole, CancellationToken` | `IEnumerable<User>` | Yes | None |
| `CheckHardwareHash` | `User, string hardware, CancellationToken` | `string` (hash) | Yes | `BusinessException(HardwareIdMismatch)` |
| `ChangeRole` | `string email, RoleEnum, CancellationToken` | void | Yes | None |
| `SetEnableStatus` | `string email, bool, CancellationToken` | void | Yes | None |
| `RemoveUser` | `string email, CancellationToken` | void | Yes | None |
**Input DTOs**:
```
RegisterUserRequest:
Email: string (required) — validated: min 8 chars, valid email format
Password: string (required) — validated: min 8 chars
Role: RoleEnum (required)
LoginRequest:
Email: string (required)
Password: string (required)
SetHWRequest:
Email: string (required, validated: not empty)
Hardware: string? (optional — null clears hardware)
SetUserQueueOffsetsRequest:
Email: string (required)
Offsets: UserQueueOffsets (required)
```
## 3. External API Specification
N/A — exposed through Admin API component.
## 4. Data Access Patterns
### Queries
| Query | Frequency | Hot Path | Index Needed |
|-------|-----------|----------|--------------|
| User by email (cached) | High | Yes | Yes |
| User list with filters | Medium | No | No |
| User insert (registration) | Low | No | No |
| User update (hardware, role, config, status) | Medium | No | No |
| User delete | Low | No | No |
### Caching Strategy
| Data | Cache Type | TTL | Invalidation |
|------|-----------|-----|-------------|
| User by email | In-memory (via ICache) | 4 hours | After UpdateHardware, UpdateQueueOffsets, CheckHardwareHash (first login) |
## 5. Implementation Details
**State Management**: Stateless — all state in PostgreSQL + in-memory cache.
**Key Dependencies**:
| Library | Version | Purpose |
|---------|---------|---------|
| FluentValidation | 11.10.0 | Request validation (auto-discovered) |
**Error Handling Strategy**:
- Domain errors thrown as `BusinessException` with specific `ExceptionEnum` codes.
- `GetByEmail` throws `ArgumentNullException` for null/whitespace email.
- Database errors propagate from `IDbFactory`.
- Write operations use `RunAdmin` (admin connection); reads use `Run` (reader connection).
## 6. Extensions and Helpers
| Helper | Purpose | Used By |
|--------|---------|---------|
| `Security.ToHash` | Password hashing (SHA-384) | RegisterUser, ValidateUser |
| `Security.GetHWHash` | Hardware fingerprint hashing | CheckHardwareHash |
| `QueryableExtensions.WhereIf` | Conditional LINQ filters | GetUsers |
## 7. Caveats & Edge Cases
**Known limitations**:
- No pagination on `GetUsers` — returns all matching users.
- `CheckHardwareHash` auto-stores hardware on first access (no explicit admin approval step).
- `RemoveUser` is a hard delete, not soft delete.
**Potential race conditions**:
- Concurrent `RegisterUser` calls with the same email: both could pass the existence check before insert. Mitigated by database unique constraint on email (if one exists).
- `CheckHardwareHash` first-login path: concurrent requests could trigger multiple hardware updates.
**Performance bottlenecks**:
- `GetUsers` loads full user objects including `UserConfig` JSON; for large user bases, projection would be more efficient.
## 8. Dependency Graph
**Must be implemented after**: Data Layer, Security & Cryptography.
**Can be implemented in parallel with**: Resource Management.
**Blocks**: Authentication (uses `GetByEmail`), Admin API.
## 9. Logging Strategy
No explicit logging in UserService.
## Modules Covered
- `Services/UserService`
- `Common/Requests/LoginRequest`
- `Common/Requests/RegisterUserRequest`
- `Common/Requests/SetHWRequest`
- `Common/Requests/SetUserQueueOffsetsRequest`
@@ -0,0 +1,87 @@
# Authentication & Security
## 1. High-Level Overview
**Purpose**: JWT token creation/validation and cryptographic utilities (password hashing, hardware fingerprint hashing, AES file encryption/decryption).
**Architectural Pattern**: Service + static utility — `AuthService` is a DI-managed service for JWT operations; `Security` is a static class for cryptographic primitives.
**Upstream dependencies**: Data Layer (JwtConfig, IUserService for GetByEmail), ASP.NET Core (IHttpContextAccessor).
**Downstream consumers**: Admin API (token creation on login, current user resolution), User Management (password hashing, hardware hashing), Resource Management (encryption key derivation, stream encryption).
## 2. Internal Interfaces
### Interface: IAuthService
| Method | Input | Output | Async | Error Types |
|--------|-------|--------|-------|-------------|
| `GetCurrentUser` | (none — reads from HttpContext) | `User?` | Yes | None |
| `CreateToken` | `User` | `string` (JWT) | No | None |
### Static: Security
| Method | Input | Output | Description |
|--------|-------|--------|-------------|
| `ToHash` | `string` | `string` (Base64) | SHA-384 hash |
| `GetHWHash` | `string hardware` | `string` (Base64) | Salted hardware hash |
| `GetApiEncryptionKey` | `string email, string password, string? hwHash` | `string` (Base64) | Derives AES encryption key |
| `EncryptTo` | `Stream input, Stream output, string key, CancellationToken` | void | AES-256-CBC encrypt stream |
| `DecryptTo` | `Stream encrypted, Stream output, string key, CancellationToken` | void | AES-256-CBC decrypt stream |
## 3. External API Specification
N/A — exposed through Admin API.
## 4. Data Access Patterns
No direct database access. `AuthService.GetCurrentUser` delegates to `IUserService.GetByEmail`.
## 5. Implementation Details
**Algorithmic Complexity**: Encryption/decryption is O(n) where n is file size, streaming in 512 KB buffers.
**State Management**: `AuthService` is stateless (reads claims from HTTP context per request). `Security` is purely static.
**Key Dependencies**:
| Library | Version | Purpose |
|---------|---------|---------|
| System.IdentityModel.Tokens.Jwt | 7.1.2 | JWT token generation |
| Microsoft.AspNetCore.Authentication.JwtBearer | 10.0.3 | JWT middleware integration |
**Error Handling Strategy**:
- `EncryptTo` throws `ArgumentNullException` for unreadable streams or empty keys.
- JWT token creation does not throw (malformed config would cause runtime errors at middleware level).
- `GetCurrentUser` returns null if claims are missing or user not found.
## 6. Extensions and Helpers
None — `Security` itself is a utility consumed by other components.
## 7. Caveats & Edge Cases
**Known limitations**:
- Password hashing uses SHA-384 without per-user salt or key stretching. Not resistant to rainbow table attacks.
- Hardware and encryption key salts are hardcoded constants.
- `GetCurrentUserEmail` assumes `ClaimTypes.Name` is always present; accessing a missing key would throw `KeyNotFoundException`.
- AES encryption prepends IV as first 16 bytes — consumers must know this format.
**Performance bottlenecks**:
- Large file encryption loads encrypted output into `MemoryStream` before sending — high memory usage for large files.
## 8. Dependency Graph
**Must be implemented after**: Data Layer (for JwtConfig, IUserService).
**Can be implemented in parallel with**: User Management (shared dependency on Data Layer).
**Blocks**: Admin API, Resource Management (uses encryption).
## 9. Logging Strategy
No explicit logging in AuthService or Security.
## Modules Covered
- `Services/AuthService`
- `Services/Security`
@@ -0,0 +1,100 @@
# Resource Management
## 1. High-Level Overview
**Purpose**: Server-side file storage management — upload, list, download (with per-user AES encryption), folder clearing, and installer distribution.
**Architectural Pattern**: Service layer — filesystem operations with encryption applied at the service boundary.
**Upstream dependencies**: Data Layer (ResourcesConfig), Authentication & Security (encryption via Security.EncryptTo).
**Downstream consumers**: Admin API (resource endpoints).
## 2. Internal Interfaces
### Interface: IResourcesService
| Method | Input | Output | Async | Error Types |
|--------|-------|--------|-------|-------------|
| `GetInstaller` | `bool isStage` | `(string?, Stream?)` | No | None (returns nulls if not found) |
| `GetEncryptedResource` | `string? dataFolder, string fileName, string key, CancellationToken` | `Stream` | Yes | `FileNotFoundException` |
| `SaveResource` | `string? dataFolder, IFormFile data, CancellationToken` | void | Yes | `BusinessException(NoFileProvided)` |
| `ListResources` | `string? dataFolder, string? search, CancellationToken` | `IEnumerable<string>` | Yes | `DirectoryNotFoundException` |
| `ClearFolder` | `string? dataFolder` | void | No | None |
**Input DTOs**:
```
GetResourceRequest:
Password: string (required, min 8 chars)
Hardware: string (required, not empty)
FileName: string (required, not empty)
CheckResourceRequest:
Hardware: string (required)
```
## 3. External API Specification
N/A — exposed through Admin API.
## 4. Data Access Patterns
No database access. All operations are filesystem-based.
### Storage Estimates
Resources are stored as flat files in configured directories. Size depends on uploaded content (AI models, DLLs, installers — potentially hundreds of MB per file).
## 5. Implementation Details
**State Management**: Stateless — reads/writes directly to filesystem.
**Key Dependencies**: None beyond BCL (System.IO).
**Error Handling Strategy**:
- `SaveResource` throws `BusinessException(NoFileProvided)` for null uploads.
- Missing files/directories throw standard .NET I/O exceptions.
- `ClearFolder` silently returns if directory doesn't exist.
- `GetInstaller` returns `(null, null)` tuple if installer file is not found.
## 6. Extensions and Helpers
| Helper | Purpose | Used By |
|--------|---------|---------|
| `Security.EncryptTo` | AES stream encryption | GetEncryptedResource |
| `Security.GetApiEncryptionKey` | Key derivation | Admin API (before calling GetEncryptedResource) |
## 7. Caveats & Edge Cases
**Known limitations**:
- No path traversal protection: `dataFolder` parameter is concatenated directly with `ResourcesFolder`. A malicious `dataFolder` like `../../etc` could access arbitrary filesystem paths.
- `SaveResource` deletes existing file before writing — no versioning or backup.
- `GetEncryptedResource` loads the entire encrypted file into a `MemoryStream` — memory-intensive for large files.
- `ListResources` wraps a synchronous `DirectoryInfo.GetFiles` in `Task.FromResult` — not truly async.
**Performance bottlenecks**:
- Full file encryption to memory before streaming response: memory usage scales with file size.
- `ClearFolder` iterates and deletes files synchronously.
## 8. Dependency Graph
**Must be implemented after**: Data Layer (ResourcesConfig), Authentication & Security (encryption).
**Can be implemented in parallel with**: User Management.
**Blocks**: Admin API.
## 9. Logging Strategy
| Log Level | When | Example |
|-----------|------|---------|
| INFO | Successful file save | `Resource {data.FileName} Saved Successfully` |
**Log format**: String interpolation via Serilog.
**Log storage**: Console + rolling file (via Serilog configured in Program.cs).
## Modules Covered
- `Services/ResourcesService`
- `Common/Requests/GetResourceRequest` (includes CheckResourceRequest)
- `Common/Configs/ResourcesConfig`
@@ -0,0 +1,124 @@
# Admin API
## 1. High-Level Overview
**Purpose**: HTTP API entry point — configures DI, middleware pipeline, authentication, authorization, CORS, Swagger, and defines all REST endpoints using ASP.NET Core Minimal API.
**Architectural Pattern**: Composition root + Minimal API endpoints — top-level statements configure the application and map HTTP routes to service methods.
**Upstream dependencies**: User Management (IUserService), Authentication & Security (IAuthService, Security), Resource Management (IResourcesService), Data Layer (IDbFactory, ICache, configs).
**Downstream consumers**: None (top-level entry point, consumed by HTTP clients).
## 2. Internal Interfaces
### BusinessExceptionHandler
| Method | Input | Output | Async | Error Types |
|--------|-------|--------|-------|-------------|
| `TryHandleAsync` | `HttpContext, Exception, CancellationToken` | `bool` | Yes | None |
Converts `BusinessException` to HTTP 409 JSON response: `{ ErrorCode: int, Message: string }`.
## 3. External API Specification
### Authentication
| Endpoint | Method | Auth | Description |
|----------|--------|------|-------------|
| `/login` | POST | Anonymous | Validates credentials, returns JWT |
### User Management
| Endpoint | Method | Auth | Description |
|----------|--------|------|-------------|
| `/users` | POST | ApiAdmin | Creates a new user |
| `/users/current` | GET | Authenticated | Returns current user |
| `/users` | GET | ApiAdmin | Lists users (optional email/role filters) |
| `/users/hardware/set` | PUT | ApiAdmin | Sets user hardware |
| `/users/queue-offsets/set` | PUT | Authenticated | Updates queue offsets |
| `/users/{email}/set-role/{role}` | PUT | ApiAdmin | Changes user role |
| `/users/{email}/enable` | PUT | ApiAdmin | Enables user |
| `/users/{email}/disable` | PUT | ApiAdmin | Disables user |
| `/users/{email}` | DELETE | ApiAdmin | Removes user |
### Resource Management
| Endpoint | Method | Auth | Description |
|----------|--------|------|-------------|
| `/resources/{dataFolder?}` | POST | Authenticated | Uploads a file (up to 200 MB) |
| `/resources/list/{dataFolder?}` | GET | Authenticated | Lists files |
| `/resources/clear/{dataFolder?}` | POST | ApiAdmin | Clears folder |
| `/resources/get/{dataFolder?}` | POST | Authenticated | Downloads encrypted resource |
| `/resources/get-installer` | GET | Authenticated | Downloads production installer |
| `/resources/get-installer/stage` | GET | Authenticated | Downloads staging installer |
| `/resources/check` | POST | Authenticated | Validates hardware |
### Authorization Policies
- **apiAdminPolicy**: requires `ApiAdmin` role (used on most admin endpoints)
- **apiUploaderPolicy**: requires `ResourceUploader` or `ApiAdmin` role (**defined but never applied to any endpoint — dead code**)
### CORS
- Allowed origins: `https://admin.azaion.com`, `http://admin.azaion.com`
- All methods/headers, credentials allowed
## 4. Data Access Patterns
No direct data access — delegates to service components.
## 5. Implementation Details
**State Management**: Stateless — ASP.NET Core request pipeline.
**Key Dependencies**:
| Library | Version | Purpose |
|---------|---------|---------|
| Swashbuckle.AspNetCore | 10.1.4 | Swagger/OpenAPI documentation |
| FluentValidation.AspNetCore | 11.3.0 | Request validation pipeline |
| Serilog | 4.1.0 | Structured logging |
| Serilog.Sinks.Console | 6.0.0 | Console log output |
| Serilog.Sinks.File | 6.0.0 | Rolling file log output |
**Error Handling Strategy**:
- `BusinessException``BusinessExceptionHandler` → HTTP 409 with JSON body.
- `UnauthorizedAccessException` → thrown in resource endpoints when current user is null.
- `FileNotFoundException` → thrown when installer not found.
- FluentValidation errors → automatic 400 Bad Request via middleware.
- Unhandled exceptions → default ASP.NET Core exception handling.
## 6. Extensions and Helpers
None.
## 7. Caveats & Edge Cases
**Known limitations**:
- All endpoints are defined in a single `Program.cs` file — no route grouping or controller separation.
- Swagger UI only available in Development environment.
- CORS origins are hardcoded (not configurable).
- Antiforgery disabled for resource upload endpoint.
- Root URL (`/`) redirects to `/swagger`.
**Performance bottlenecks**:
- Kestrel max request body: 200 MB — allows large file uploads but could be a memory concern.
## 8. Dependency Graph
**Must be implemented after**: All other components (composition root).
**Can be implemented in parallel with**: Nothing — depends on all services.
**Blocks**: Nothing.
## 9. Logging Strategy
| Log Level | When | Example |
|-----------|------|---------|
| WARN | Business exception caught | `BusinessExceptionHandler` logs the exception |
| INFO | Serilog minimum level | General application events |
**Log format**: Serilog structured logging with context enrichment.
**Log storage**: Console + rolling file (`logs/log.txt`, daily rotation).
## Modules Covered
- `AdminApi/Program`
- `AdminApi/BusinessExceptionHandler`
+96
View File
@@ -0,0 +1,96 @@
# Azaion Admin API — Data Model
## Entity-Relationship Diagram
```mermaid
erDiagram
USERS {
uuid id PK
varchar email "unique, not null"
varchar password_hash "not null"
text hardware "nullable"
varchar hardware_hash "nullable"
varchar role "not null (text enum)"
varchar user_config "nullable (JSON)"
timestamp created_at "not null, default now()"
timestamp last_login "nullable"
bool is_enabled "not null, default true"
}
```
The system has a single table (`users`). There are no foreign key relationships.
## Table: `users`
### Columns
| Column | Type | Nullable | Default | Description |
|--------|------|----------|---------|-------------|
| `id` | `uuid` | No | (application-generated) | Primary key, `Guid.NewGuid()` |
| `email` | `varchar(160)` | No | — | Unique user identifier |
| `password_hash` | `varchar(255)` | No | — | SHA-384 hash, Base64-encoded |
| `hardware` | `text` | Yes | null | Raw hardware fingerprint string |
| `hardware_hash` | `varchar(120)` | Yes | null | Defined in DDL but not used by application code |
| `role` | `varchar(20)` | No | — | Text representation of `RoleEnum` |
| `user_config` | `varchar(512)` | Yes | null | JSON-serialized `UserConfig` object |
| `created_at` | `timestamp` | No | `now()` | Account creation time |
| `last_login` | `timestamp` | Yes | null | Last hardware check / resource access time |
| `is_enabled` | `bool` | No | `true` | Account active flag |
### ORM Mapping (linq2db)
Column names are auto-converted from PascalCase to snake_case via `AzaionDbSchemaHolder`:
- `User.PasswordHash``password_hash`
- `User.CreatedAt``created_at`
Special mappings:
- `Role`: stored as text, converted to/from `RoleEnum` via `Enum.Parse`
- `UserConfig`: stored as nullable JSON string, serialized/deserialized via `Newtonsoft.Json`
### Permissions
| Role | Privileges |
|------|-----------|
| `azaion_reader` | SELECT on `users` |
| `azaion_admin` | SELECT, INSERT, UPDATE, DELETE on `users` |
| `azaion_superadmin` | Superuser (DB owner) |
### Seed Data
Two default users (from `env/db/02_structure.sql`):
| Email | Role |
|-------|------|
| `admin@azaion.com` | `ApiAdmin` |
| `uploader@azaion.com` | `ResourceUploader` |
## Schema Migration History
Schema is managed via SQL scripts in `env/db/`:
1. `00_install.sh` — PostgreSQL installation and configuration
2. `01_permissions.sql` — Role creation (superadmin, admin, reader)
3. `02_structure.sql` — Table creation + seed data
4. `03_add_timestamp_columns.sql` — Adds `created_at`, `last_login`, `is_enabled` columns
No ORM migration framework is used. Schema changes are applied manually via SQL scripts.
## UserConfig JSON Schema
```json
{
"QueueOffsets": {
"AnnotationsOffset": 0,
"AnnotationsConfirmOffset": 0,
"AnnotationsCommandsOffset": 0
}
}
```
Stored in the `user_config` column. Deserialized to `UserConfig``UserQueueOffsets` on read. Default empty `UserConfig` is created when the field is null or empty.
## Observations
- The `hardware_hash` column exists in the DDL but is not referenced in application code. The application stores the raw hardware string in `hardware` and computes hashes at runtime.
- No unique constraint on `email` column in the DDL — uniqueness is enforced at the application level (`UserService.RegisterUser` checks for duplicates before insert).
- `user_config` is limited to `varchar(512)`, which could be insufficient if queue offsets grow or additional config fields are added.
@@ -0,0 +1,36 @@
# CI/CD Pipeline
## Woodpecker CI
### Pipeline: `.woodpecker/build-arm.yml`
**Triggers**: Push or manual trigger on branches `dev`, `stage`, `main`.
**Platform**: ARM64
**Steps**:
1. **build-push**: Uses `docker` image, builds the Dockerfile, tags based on branch, pushes to local registry.
### Tag Strategy
```
main → localhost:5000/admin:arm
stage → localhost:5000/admin:stage-arm
dev → localhost:5000/admin:dev-arm
```
### Manual Deploy
`deploy.cmd` script (for manual/local builds):
```
docker build -t docker.azaion.com/api .
docker login docker.azaion.com
docker push docker.azaion.com/api
```
## Observations
- No automated testing step in the CI pipeline (build only, no test run).
- ARM64-only builds — no x86/amd64 pipeline.
- No staging or production deployment automation beyond docker push.
- Two registries: `localhost:5000` (CI) and `docker.azaion.com` (manual deploy) — not synchronized.
@@ -0,0 +1,28 @@
# Containerization
## Dockerfile
Multi-stage build targeting .NET 10.0:
1. **Base stage** (`mcr.microsoft.com/dotnet/aspnet:10.0`): Runtime image, exposes port 8080.
2. **Build stage** (`mcr.microsoft.com/dotnet/sdk:10.0`): Restores packages, builds release configuration. Supports cross-platform builds via `$BUILDPLATFORM` and `$TARGETARCH`.
3. **Publish stage**: Publishes with `UseAppHost=false`, targets Linux with specified architecture.
4. **Final stage**: Copies published output, sets entrypoint to `dotnet Azaion.AdminApi.dll`.
## Container Registry
- Private registry: `docker.azaion.com`
- Deploy command: `docker build -t docker.azaion.com/api . && docker push docker.azaion.com/api`
- CI registry: `localhost:5000` (Woodpecker CI local registry)
## Tags
| Branch | Tag |
|--------|-----|
| `main` | `arm` |
| `dev` | `dev-arm` |
| `stage` | `stage-arm` |
## Docker Test
A placeholder `docker.test/Dockerfile` exists (`FROM alpine:latest; CMD echo hello`) — appears unused.
@@ -0,0 +1,44 @@
# Environment Strategy
## Environments
| Environment | Infrastructure | Config Source | Swagger |
|-------------|---------------|---------------|---------|
| Development | Local machine | appsettings.json / env vars | Enabled |
| Production | Linux server (self-hosted) | Environment variables | Disabled |
## Configuration
### appsettings.json Defaults
- `ResourcesConfig`: ResourcesFolder=`"Content"`, SuiteInstallerFolder=`"suite"`, SuiteStageInstallerFolder=`"suite-stage"`
- `JwtConfig`: Issuer=`"AzaionApi"`, Audience=`"Annotators/OrangePi/Admins"`, TokenLifetimeHours=`4`
- `ConnectionStrings` and `JwtConfig.Secret` are NOT in appsettings — must be provided via environment variables
Configuration is loaded via ASP.NET Core's `IConfiguration` with the following sections:
| Section | Purpose | Example Env Var |
|---------|---------|----------------|
| `ConnectionStrings.AzaionDb` | Reader DB connection | `ASPNETCORE_ConnectionStrings__AzaionDb` |
| `ConnectionStrings.AzaionDbAdmin` | Admin DB connection | `ASPNETCORE_ConnectionStrings__AzaionDbAdmin` |
| `JwtConfig.Secret` | JWT signing key | `ASPNETCORE_JwtConfig__Secret` |
| `JwtConfig.Issuer` | Token issuer | — |
| `JwtConfig.Audience` | Token audience | — |
| `JwtConfig.TokenLifetimeHours` | Token TTL | — |
| `ResourcesConfig.ResourcesFolder` | File storage root | — |
| `ResourcesConfig.SuiteInstallerFolder` | Prod installer dir | — |
| `ResourcesConfig.SuiteStageInstallerFolder` | Stage installer dir | — |
## Infrastructure Scripts (`env/`)
| Directory | Purpose |
|-----------|---------|
| `env/db/` | PostgreSQL install, role creation, schema DDL, migrations |
| `env/api/` | API server setup (Nginx reverse proxy, container management) |
| `env/rabbit/` | RabbitMQ install + config (not used by this API) |
| `env/cdn/` | MinIO object storage setup (not used by this API) |
## Database
- PostgreSQL on custom port 4312 (security through obscurity)
- Three DB roles: `azaion_superadmin` (owner), `azaion_admin` (read/write), `azaion_reader` (read-only)
- Schema managed via SQL scripts, no ORM migrations
@@ -0,0 +1,38 @@
# Observability
## Logging
| Aspect | Implementation |
|--------|---------------|
| Framework | Serilog 4.1.0 |
| Sinks | Console, Rolling File (`logs/log.txt`, daily) |
| Minimum Level | Information |
| Enrichment | `FromLogContext` |
### Log Sources
| Source | Level | Content |
|--------|-------|---------|
| BusinessExceptionHandler | WARN | Business exceptions with message |
| ResourcesService | INFO | Successful file saves |
| DbFactory (linq2db trace) | INFO | SQL query text (via `Console.WriteLine`) |
## Metrics
No metrics collection configured (no Prometheus, Application Insights, or similar).
## Health Checks
No health check endpoint configured.
## Tracing
No distributed tracing configured.
## Observations
- Logging is minimal — no structured request/response logging.
- No health check endpoint for container orchestration or load balancer probes.
- SQL trace goes directly to `Console.WriteLine`, not through Serilog.
- No log correlation (request IDs, trace IDs).
- No alerting or monitoring infrastructure.
+75
View File
@@ -0,0 +1,75 @@
# Component Diagram
```mermaid
graph TD
subgraph "Common Helpers"
EXT["Extensions<br/>(Enum, String, Stream, Queryable)"]
BEX["BusinessException<br/>(ExceptionEnum)"]
end
subgraph "01 Data Layer"
DB["AzaionDb + SchemaHolder"]
DBF["DbFactory<br/>(IDbFactory)"]
CACHE["MemoryCache<br/>(ICache)"]
ENT["User, RoleEnum"]
CFG["Configs<br/>(ConnectionStrings, JwtConfig, ResourcesConfig)"]
end
subgraph "02 User Management"
US["UserService<br/>(IUserService)"]
REQ["Request DTOs<br/>+ Validators"]
end
subgraph "03 Auth & Security"
AUTH["AuthService<br/>(IAuthService)"]
SEC["Security<br/>(static: hash, encrypt, decrypt)"]
end
subgraph "04 Resource Management"
RES["ResourcesService<br/>(IResourcesService)"]
end
subgraph "05 Admin API"
API["Program.cs<br/>(Minimal API endpoints)"]
EXH["BusinessExceptionHandler"]
end
DB --> ENT
DB --> EXT
DBF --> DB
DBF --> CFG
US --> DBF
US --> CACHE
US --> SEC
US --> BEX
US --> EXT
AUTH --> US
AUTH --> CFG
RES --> CFG
RES --> SEC
RES --> BEX
API --> US
API --> AUTH
API --> RES
API --> DBF
API --> CACHE
EXH --> BEX
```
## Component Summary
| # | Component | Modules | Purpose |
|---|-----------|---------|---------|
| 01 | Data Layer | 9 | DB access, entities, configs, caching |
| 02 | User Management | 5 | User CRUD, hardware binding, role management |
| 03 | Auth & Security | 2 | JWT tokens, cryptographic utilities |
| 04 | Resource Management | 3 | File upload/download/encryption |
| 05 | Admin API | 2 | HTTP endpoints, middleware, DI composition |
| — | Common Helpers | 6 | Extensions, BusinessException |
| — | Tests | 2 | SecurityTest, UserServiceTest |
**Total**: 27 modules across 5 components + common helpers + tests.
@@ -0,0 +1,29 @@
# Flow: Encrypted Resource Download
```mermaid
sequenceDiagram
participant Client
participant API as Admin API
participant Auth as AuthService
participant US as UserService
participant Sec as Security
participant RS as ResourcesService
participant FS as Filesystem
Client->>API: POST /resources/get {password, hardware, fileName}
API->>Auth: GetCurrentUser()
Auth-->>API: User
API->>US: CheckHardwareHash(user, hardware)
US->>Sec: GetHWHash(hardware)
Sec-->>US: hash
US-->>API: hwHash
API->>Sec: GetApiEncryptionKey(email, password, hwHash)
Sec-->>API: AES key
API->>RS: GetEncryptedResource(folder, fileName, key)
RS->>FS: Read file
FS-->>RS: FileStream
RS->>Sec: EncryptTo(stream, key) [AES-256-CBC]
Sec-->>RS: Encrypted MemoryStream
RS-->>API: Stream
API-->>Client: 200 OK (application/octet-stream)
```
@@ -0,0 +1,17 @@
# Flow: Hardware Check
```mermaid
flowchart TD
Start([POST /resources/check]) --> GetUser[AuthService.GetCurrentUser]
GetUser --> CheckNull{User null?}
CheckNull -->|Yes| Unauth[401 Unauthorized]
CheckNull -->|No| CheckHW[UserService.CheckHardwareHash]
CheckHW --> HasHW{User has stored hardware?}
HasHW -->|No - first time| StoreHW[Store hardware string in DB]
StoreHW --> UpdateLogin[Update last_login]
UpdateLogin --> ReturnHash([Return hwHash])
HasHW -->|Yes| CompareHash{Hashes match?}
CompareHash -->|Yes| UpdateLogin2[Update last_login]
UpdateLogin2 --> ReturnHash2([Return hwHash])
CompareHash -->|No| Mismatch([409: HardwareIdMismatch])
```
@@ -0,0 +1,20 @@
# Flow: User Login
```mermaid
sequenceDiagram
participant Client
participant API as Admin API
participant US as UserService
participant DB as PostgreSQL
participant Auth as AuthService
Client->>API: POST /login {email, password}
API->>US: ValidateUser(request)
US->>DB: SELECT user WHERE email = ?
DB-->>US: User record
US->>US: Compare password hash (SHA-384)
US-->>API: User entity
API->>Auth: CreateToken(user)
Auth-->>API: JWT string (HMAC-SHA256)
API-->>Client: 200 OK {token}
```
@@ -0,0 +1,39 @@
# Module: Azaion.AdminApi.BusinessExceptionHandler
## Purpose
ASP.NET Core `IExceptionHandler` that intercepts `BusinessException` instances and converts them to structured HTTP 409 (Conflict) JSON responses.
## Public Interface
| Method | Signature | Description |
|--------|-----------|-------------|
| `TryHandleAsync` | `ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken ct)` | Handles `BusinessException`, returns false for other exception types |
## Internal Logic
- Checks if the exception is a `BusinessException` via pattern matching.
- If not, returns `false` to let other handlers process it.
- If yes: logs as warning, sets HTTP 409 status, serializes `{ ErrorCode, Message }` as JSON via `Newtonsoft.Json`.
## Dependencies
- `BusinessException`, `ExceptionEnum`
- `ILogger<BusinessExceptionHandler>`
- `Newtonsoft.Json`
## Consumers
- Registered in `Program.cs` via `builder.Services.AddExceptionHandler<BusinessExceptionHandler>()`
- Activated by `app.UseExceptionHandler(_ => {})` middleware
## Data Models
Response body: `{ ErrorCode: ExceptionEnum, Message: string }`.
## Configuration
None.
## External Integrations
None.
## Security
Exposes error codes and messages to the client. Messages are user-facing strings from `ExceptionEnum` descriptions.
## Tests
None.
@@ -0,0 +1,93 @@
# Module: Azaion.AdminApi.Program
## Purpose
Application entry point: configures DI, middleware, authentication, authorization, CORS, Swagger, logging, and defines all HTTP endpoints using ASP.NET Core Minimal API.
## Public Interface (HTTP Endpoints)
| Method | Path | Auth | Summary |
|--------|------|------|---------|
| POST | `/login` | Anonymous | Validates credentials, returns JWT token |
| POST | `/users` | ApiAdmin | Creates a new user |
| GET | `/users/current` | Any authenticated | Returns current user from JWT claims |
| GET | `/users` | ApiAdmin | Lists users with optional email/role filters |
| PUT | `/users/hardware/set` | ApiAdmin | Sets a user's hardware fingerprint |
| PUT | `/users/queue-offsets/set` | Any authenticated | Updates user's queue offsets |
| PUT | `/users/{email}/set-role/{role}` | ApiAdmin | Changes a user's role |
| PUT | `/users/{email}/enable` | ApiAdmin | Enables a user account |
| PUT | `/users/{email}/disable` | ApiAdmin | Disables a user account |
| DELETE | `/users/{email}` | ApiAdmin | Removes a user |
| POST | `/resources/{dataFolder?}` | Any authenticated | Uploads a resource file |
| GET | `/resources/list/{dataFolder?}` | Any authenticated | Lists files in a resource folder |
| POST | `/resources/clear/{dataFolder?}` | ApiAdmin | Clears a resource folder |
| POST | `/resources/get/{dataFolder?}` | Any authenticated | Downloads an encrypted resource |
| GET | `/resources/get-installer` | Any authenticated | Downloads latest production installer |
| GET | `/resources/get-installer/stage` | Any authenticated | Downloads latest staging installer |
| POST | `/resources/check` | Any authenticated | Validates hardware fingerprint |
## Internal Logic
### DI Registration
- `IUserService``UserService` (Scoped)
- `IAuthService``AuthService` (Scoped)
- `IResourcesService``ResourcesService` (Scoped)
- `IDbFactory``DbFactory` (Singleton)
- `ICache``MemoryCache` (Scoped)
- `LazyCache` via `AddLazyCache()`
- FluentValidation validators auto-discovered from `RegisterUserValidator` assembly
- `BusinessExceptionHandler` registered as exception handler
### Middleware Pipeline
1. Swagger (dev only)
2. CORS (`AdminCorsPolicy`)
3. Authentication (JWT Bearer)
4. Authorization
5. URL rewrite: root `/``/swagger`
6. Exception handler
### Authorization Policies
- `apiAdminPolicy`: requires `RoleEnum.ApiAdmin` role
- `apiUploaderPolicy`: requires `RoleEnum.ResourceUploader` OR `RoleEnum.ApiAdmin` role
### Configuration Sections
- `JwtConfig` — JWT signing/validation
- `ConnectionStrings` — DB connections
- `ResourcesConfig` — file storage paths
### Kestrel
- Max request body size: 200 MB (for file uploads)
### Logging
- Serilog: console + rolling file (`logs/log.txt`)
### CORS
- Allowed origins: `https://admin.azaion.com`, `http://admin.azaion.com`
- All methods and headers allowed
- Credentials allowed
## Dependencies
All services, configs, entities, and request types from Azaion.Common and Azaion.Services.
## Consumers
None — this is the application entry point.
## Data Models
None defined here.
## Configuration
Reads `JwtConfig`, `ConnectionStrings`, `ResourcesConfig` from `IConfiguration`.
## External Integrations
- PostgreSQL (via DI-registered `DbFactory`)
- Local filesystem (via `ResourcesService`)
## Security
- JWT Bearer authentication with full validation (issuer, audience, lifetime, signing key)
- Role-based authorization policies
- CORS restricted to `admin.azaion.com`
- Request body limit of 200 MB
- Antiforgery disabled for resource upload endpoint
- Password sent via POST body (not URL)
## Tests
None directly; tested indirectly through integration tests.
@@ -0,0 +1,54 @@
# Module: Azaion.Common.BusinessException
## Purpose
Custom exception type for domain-level errors, paired with an `ExceptionEnum` catalog of all business error codes.
## Public Interface
### BusinessException
| Member | Signature | Description |
|--------|-----------|-------------|
| Constructor | `BusinessException(ExceptionEnum exEnum)` | Creates exception with message from `ExceptionEnum`'s `[Description]` attribute |
| `ExceptionEnum` | `ExceptionEnum ExceptionEnum { get; set; }` | The specific error code |
| `GetMessage` | `static string GetMessage(ExceptionEnum exEnum)` | Looks up human-readable message for an error code |
### ExceptionEnum
| Value | Code | Description |
|-------|------|-------------|
| `NoEmailFound` | 10 | No such email found |
| `EmailExists` | 20 | Email already exists |
| `WrongPassword` | 30 | Passwords do not match |
| `PasswordLengthIncorrect` | 32 | Password should be at least 8 characters |
| `EmailLengthIncorrect` | 35 | Email is empty or invalid |
| `WrongEmail` | 37 | (no description attribute) |
| `HardwareIdMismatch` | 40 | Hardware mismatch — unauthorized hardware |
| `BadHardware` | 45 | Hardware should be not empty |
| `WrongResourceName` | 50 | Wrong resource file name |
| `NoFileProvided` | 60 | No file provided |
## Internal Logic
Static constructor eagerly loads all `ExceptionEnum` descriptions into a dictionary via `EnumExtensions.GetDescriptions<ExceptionEnum>()`. Messages are retrieved by dictionary lookup with fallback to `ToString()`.
## Dependencies
- `EnumExtensions` — for `GetDescriptions<T>()`
## Consumers
- `BusinessExceptionHandler` — catches and serializes to HTTP 409 response
- `UserService` — throws for email/password/hardware validation failures
- `ResourcesService` — throws for missing file uploads
- FluentValidation validators — reference `ExceptionEnum` codes in `.WithErrorCode()`
## Data Models
None.
## Configuration
None.
## External Integrations
None.
## Security
Error codes are returned to the client via `BusinessExceptionHandler`. Codes are numeric and messages are user-facing.
## Tests
None.
@@ -0,0 +1,37 @@
# Module: Azaion.Common.Configs.ConnectionStrings
## Purpose
Configuration POCO for PostgreSQL connection strings, bound from `appsettings.json` section `ConnectionStrings`.
## Public Interface
| Property | Type | Description |
|----------|------|-------------|
| `AzaionDb` | `string` | Read-only connection string (used for queries) |
| `AzaionDbAdmin` | `string` | Admin connection string (used for writes: insert, update, delete) |
## Internal Logic
None — pure data class.
## Dependencies
None.
## Consumers
- `DbFactory` constructor — receives `IOptions<ConnectionStrings>` to build data options
## Data Models
None.
## Configuration
Bound via `builder.Configuration.GetSection(nameof(ConnectionStrings))` in `Program.cs`. Expected env vars (from `env/api/env.ps1`):
- `ASPNETCORE_ConnectionStrings__AzaionDb`
- `ASPNETCORE_ConnectionStrings__AzaionDbAdmin`
## External Integrations
None.
## Security
Contains database credentials at runtime; values must not be logged or exposed.
## Tests
Hardcoded in `UserServiceTest` (test credentials).
@@ -0,0 +1,38 @@
# Module: Azaion.Common.Configs.JwtConfig
## Purpose
Configuration POCO for JWT token generation parameters, bound from `appsettings.json` section `JwtConfig`.
## Public Interface
| Property | Type | Description |
|----------|------|-------------|
| `Issuer` | `string` | Token issuer claim |
| `Audience` | `string` | Token audience claim |
| `Secret` | `string` | HMAC-SHA256 signing key |
| `TokenLifetimeHours` | `double` | Token expiry duration in hours |
## Internal Logic
None — pure data class.
## Dependencies
None.
## Consumers
- `Program.cs` — reads `JwtConfig` to configure JWT Bearer authentication middleware
- `AuthService.CreateToken` — uses Issuer, Audience, Secret, TokenLifetimeHours to build JWT tokens
## Data Models
None.
## Configuration
Bound via `builder.Configuration.GetSection(nameof(JwtConfig))`. Expected env var: `ASPNETCORE_JwtConfig__Secret`.
## External Integrations
None.
## Security
`Secret` is the symmetric signing key for all JWT tokens. Must be kept secret and sufficiently long for HMAC-SHA256.
## Tests
None.
@@ -0,0 +1,36 @@
# Module: Azaion.Common.Configs.ResourcesConfig
## Purpose
Configuration POCO for file resource storage paths, bound from `appsettings.json` section `ResourcesConfig`.
## Public Interface
| Property | Type | Description |
|----------|------|-------------|
| `ResourcesFolder` | `string` | Root directory for uploaded resource files |
| `SuiteInstallerFolder` | `string` | Subdirectory for production installer files |
| `SuiteStageInstallerFolder` | `string` | Subdirectory for staging installer files |
## Internal Logic
None — pure data class.
## Dependencies
None.
## Consumers
- `ResourcesService` — uses all three properties to resolve file paths
## Data Models
None.
## Configuration
Bound via `builder.Configuration.GetSection(nameof(ResourcesConfig))` in `Program.cs`.
## External Integrations
None.
## Security
Paths control where files are read from and written to on the server's filesystem.
## Tests
None.
@@ -0,0 +1,36 @@
# Module: Azaion.Common.Database.AzaionDb
## Purpose
linq2db `DataConnection` subclass representing the application's database context.
## Public Interface
| Member | Type | Description |
|--------|------|-------------|
| Constructor | `AzaionDb(DataOptions dataOptions)` | Initializes connection with pre-configured options |
| `Users` | `ITable<User>` | Typed table accessor for the `users` table |
## Internal Logic
Delegates all connection management to the base `DataConnection` class. `Users` property calls `this.GetTable<User>()`.
## Dependencies
- `User` entity
- linq2db (`LinqToDB.Data.DataConnection`, `LinqToDB.ITable<T>`)
## Consumers
- `DbFactory` — creates `AzaionDb` instances inside `Run`/`RunAdmin` methods
## Data Models
Provides access to the `users` table.
## Configuration
Receives `DataOptions` (containing connection string + mapping schema) from `DbFactory`.
## External Integrations
PostgreSQL database via Npgsql.
## Security
None at this level; connection string security is handled by `DbFactory`.
## Tests
Indirectly used by `UserServiceTest`.
@@ -0,0 +1,47 @@
# Module: Azaion.Common.Database.DbFactory
## Purpose
Factory for creating short-lived `AzaionDb` connections, providing separate read-only and admin (write) access patterns.
## Public Interface
### IDbFactory
| Method | Signature | Description |
|--------|-----------|-------------|
| `Run<T>` | `Task<T> Run<T>(Func<AzaionDb, Task<T>> func)` | Execute a read query via the read-only connection |
| `Run` | `Task Run(Func<AzaionDb, Task> func)` | Execute a read query (no return value) |
| `RunAdmin` | `Task RunAdmin(Func<AzaionDb, Task> func)` | Execute a write operation via the admin connection |
### DbFactory (implementation)
Constructor receives `IOptions<ConnectionStrings>` and builds two `DataOptions`:
- `_dataOptions` from `AzaionDb` (reader connection)
- `_dataOptionsAdmin` from `AzaionDbAdmin` (admin connection)
## Internal Logic
- `LoadOptions` validates the connection string is not empty, then builds `DataOptions` with PostgreSQL provider, the schema holder's mapping schema, and SQL trace logging to console.
- Each `Run`/`RunAdmin` call creates a new `AzaionDb` instance, executes the callback, and disposes the connection (`await using`).
## Dependencies
- `AzaionDb`, `AzaionDbSchemaHolder`
- `ConnectionStrings` config
- linq2db `DataOptions`
## Consumers
- `UserService` — all database operations go through `dbFactory.Run` or `dbFactory.RunAdmin`
- Registered as `Singleton` in DI (`Program.cs`)
## Data Models
None.
## Configuration
- `ConnectionStrings.AzaionDb` — reader connection string
- `ConnectionStrings.AzaionDbAdmin` — admin connection string
## External Integrations
PostgreSQL via Npgsql (through linq2db).
## Security
Enforces read/write separation: `Run` uses the read-only connection; `RunAdmin` uses the admin connection with INSERT/UPDATE/DELETE privileges.
## Tests
Directly instantiated in `UserServiceTest` with hardcoded connection strings.
@@ -0,0 +1,43 @@
# Module: Azaion.Common.Database.AzaionDbSchemaHolder
## Purpose
Static holder for the linq2db `MappingSchema` that maps C# entities to PostgreSQL table/column naming conventions and handles custom type conversions.
## Public Interface
| Member | Type | Description |
|--------|------|-------------|
| `MappingSchema` | `static readonly MappingSchema` | Pre-built schema with column name and type mappings |
## Internal Logic
Static constructor:
1. Creates a `MappingSchema` with a global callback that converts all column names to snake_case via `StringExtensions.ToSnakeCase`.
2. Uses `FluentMappingBuilder` to configure the `User` entity:
- Table name: `"users"`
- `Id`: primary key, `DataType.Guid`
- `Role`: stored as text, with custom conversion to/from `RoleEnum` via `Enum.Parse`
- `UserConfig`: stored as nullable JSON text, serialized/deserialized via `Newtonsoft.Json`
## Dependencies
- `User`, `RoleEnum` entities
- `StringExtensions.ToSnakeCase`
- linq2db `MappingSchema`, `FluentMappingBuilder`
- `Newtonsoft.Json`
## Consumers
- `DbFactory.LoadOptions` — passes `MappingSchema` to `DataOptions.UseMappingSchema()`
## Data Models
Defines the ORM mapping for the `users` table.
## Configuration
None — all mappings are compile-time.
## External Integrations
None directly; mappings are used when queries execute against PostgreSQL.
## Security
None.
## Tests
None.
@@ -0,0 +1,46 @@
# Module: Azaion.Common.Entities.RoleEnum
## Purpose
Defines the authorization role hierarchy for the system.
## Public Interface
| Enum Value | Int Value | Description |
|-----------|-----------|-------------|
| `None` | 0 | No role assigned |
| `Operator` | 10 | Annotator access only; can send annotations to queue |
| `Validator` | 20 | Annotator + dataset explorer; can receive annotations from queue |
| `CompanionPC` | 30 | Companion PC role |
| `Admin` | 40 | Admin role |
| `ResourceUploader` | 50 | Can upload DLLs and AI models |
| `ApiAdmin` | 1000 | Full access to all operations |
## Internal Logic
Integer values define a loose hierarchy; higher values don't necessarily imply more permissions — policy-based authorization in `Program.cs` maps specific roles to policies.
## Dependencies
None.
## Consumers
- `User.Role` property type
- `RegisterUserRequest.Role` property type
- `Program.cs` — authorization policies (`apiAdminPolicy`, `apiUploaderPolicy`)
- `AuthService.CreateToken` — embeds role as claim
- `AzaionDbSchemaHolder` — maps Role to/from text in DB
- `UserService.GetUsers` — filters by role
- `UserService.ChangeRole` — updates user role
## Data Models
Part of the `User` entity.
## Configuration
None.
## External Integrations
None.
## Security
Core to the RBAC authorization model. `ApiAdmin` has unrestricted access; `ResourceUploader` can upload resources; other roles have endpoint-level restrictions.
## Tests
None.
@@ -0,0 +1,62 @@
# Module: Azaion.Common.Entities.User
## Purpose
Domain entity representing a system user, plus related value objects `UserConfig` and `UserQueueOffsets`.
## Public Interface
### User
| Property | Type | Description |
|----------|------|-------------|
| `Id` | `Guid` | Primary key |
| `Email` | `string` | Unique user email |
| `PasswordHash` | `string` | SHA-384 hash of plaintext password |
| `Hardware` | `string?` | Raw hardware fingerprint string (set on first resource access) |
| `Role` | `RoleEnum` | Authorization role |
| `CreatedAt` | `DateTime` | Account creation timestamp |
| `LastLogin` | `DateTime?` | Last successful resource-check/hardware-check timestamp |
| `UserConfig` | `UserConfig?` | JSON-serialized user configuration |
| `IsEnabled` | `bool` | Account active flag |
| Method | Signature | Description |
|--------|-----------|-------------|
| `GetCacheKey` | `static string GetCacheKey(string email)` | Returns cache key `"User.{email}"` |
### UserConfig
| Property | Type | Description |
|----------|------|-------------|
| `QueueOffsets` | `UserQueueOffsets?` | Annotation queue offset tracking |
### UserQueueOffsets
| Property | Type | Description |
|----------|------|-------------|
| `AnnotationsOffset` | `ulong` | Offset for annotations queue |
| `AnnotationsConfirmOffset` | `ulong` | Offset for annotation confirmations |
| `AnnotationsCommandsOffset` | `ulong` | Offset for annotation commands |
## Internal Logic
`GetCacheKey` returns empty string for null/empty email to avoid cache key collisions.
## Dependencies
- `RoleEnum`
## Consumers
- All services (`UserService`, `AuthService`, `ResourcesService`) work with `User`
- `AzaionDb` exposes `ITable<User>`
- `AzaionDbSchemaHolder` maps `User` to the `users` PostgreSQL table
- `SetUserQueueOffsetsRequest` uses `UserQueueOffsets`
## Data Models
Maps to PostgreSQL table `users` with columns: `id`, `email`, `password_hash`, `hardware`, `role`, `user_config` (JSON text), `created_at`, `last_login`, `is_enabled`.
## Configuration
None.
## External Integrations
None.
## Security
`PasswordHash` stores SHA-384 hash. `Hardware` stores raw hardware fingerprint (hashed for comparison via `Security.GetHWHash`).
## Tests
Indirectly tested via `UserServiceTest` and `SecurityTest`.
@@ -0,0 +1,37 @@
# Module: Azaion.Common.Extensions.EnumExtensions
## Purpose
Static utility class for extracting `DescriptionAttribute` values from enum members and retrieving default enum values.
## Public Interface
| Method | Signature | Description |
|--------|-----------|-------------|
| `GetDescriptions<T>` | `static Dictionary<T, string> GetDescriptions<T>() where T : Enum` | Returns all enum values mapped to their `[Description]` text |
| `GetDescription` | `static string GetDescription(this Enum enumValue)` | Extension: gets the `[Description]` of a single enum value |
| `GetDefaultValue<TEnum>` | `static TEnum GetDefaultValue<TEnum>() where TEnum : struct` | Returns the `[DefaultValue]` attribute's value for an enum type |
## Internal Logic
- `GetDescriptions<T>` iterates all enum values via `Enum.GetValues`, using a private helper `GetEnumAttrib` to extract the `DescriptionAttribute` via reflection. Falls back to `.ToString()` when no attribute exists.
- `GetEnumAttrib<T, TAttrib>` fetches a custom attribute from an enum member's `FieldInfo`.
## Dependencies
- `System.ComponentModel.DescriptionAttribute`, `System.Reflection` (BCL only)
## Consumers
- `BusinessException` (static constructor calls `GetDescriptions<ExceptionEnum>()`)
## Data Models
None.
## Configuration
None.
## External Integrations
None.
## Security
None.
## Tests
None.
@@ -0,0 +1,34 @@
# Module: Azaion.Common.Extensions.QueryableExtensions
## Purpose
Conditional LINQ `Where` extension for building dynamic query filters.
## Public Interface
| Method | Signature | Description |
|--------|-----------|-------------|
| `WhereIf<TSource>` | `static IQueryable<TSource> WhereIf<TSource>(this IQueryable<TSource> query, bool? condition, Expression<Func<TSource, bool>> truePredicate, Expression<Func<TSource, bool>>? falsePredicate = null)` | Applies `truePredicate` when condition is true, optional `falsePredicate` when false, no-op when null |
## Internal Logic
If `condition` is null, returns the query unmodified. If true, applies `truePredicate`. If false and `falsePredicate` is provided, applies it; otherwise returns unmodified query.
## Dependencies
- `System.Linq.Expressions` (BCL only)
## Consumers
- `UserService.GetUsers` — uses `WhereIf` for optional email and role search filters
## Data Models
None.
## Configuration
None.
## External Integrations
None.
## Security
None.
## Tests
None.
@@ -0,0 +1,34 @@
# Module: Azaion.Common.Extensions.StreamExtensions
## Purpose
Stream-to-string conversion utility.
## Public Interface
| Method | Signature | Description |
|--------|-----------|-------------|
| `ConvertToString` | `static string ConvertToString(this Stream stream)` | Reads entire stream as UTF-8 string, resets position to 0 afterward |
## Internal Logic
Resets stream position to 0, reads via `StreamReader`, then resets again so the stream remains usable.
## Dependencies
- `System.Text.Encoding`, `System.IO.StreamReader` (BCL only)
## Consumers
- `SecurityTest.EncryptDecryptTest` — converts decrypted stream to string for assertion
## Data Models
None.
## Configuration
None.
## External Integrations
None.
## Security
None.
## Tests
Indirectly tested via `SecurityTest.EncryptDecryptTest`.
@@ -0,0 +1,34 @@
# Module: Azaion.Common.Extensions.StringExtensions
## Purpose
Provides a `ToSnakeCase` string extension for converting PascalCase/camelCase identifiers to snake_case.
## Public Interface
| Method | Signature | Description |
|--------|-----------|-------------|
| `ToSnakeCase` | `static string ToSnakeCase(this string text)` | Converts PascalCase to snake_case (e.g., `PasswordHash``password_hash`) |
## Internal Logic
Iterates characters; prepends `_` before each uppercase letter and lowercases it. Returns original text for null/empty/single-char inputs.
## Dependencies
- `System.Text.StringBuilder` (BCL only)
## Consumers
- `AzaionDbSchemaHolder` — uses `ToSnakeCase` to map C# property names to PostgreSQL column names
## Data Models
None.
## Configuration
None.
## External Integrations
None.
## Security
None.
## Tests
None.
@@ -0,0 +1,50 @@
# Module: Azaion.Common.Requests.GetResourceRequest
## Purpose
Request DTOs and validator for resource access endpoints. Contains both `GetResourceRequest` and `CheckResourceRequest`.
## Public Interface
### CheckResourceRequest
| Property | Type | Description |
|----------|------|-------------|
| `Hardware` | `string` | Hardware fingerprint to validate |
### GetResourceRequest
| Property | Type | Description |
|----------|------|-------------|
| `Password` | `string` | User's password (used to derive encryption key) |
| `Hardware` | `string` | Hardware fingerprint for authorization |
| `FileName` | `string` | Resource file to retrieve |
### GetResourceRequestValidator
| Rule | Constraint | Error Code |
|------|-----------|------------|
| `Password` min length | >= 8 chars | `PasswordLengthIncorrect` |
| `Hardware` not empty | Required | `BadHardware` |
| `FileName` not empty | Required | `WrongResourceName` |
## Internal Logic
Validator uses `BusinessException.GetMessage()` to derive user-facing error messages from `ExceptionEnum`.
## Dependencies
- `BusinessException`, `ExceptionEnum`
- FluentValidation
## Consumers
- `Program.cs` `/resources/get/{dataFolder?}` and `/resources/check` endpoints
## Data Models
None.
## Configuration
None.
## External Integrations
None.
## Security
Password is sent in the POST body (not URL) to avoid logging in access logs. Hardware fingerprint validates device authorization.
## Tests
None.
@@ -0,0 +1,36 @@
# Module: Azaion.Common.Requests.LoginRequest
## Purpose
Request DTO for the `/login` endpoint.
## Public Interface
| Property | Type | Description |
|----------|------|-------------|
| `Email` | `string` | User's email address |
| `Password` | `string` | User's plaintext password |
## Internal Logic
None — pure data class. No FluentValidation validator defined for this request.
## Dependencies
None.
## Consumers
- `Program.cs` `/login` endpoint — receives as request body
- `UserService.ValidateUser` — accepts as parameter
## Data Models
None.
## Configuration
None.
## External Integrations
None.
## Security
Carries plaintext password; must only be transmitted over HTTPS.
## Tests
None.
@@ -0,0 +1,46 @@
# Module: Azaion.Common.Requests.RegisterUserRequest
## Purpose
Request DTO and FluentValidation validator for user registration (`POST /users`).
## Public Interface
### RegisterUserRequest
| Property | Type | Description |
|----------|------|-------------|
| `Email` | `string` | New user's email |
| `Password` | `string` | Plaintext password |
| `Role` | `RoleEnum` | Role to assign |
### RegisterUserValidator
| Rule | Constraint | Error Code |
|------|-----------|------------|
| `Email` min length | >= 8 chars | `EmailLengthIncorrect` |
| `Email` format | Valid email address | `WrongEmail` |
| `Password` min length | >= 8 chars | `PasswordLengthIncorrect` |
## Internal Logic
Validator is auto-discovered by `AddValidatorsFromAssemblyContaining<RegisterUserValidator>()` in `Program.cs`.
## Dependencies
- `RoleEnum`, `ExceptionEnum` (from `BusinessException`)
- FluentValidation
## Consumers
- `Program.cs` `/users` endpoint
- `UserService.RegisterUser`
## Data Models
None.
## Configuration
None.
## External Integrations
None.
## Security
Enforces minimum password length of 8 characters and email format validation.
## Tests
None.
@@ -0,0 +1,39 @@
# Module: Azaion.Common.Requests.SetHWRequest
## Purpose
Request DTO and validator for setting a user's hardware fingerprint (`PUT /users/hardware/set`).
## Public Interface
### SetHWRequest
| Property | Type | Description |
|----------|------|-------------|
| `Email` | `string` | Target user's email |
| `Hardware` | `string?` | Hardware fingerprint (null clears it) |
### SetHWRequestValidator
| Rule | Constraint | Error Code |
|------|-----------|------------|
| `Email` not empty | Required | `EmailLengthIncorrect` |
## Dependencies
- `BusinessException`, `ExceptionEnum`
- FluentValidation
## Consumers
- `Program.cs` `/users/hardware/set` endpoint
## Data Models
None.
## Configuration
None.
## External Integrations
None.
## Security
None.
## Tests
None.
@@ -0,0 +1,35 @@
# Module: Azaion.Common.Requests.SetUserQueueOffsetsRequest
## Purpose
Request DTO for updating a user's annotation queue offsets (`PUT /users/queue-offsets/set`).
## Public Interface
| Property | Type | Description |
|----------|------|-------------|
| `Email` | `string` | Target user's email |
| `Offsets` | `UserQueueOffsets` | New queue offset values |
## Internal Logic
None — pure data class. No validator defined.
## Dependencies
- `UserQueueOffsets` (from `Entities/User.cs`)
## Consumers
- `Program.cs` `/users/queue-offsets/set` endpoint
## Data Models
None.
## Configuration
None.
## External Integrations
None.
## Security
None.
## Tests
None.
@@ -0,0 +1,48 @@
# Module: Azaion.Services.AuthService
## Purpose
JWT token creation and current-user resolution from HTTP context claims.
## Public Interface
### IAuthService
| Method | Signature | Description |
|--------|-----------|-------------|
| `GetCurrentUser` | `Task<User?> GetCurrentUser()` | Extracts email from JWT claims, returns full User entity |
| `CreateToken` | `string CreateToken(User user)` | Generates a signed JWT token for the given user |
## Internal Logic
- **GetCurrentUser**: reads `ClaimTypes.Name` from `HttpContext.User.Claims`, then delegates to `IUserService.GetByEmail`.
- **CreateToken**: builds a `SecurityTokenDescriptor` with claims (NameIdentifier = user ID, Name = email, Role = role), signs with HMAC-SHA256 using the configured secret, sets expiry from `JwtConfig.TokenLifetimeHours`.
Private method:
- `GetCurrentUserEmail` — extracts email from claims dictionary.
## Dependencies
- `IHttpContextAccessor` — for accessing current HTTP context
- `IOptions<JwtConfig>` — JWT configuration
- `IUserService` — for `GetByEmail` lookup
- `System.IdentityModel.Tokens.Jwt`
- `Microsoft.IdentityModel.Tokens`
## Consumers
- `Program.cs` `/login` endpoint — calls `CreateToken` after successful validation
- `Program.cs` `/users/current`, `/resources/get`, `/resources/get-installer`, `/resources/check` — call `GetCurrentUser`
## Data Models
None.
## Configuration
Uses `JwtConfig` (Issuer, Audience, Secret, TokenLifetimeHours).
## External Integrations
None.
## Security
- Token includes user ID, email, and role as claims
- Signed with HMAC-SHA256
- Expiry controlled by `TokenLifetimeHours` config
- Token validation parameters are configured in `Program.cs` (ValidateIssuer, ValidateAudience, ValidateLifetime, ValidateIssuerSigningKey)
## Tests
None.
@@ -0,0 +1,41 @@
# Module: Azaion.Services.Cache
## Purpose
In-memory caching abstraction using LazyCache, providing get-or-add and invalidation operations.
## Public Interface
### ICache
| Method | Signature | Description |
|--------|-----------|-------------|
| `GetFromCacheAsync<T>` | `Task<T> GetFromCacheAsync<T>(string key, Func<Task<T>> fetchFunc, TimeSpan? expiration = null)` | Returns cached value or fetches and caches it |
| `Invalidate` | `void Invalidate(string key)` | Removes a key from the cache |
### MemoryCache (implementation)
Default expiration: 4 hours (`TimeSpan.FromHours(4)`).
## Internal Logic
Wraps `LazyCache.CachingService`. `GetFromCacheAsync` uses `GetOrAddAsync` with absolute expiration relative to now. `Invalidate` calls `Remove`.
## Dependencies
- LazyCache (`IAppCache`, `CachingService`)
## Consumers
- `UserService.GetByEmail` — caches user lookups by `User.GetCacheKey(email)`
- `UserService.UpdateHardware`, `UserService.UpdateQueueOffsets`, `UserService.CheckHardwareHash` — invalidate cache after writes
- Registered as `Scoped` in DI (`Program.cs`): `AddScoped<ICache, MemoryCache>`
## Data Models
None.
## Configuration
None — default 4-hour expiration is hardcoded.
## External Integrations
None.
## Security
None.
## Tests
`MemoryCache` is instantiated directly in `UserServiceTest`.
@@ -0,0 +1,49 @@
# Module: Azaion.Services.ResourcesService
## Purpose
File-based resource management: upload, list, download (encrypted), clear, and installer retrieval from the server's filesystem.
## Public Interface
### IResourcesService
| Method | Signature | Description |
|--------|-----------|-------------|
| `GetInstaller` | `(string?, Stream?) GetInstaller(bool isStage)` | Returns the latest installer file (prod or stage) |
| `GetEncryptedResource` | `Task<Stream> GetEncryptedResource(string? dataFolder, string fileName, string key, CancellationToken ct)` | Reads a file and returns it AES-encrypted |
| `SaveResource` | `Task SaveResource(string? dataFolder, IFormFile data, CancellationToken ct)` | Saves an uploaded file to the resource folder |
| `ListResources` | `Task<IEnumerable<string>> ListResources(string? dataFolder, string? search, CancellationToken ct)` | Lists file names in a resource folder, optionally filtered |
| `ClearFolder` | `void ClearFolder(string? dataFolder)` | Deletes all files and subdirectories in the specified folder |
## Internal Logic
- **GetResourceFolder**: resolves the target directory. If `dataFolder` is null/empty, uses `ResourcesConfig.ResourcesFolder` directly; otherwise, appends it as a subdirectory.
- **GetInstaller**: scans the installer folder for files matching `"AzaionSuite.Iterative*"`, returns the first match as a `FileStream`.
- **GetEncryptedResource**: opens the file, encrypts via `Security.EncryptTo` extension into a `MemoryStream`, returns the encrypted stream.
- **SaveResource**: creates the folder if needed, deletes any existing file with the same name, then copies the uploaded file.
- **ListResources**: uses `DirectoryInfo.GetFiles` with optional search pattern.
- **ClearFolder**: iterates and deletes all files and subdirectories.
## Dependencies
- `IOptions<ResourcesConfig>` — folder paths
- `ILogger<ResourcesService>` — logs successful saves
- `BusinessException` — thrown for null file uploads
- `Security.EncryptTo` — stream encryption extension
## Consumers
- `Program.cs` — all `/resources/*` endpoints
## Data Models
None.
## Configuration
Uses `ResourcesConfig` (ResourcesFolder, SuiteInstallerFolder, SuiteStageInstallerFolder).
## External Integrations
Local filesystem for resource storage.
## Security
- Resources are encrypted per-user using a key derived from email + password + hardware hash
- File deletion overwrites existing files before writing new ones
- No path traversal protection on `dataFolder` parameter
## Tests
None.
@@ -0,0 +1,51 @@
# Module: Azaion.Services.Security
## Purpose
Static utility class providing cryptographic operations: password hashing, hardware fingerprint hashing, encryption key derivation, and AES-CBC stream encryption/decryption.
## Public Interface
| Method | Signature | Description |
|--------|-----------|-------------|
| `ToHash` | `static string ToHash(this string str)` | Extension: SHA-384 hash of input, returned as Base64 |
| `GetHWHash` | `static string GetHWHash(string hardware)` | Derives a salted hash from hardware fingerprint string |
| `GetApiEncryptionKey` | `static string GetApiEncryptionKey(string email, string password, string? hardwareHash)` | Derives an AES encryption key from email + password + hardware hash |
| `EncryptTo` | `static async Task EncryptTo(this Stream inputStream, Stream toStream, string key, CancellationToken ct)` | AES-256-CBC encrypts a stream; prepends IV to output |
| `DecryptTo` | `static async Task DecryptTo(this Stream encryptedStream, Stream toStream, string key, CancellationToken ct)` | Reads IV prefix, then AES-256-CBC decrypts stream |
## Internal Logic
- **Password hashing**: `ToHash` uses SHA-384 with UTF-8 encoding, outputting Base64.
- **Hardware hashing**: `GetHWHash` salts the raw hardware string with `"Azaion_{hardware}_%$$$)0_"` before hashing.
- **Encryption key derivation**: `GetApiEncryptionKey` concatenates email, password, and hardware hash with a static salt, then hashes.
- **Encryption**: AES-256-CBC with PKCS7 padding. Key is SHA-256 of the derived key string. IV is randomly generated and prepended to the output stream. Uses 512 KB buffer for streaming.
- **Decryption**: Reads the first 16 bytes as IV, then AES-256-CBC decrypts with PKCS7 padding.
## Dependencies
- `System.Security.Cryptography` (Aes, SHA256, SHA384)
- `System.Text.Encoding`
## Consumers
- `UserService.CheckHardwareHash` — calls `GetHWHash` to verify hardware fingerprint
- `Program.cs` `/resources/get` endpoint — calls `GetApiEncryptionKey`
- `ResourcesService.GetEncryptedResource` — uses `EncryptTo` extension
- `SecurityTest` — directly tests `GetApiEncryptionKey`, `EncryptTo`, `DecryptTo`
## Data Models
None.
## Configuration
- `BUFFER_SIZE = 524288` (512 KB) — hardcoded streaming buffer size
## External Integrations
None.
## Security
Core cryptographic module. Key observations:
- Passwords are hashed with SHA-384 (no per-user salt, no key stretching — not bcrypt/scrypt/argon2)
- Hardware hash uses a static salt
- AES encryption uses SHA-256 of the derived key, with random IV per encryption
- All salts/prefixes are hardcoded constants
## Tests
- `SecurityTest.EncryptDecryptTest` — round-trip encrypt/decrypt of a string
- `SecurityTest.EncryptDecryptLargeFileTest` — round-trip encrypt/decrypt of a ~400 MB generated file
@@ -0,0 +1,62 @@
# Module: Azaion.Services.UserService
## Purpose
Core business logic for user management: registration, authentication, hardware binding, role management, and account lifecycle.
## Public Interface
### IUserService
| Method | Signature | Description |
|--------|-----------|-------------|
| `RegisterUser` | `Task RegisterUser(RegisterUserRequest request, CancellationToken ct)` | Creates a new user with hashed password |
| `ValidateUser` | `Task<User> ValidateUser(LoginRequest request, CancellationToken ct)` | Validates email + password, returns user |
| `GetByEmail` | `Task<User?> GetByEmail(string? email, CancellationToken ct)` | Cached user lookup by email |
| `UpdateHardware` | `Task UpdateHardware(string email, string? hardware, CancellationToken ct)` | Sets/clears user's hardware fingerprint |
| `UpdateQueueOffsets` | `Task UpdateQueueOffsets(string email, UserQueueOffsets offsets, CancellationToken ct)` | Updates user's annotation queue offsets |
| `GetUsers` | `Task<IEnumerable<User>> GetUsers(string? searchEmail, RoleEnum? searchRole, CancellationToken ct)` | Lists users with optional email/role filters |
| `CheckHardwareHash` | `Task<string> CheckHardwareHash(User user, string hardware, CancellationToken ct)` | Validates or initializes hardware binding |
| `ChangeRole` | `Task ChangeRole(string email, RoleEnum newRole, CancellationToken ct)` | Changes a user's role |
| `SetEnableStatus` | `Task SetEnableStatus(string email, bool isEnabled, CancellationToken ct)` | Enables or disables a user account |
| `RemoveUser` | `Task RemoveUser(string email, CancellationToken ct)` | Permanently deletes a user |
## Internal Logic
- **RegisterUser**: checks for duplicate email, hashes password via `Security.ToHash`, inserts via `RunAdmin`.
- **ValidateUser**: finds user by email, compares password hash. Throws `NoEmailFound` or `WrongPassword`.
- **GetByEmail**: uses `ICache.GetFromCacheAsync` with key `User.{email}`.
- **CheckHardwareHash**: on first access (null hardware), stores the raw hardware string and returns the hash. On subsequent access, compares hashes. Throws `HardwareIdMismatch` on mismatch. Also updates `LastLogin` timestamp.
- **UpdateHardware/UpdateQueueOffsets**: use `RunAdmin` for writes, then invalidate cache.
- **GetUsers**: uses `WhereIf` for optional filter predicates.
Private method:
- `UpdateLastLoginDate` — updates `LastLogin` to `DateTime.UtcNow`.
## Dependencies
- `IDbFactory` (database access)
- `ICache` (user caching)
- `Security` (hashing)
- `BusinessException` (domain errors)
- `QueryableExtensions.WhereIf`
- `User`, `UserConfig`, `UserQueueOffsets`, `RoleEnum`
- `RegisterUserRequest`, `LoginRequest`
## Consumers
- `Program.cs` — all `/users/*` endpoints delegate to `IUserService`
- `AuthService.GetCurrentUser` — calls `GetByEmail`
- `Program.cs` `/resources/get` — calls `CheckHardwareHash`
## Data Models
Operates on `User` entity via `AzaionDb.Users` table.
## Configuration
None.
## External Integrations
PostgreSQL via `IDbFactory`.
## Security
- Passwords hashed with SHA-384 (via `Security.ToHash`) before storage
- Hardware binding prevents resource access from unauthorized devices
- Read operations use read-only DB connection; writes use admin connection
## Tests
- `UserServiceTest.CheckHardwareHashTest` — integration test against live database
@@ -0,0 +1,45 @@
# Module: Azaion.Test.SecurityTest
## Purpose
xUnit tests for the `Security` encryption/decryption functionality.
## Public Interface
| Test | Description |
|------|-------------|
| `EncryptDecryptTest` | Round-trip encrypt/decrypt of a ~1 KB string; asserts decrypted output matches original |
| `EncryptDecryptLargeFileTest` | Round-trip encrypt/decrypt of a ~400 MB generated file; compares SHA-256 hashes of original and decrypted files |
## Internal Logic
- **EncryptDecryptTest**: creates a key via `Security.GetApiEncryptionKey`, encrypts a test string to a `MemoryStream`, decrypts back, compares with `FluentAssertions`.
- **EncryptDecryptLargeFileTest**: generates a large JSON file (4M numbers chunked), encrypts, decrypts to a new file, compares file hashes via `SHA256.HashDataAsync`.
Private helpers:
- `CompareFiles` — SHA-256 hash comparison of two files
- `CreateLargeFile` — generates a large file by serializing number dictionaries in 100K chunks
- `StringToStream` — converts a UTF-8 string to a `MemoryStream`
## Dependencies
- `Security` (encrypt/decrypt)
- `StreamExtensions.ConvertToString`
- `FluentAssertions`
- `Newtonsoft.Json`
- xUnit
## Consumers
None — test module.
## Data Models
None.
## Configuration
None.
## External Integrations
Local filesystem (creates/deletes `large.txt` and `large_decrypted.txt` during large file test).
## Security
None.
## Tests
This IS the test module.
@@ -0,0 +1,39 @@
# Module: Azaion.Test.UserServiceTest
## Purpose
xUnit integration test for `UserService.CheckHardwareHash` against a live PostgreSQL database.
## Public Interface
| Test | Description |
|------|-------------|
| `CheckHardwareHashTest` | Looks up a known user by email, then calls `CheckHardwareHash` with a hardware fingerprint string |
## Internal Logic
- Creates a `DbFactory` with hardcoded connection strings pointing to a remote PostgreSQL instance.
- Creates a `UserService` with that factory and a fresh `MemoryCache`.
- Fetches user `spielberg@azaion.com`, then calls `CheckHardwareHash` with a specific hardware string.
- No assertion — the test only verifies no exception is thrown.
## Dependencies
- `UserService`, `DbFactory`, `MemoryCache`
- `ConnectionStrings`, `OptionsWrapper`
- xUnit
## Consumers
None — test module.
## Data Models
None.
## Configuration
Hardcoded connection strings to `188.245.120.247:4312` (remote database).
## External Integrations
Live PostgreSQL database (remote server).
## Security
Contains hardcoded database credentials in source code. This is a security concern — credentials should be in test configuration or environment variables.
## Tests
This IS the test module.
+45
View File
@@ -0,0 +1,45 @@
{
"current_step": "complete",
"completed_steps": ["discovery", "module-analysis", "component-assembly", "system-synthesis", "verification", "solution-extraction", "problem-extraction", "final-report"],
"focus_dir": null,
"modules_total": 27,
"modules_documented": [
"Common/Extensions/EnumExtensions",
"Common/Extensions/StringExtensions",
"Common/Extensions/StreamExtensions",
"Common/Extensions/QueryableExtensions",
"Common/Entities/RoleEnum",
"Common/Configs/ConnectionStrings",
"Common/Configs/JwtConfig",
"Common/Configs/ResourcesConfig",
"Common/Requests/LoginRequest",
"Common/Entities/User",
"Common/BusinessException",
"Common/Requests/RegisterUserRequest",
"Common/Requests/GetResourceRequest",
"Common/Requests/SetHWRequest",
"Common/Requests/SetUserQueueOffsetsRequest",
"Common/Database/AzaionDb",
"Common/Database/AzaionDbSchemaHolder",
"Common/Database/DbFactory",
"Services/Security",
"Services/Cache",
"Services/UserService",
"Services/AuthService",
"Services/ResourcesService",
"AdminApi/BusinessExceptionHandler",
"AdminApi/Program",
"Test/SecurityTest",
"Test/UserServiceTest"
],
"modules_remaining": [],
"module_batch": 4,
"components_written": [
"01_data_layer",
"02_user_management",
"03_auth_and_security",
"04_resource_management",
"05_admin_api"
],
"last_updated": "2026-04-16T00:15:00Z"
}
+268
View File
@@ -0,0 +1,268 @@
# Azaion Admin API — System Flows
## Flow Inventory
| # | Flow Name | Trigger | Primary Components | Criticality |
|---|-----------|---------|-------------------|-------------|
| F1 | User Login | POST /login | Admin API, User Mgmt, Auth & Security | High |
| F2 | User Registration | POST /users | Admin API, User Mgmt | High |
| F3 | Encrypted Resource Download | POST /resources/get | Admin API, Auth, User Mgmt, Resource Mgmt | High |
| F4 | Hardware Check | POST /resources/check | Admin API, Auth, User Mgmt | High |
| F5 | Resource Upload | POST /resources | Admin API, Resource Mgmt | Medium |
| F6 | Installer Download | GET /resources/get-installer | Admin API, Auth, Resource Mgmt | Medium |
| F7 | User Management (CRUD) | Various /users/* | Admin API, User Mgmt | Medium |
## Flow Dependencies
| Flow | Depends On | Shares Data With |
|------|-----------|-----------------|
| F1 | — | All other flows (produces JWT token) |
| F2 | — | F1, F3, F4 (creates user records) |
| F3 | F1 (requires JWT), F4 (hardware must be bound) | F4 (via hardware hash) |
| F4 | F1 (requires JWT) | F3 (hardware binding) |
| F5 | F1 (requires JWT) | F3 (uploaded resources are later downloaded) |
| F6 | F1 (requires JWT) | — |
| F7 | F1 (requires JWT, ApiAdmin role) | F3, F4 (user data) |
---
## Flow F1: User Login
### Description
A user submits email/password credentials. The system validates them against the database and returns a signed JWT token for subsequent authenticated requests.
### Preconditions
- User account exists in the database
- User knows correct password
### Sequence Diagram
```mermaid
sequenceDiagram
participant Client
participant API as Admin API
participant US as UserService
participant DB as PostgreSQL
participant Auth as AuthService
Client->>API: POST /login {email, password}
API->>US: ValidateUser(request)
US->>DB: SELECT user WHERE email = ?
DB-->>US: User record
US->>US: Compare password hash
US-->>API: User entity
API->>Auth: CreateToken(user)
Auth-->>API: JWT string
API-->>Client: 200 OK {token}
```
### Error Scenarios
| Error | Where | Detection | Recovery |
|-------|-------|-----------|----------|
| Email not found | UserService.ValidateUser | No DB record | 409: NoEmailFound (code 10) |
| Wrong password | UserService.ValidateUser | Hash mismatch | 409: WrongPassword (code 30) |
---
## Flow F2: User Registration
### Description
An admin creates a new user account with email, password, and role.
### Preconditions
- Caller has ApiAdmin role
- Email is not already registered
### Sequence Diagram
```mermaid
sequenceDiagram
participant Admin
participant API as Admin API
participant VAL as FluentValidation
participant US as UserService
participant DB as PostgreSQL
Admin->>API: POST /users {email, password, role}
API->>VAL: Validate RegisterUserRequest
VAL-->>API: OK
API->>US: RegisterUser(request)
US->>DB: SELECT user WHERE email = ?
DB-->>US: null (no duplicate)
US->>US: Hash password (SHA-384)
US->>DB: INSERT user (admin connection)
DB-->>US: OK
US-->>API: void
API-->>Admin: 200 OK
```
### Error Scenarios
| Error | Where | Detection | Recovery |
|-------|-------|-----------|----------|
| Validation failure | FluentValidation | Email < 8 chars, bad format, password < 8 chars | 400 Bad Request |
| Duplicate email | UserService.RegisterUser | Existing user found | 409: EmailExists (code 20) |
---
## Flow F3: Encrypted Resource Download
### Description
An authenticated user requests a resource file. The system validates hardware binding, derives a per-user encryption key, encrypts the file with AES-256-CBC, and streams the encrypted content.
### Preconditions
- User is authenticated (JWT)
- User's hardware is bound (via prior F4 call)
- Resource file exists on server
### Sequence Diagram
```mermaid
sequenceDiagram
participant Client
participant API as Admin API
participant Auth as AuthService
participant US as UserService
participant Sec as Security
participant RS as ResourcesService
participant FS as Filesystem
Client->>API: POST /resources/get {password, hardware, fileName}
API->>Auth: GetCurrentUser()
Auth-->>API: User
API->>US: CheckHardwareHash(user, hardware)
US->>Sec: GetHWHash(hardware)
Sec-->>US: hash
US-->>API: hwHash
API->>Sec: GetApiEncryptionKey(email, password, hwHash)
Sec-->>API: AES key
API->>RS: GetEncryptedResource(folder, fileName, key)
RS->>FS: Read file
FS-->>RS: FileStream
RS->>Sec: EncryptTo(stream, key)
Sec-->>RS: Encrypted MemoryStream
RS-->>API: Stream
API-->>Client: 200 OK (application/octet-stream)
```
### Error Scenarios
| Error | Where | Detection | Recovery |
|-------|-------|-----------|----------|
| Not authenticated | API | No/invalid JWT | 401 Unauthorized |
| Hardware mismatch | UserService.CheckHardwareHash | Hash comparison fails | 409: HardwareIdMismatch (code 40) |
| File not found | ResourcesService | FileStream throws | 500 Internal Server Error |
---
## Flow F4: Hardware Check (First Login / Validation)
### Description
Client submits its hardware fingerprint. On first call, the hardware is stored for the user. On subsequent calls, the stored hash is compared against the provided hardware.
### Preconditions
- User is authenticated (JWT)
### Sequence Diagram
```mermaid
sequenceDiagram
participant Client
participant API as Admin API
participant Auth as AuthService
participant US as UserService
participant DB as PostgreSQL
Client->>API: POST /resources/check {hardware}
API->>Auth: GetCurrentUser()
Auth-->>API: User
API->>US: CheckHardwareHash(user, hardware)
alt First time (no stored hardware)
US->>DB: UPDATE user SET hardware = ? (admin conn)
US->>DB: UPDATE user SET last_login = now()
US-->>API: hwHash
else Hardware already bound
US->>US: Compare hashes
alt Match
US->>DB: UPDATE user SET last_login = now()
US-->>API: hwHash
else Mismatch
US-->>API: throw HardwareIdMismatch
end
end
API-->>Client: 200 OK (true) / 409
```
---
## Flow F5: Resource Upload
### Description
An authenticated user uploads a file to a specified resource folder on the server.
### Preconditions
- User is authenticated (JWT)
- File size <= 200 MB
### Sequence Diagram
```mermaid
sequenceDiagram
participant User
participant API as Admin API
participant RS as ResourcesService
participant FS as Filesystem
User->>API: POST /resources/{folder} (multipart/form-data)
API->>RS: SaveResource(folder, file)
RS->>FS: Create directory (if needed)
RS->>FS: Delete existing file (same name)
RS->>FS: Write file
FS-->>RS: OK
RS-->>API: void
API-->>User: 200 OK
```
---
## Flow F6: Installer Download
### Description
An authenticated user downloads the latest Azaion Suite installer (production or staging).
### Preconditions
- User is authenticated (JWT)
- Installer file exists on server
### Sequence Diagram
```mermaid
sequenceDiagram
participant Client
participant API as Admin API
participant Auth as AuthService
participant RS as ResourcesService
participant FS as Filesystem
Client->>API: GET /resources/get-installer
API->>Auth: GetCurrentUser()
Auth-->>API: User (not null)
API->>RS: GetInstaller(isStage: false)
RS->>FS: Scan for AzaionSuite.Iterative*
FS-->>RS: FileInfo
RS-->>API: (name, FileStream)
API-->>Client: 200 OK (application/octet-stream)
```
---
## Flow F7: User Management (CRUD)
### Description
Admin operations: list users, change role, enable/disable, set hardware, update queue offsets, delete user.
### Preconditions
- Caller has ApiAdmin role (for most operations)
All operations follow the same pattern: API endpoint → UserService method → DbFactory.RunAdmin → PostgreSQL UPDATE/DELETE. Cache is invalidated for affected user keys after writes.
+475
View File
@@ -0,0 +1,475 @@
# Blackbox Tests
## Positive Scenarios
### FT-P-01: Successful Login
**Summary**: User with valid credentials receives a JWT token.
**Traces to**: AC-1
**Category**: Authentication
**Preconditions**:
- Seed user `admin@azaion.com` exists in database
**Input data**: Valid email/password for seed admin user
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | POST /login with valid email and password | HTTP 200, body contains non-empty `token` string |
**Expected outcome**: HTTP 200 with JWT token in response body
**Max execution time**: 5s
---
### FT-P-02: Successful User Registration
**Summary**: ApiAdmin creates a new user account.
**Traces to**: AC-5, AC-6, AC-7
**Category**: User Management
**Preconditions**:
- Caller authenticated as ApiAdmin
**Input data**: `{"email":"newuser@test.com","password":"validpwd1","role":"Operator"}`
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Login as admin to get JWT | HTTP 200, JWT token |
| 2 | POST /users with valid registration data and ApiAdmin JWT | HTTP 200 |
**Expected outcome**: HTTP 200, user created
**Max execution time**: 5s
---
### FT-P-03: JWT Token Structure Validation
**Summary**: JWT token contains correct issuer, audience, and lifetime claims.
**Traces to**: AC-4
**Category**: Authentication
**Preconditions**:
- Valid login completed
**Input data**: JWT token from login response
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Login to get JWT | HTTP 200, JWT token |
| 2 | Decode JWT payload (Base64) | Claims contain `iss`, `aud`, `exp` |
| 3 | Validate `iss` == "AzaionApi" | Match |
| 4 | Validate `aud` == "Annotators/OrangePi/Admins" | Match |
| 5 | Validate `exp` - `iat` ≈ 14400s (4 hours) | Within ± 60s |
**Expected outcome**: All JWT claims match expected values
**Max execution time**: 5s
---
### FT-P-04: First Hardware Check Stores Fingerprint
**Summary**: On first hardware check, the fingerprint is stored for the user.
**Traces to**: AC-10
**Category**: Hardware Binding
**Preconditions**:
- User exists with no hardware bound
**Input data**: `{"hardware":"test-hw-fingerprint-001"}`
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Register new user, login to get JWT | HTTP 200 |
| 2 | POST /resources/check with hardware string | HTTP 200, body `true` |
**Expected outcome**: HTTP 200, hardware stored
**Max execution time**: 5s
---
### FT-P-05: Subsequent Hardware Check Matches
**Summary**: Same hardware fingerprint passes validation on subsequent calls.
**Traces to**: AC-11
**Category**: Hardware Binding
**Preconditions**:
- User with hardware already bound (from FT-P-04)
**Input data**: Same hardware string as initial binding
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | POST /resources/check with same hardware | HTTP 200, body `true` |
**Expected outcome**: HTTP 200
**Max execution time**: 5s
---
### FT-P-06: List All Users
**Summary**: ApiAdmin retrieves the user list.
**Traces to**: AC-9
**Category**: User Management
**Preconditions**:
- Caller authenticated as ApiAdmin
**Input data**: GET /users with ApiAdmin JWT
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | GET /users with ApiAdmin JWT | HTTP 200, JSON array with >= 1 user |
**Expected outcome**: HTTP 200, array containing at least seed users
**Max execution time**: 5s
---
### FT-P-07: Filter Users by Email
**Summary**: ApiAdmin filters users by email substring.
**Traces to**: AC-9
**Category**: User Management
**Preconditions**:
- Caller authenticated as ApiAdmin, seed users exist
**Input data**: GET /users?email=admin
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | GET /users?email=admin with ApiAdmin JWT | HTTP 200, all returned emails contain "admin" |
**Expected outcome**: HTTP 200, filtered list
**Max execution time**: 5s
---
### FT-P-08: Upload Resource File
**Summary**: Authenticated user uploads a file to a resource folder.
**Traces to**: AC-13
**Category**: Resource Distribution
**Preconditions**:
- Caller authenticated
**Input data**: Multipart form upload with 1 KB text file
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | POST /resources/testfolder with multipart file | HTTP 200 |
**Expected outcome**: HTTP 200, file stored
**Max execution time**: 5s
---
### FT-P-09: Download Encrypted Resource
**Summary**: Authenticated user downloads an encrypted resource file.
**Traces to**: AC-14, AC-18
**Category**: Resource Distribution
**Preconditions**:
- User authenticated, hardware bound, resource file uploaded
**Input data**: `{"password":"validpwd1","hardware":"test-hw-001","fileName":"test.txt"}`
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | POST /resources/get with credentials | HTTP 200, Content-Type: application/octet-stream, non-empty body |
**Expected outcome**: HTTP 200 with encrypted binary content
**Max execution time**: 10s
---
### FT-P-10: Encryption Round-Trip Verification
**Summary**: Downloaded encrypted resource decrypts to original file content.
**Traces to**: AC-15, AC-19
**Category**: Resource Distribution
**Preconditions**:
- Known file uploaded, user credentials known
**Input data**: Original file content, user email, password, hardware hash
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Upload known file | HTTP 200 |
| 2 | Download encrypted file via API | HTTP 200, encrypted bytes |
| 3 | Derive AES key from email + password + hwHash | Key bytes |
| 4 | Decrypt downloaded content with derived key | Decrypted bytes |
| 5 | Compare decrypted bytes with original | Byte-level equality |
**Expected outcome**: Decrypted content matches original file exactly
**Max execution time**: 10s
---
### FT-P-11: Change User Role
**Summary**: ApiAdmin changes a user's role.
**Traces to**: AC-9
**Category**: User Management
**Preconditions**:
- Target user exists, caller is ApiAdmin
**Input data**: `{"email":"testuser@test.com","role":"Admin"}`
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | PUT /users/role with ApiAdmin JWT | HTTP 200 |
**Expected outcome**: HTTP 200, role updated
**Max execution time**: 5s
---
### FT-P-12: Disable User Account
**Summary**: ApiAdmin disables a user account.
**Traces to**: AC-9
**Category**: User Management
**Preconditions**:
- Target user exists, caller is ApiAdmin
**Input data**: `{"email":"testuser@test.com","isEnabled":false}`
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | PUT /users/enable with ApiAdmin JWT | HTTP 200 |
**Expected outcome**: HTTP 200, account disabled
**Max execution time**: 5s
---
### FT-P-13: Delete User
**Summary**: ApiAdmin deletes a user account.
**Traces to**: AC-9
**Category**: User Management
**Preconditions**:
- Target user exists, caller is ApiAdmin
**Input data**: DELETE /users?email=testuser@test.com
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | DELETE /users?email=testuser@test.com with ApiAdmin JWT | HTTP 200 |
**Expected outcome**: HTTP 200, user deleted
**Max execution time**: 5s
---
## Negative Scenarios
### FT-N-01: Login with Unknown Email
**Summary**: Login attempt with non-existent email returns appropriate error.
**Traces to**: AC-2
**Category**: Authentication
**Preconditions**:
- Email does not exist in database
**Input data**: `{"email":"nonexistent@test.com","password":"anypass1"}`
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | POST /login with unknown email | HTTP 409, ExceptionEnum code 10 (NoEmailFound) |
**Expected outcome**: HTTP 409 with error code 10
**Max execution time**: 5s
---
### FT-N-02: Login with Wrong Password
**Summary**: Login attempt with correct email but wrong password returns error.
**Traces to**: AC-3
**Category**: Authentication
**Preconditions**:
- User exists in database
**Input data**: `{"email":"admin@azaion.com","password":"wrongpassword123"}`
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | POST /login with wrong password | HTTP 409, ExceptionEnum code 30 (WrongPassword) |
**Expected outcome**: HTTP 409 with error code 30
**Max execution time**: 5s
---
### FT-N-03: Register with Short Email
**Summary**: Registration with email shorter than 8 characters is rejected.
**Traces to**: AC-5
**Category**: User Management
**Preconditions**:
- Caller authenticated as ApiAdmin
**Input data**: `{"email":"short","password":"validpwd1","role":"Operator"}`
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | POST /users with short email | HTTP 400, validation error |
**Expected outcome**: HTTP 400 with email length validation error
**Max execution time**: 5s
---
### FT-N-04: Register with Invalid Email Format
**Summary**: Registration with invalid email format (>= 8 chars but not email) is rejected.
**Traces to**: AC-6
**Category**: User Management
**Preconditions**:
- Caller authenticated as ApiAdmin
**Input data**: `{"email":"notanemail","password":"validpwd1","role":"Operator"}`
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | POST /users with invalid email format | HTTP 400, validation error |
**Expected outcome**: HTTP 400 with email format validation error
**Max execution time**: 5s
---
### FT-N-05: Upload Empty File
**Summary**: Upload request with no file attached returns error.
**Traces to**: AC-16
**Category**: Resource Distribution
**Preconditions**:
- Caller authenticated
**Input data**: POST /resources/testfolder with no file
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | POST /resources/testfolder with empty request | HTTP 409, ExceptionEnum code 70 (NoFileProvided) |
**Expected outcome**: HTTP 409 with error code 70
**Max execution time**: 5s
---
### FT-N-06: Hardware Mismatch
**Summary**: Hardware check with different fingerprint after binding returns error.
**Traces to**: AC-12
**Category**: Hardware Binding
**Preconditions**:
- User has hardware already bound to a different fingerprint
**Input data**: `{"hardware":"different-hardware-xyz"}`
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | POST /resources/check with different hardware | HTTP 409, ExceptionEnum code 40 (HardwareIdMismatch) |
**Expected outcome**: HTTP 409 with error code 40
**Max execution time**: 5s
---
### FT-N-07: Register Duplicate Email
**Summary**: Registration with already-existing email returns error.
**Traces to**: AC-8
**Category**: User Management
**Preconditions**:
- User with target email already exists
**Input data**: `{"email":"admin@azaion.com","password":"validpwd1","role":"Operator"}`
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | POST /users with existing email | HTTP 409, ExceptionEnum code 20 (EmailExists) |
**Expected outcome**: HTTP 409 with error code 20
**Max execution time**: 5s
---
### FT-N-08: Register with Short Password
**Summary**: Registration with password shorter than 8 characters is rejected.
**Traces to**: AC-7
**Category**: User Management
**Preconditions**:
- Caller authenticated as ApiAdmin
**Input data**: `{"email":"newuser@test.com","password":"short","role":"Operator"}`
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | POST /users with short password | HTTP 400, validation error |
**Expected outcome**: HTTP 400 with password length validation error
**Max execution time**: 5s
+103
View File
@@ -0,0 +1,103 @@
# Test Environment
## Overview
**System under test**: Azaion Admin API — ASP.NET Core Minimal API at `http://localhost:8080`
**Consumer app purpose**: Standalone xUnit test project exercising the API through HTTP requests, validating black-box use cases without access to internals.
## Docker Environment
### Services
| Service | Image / Build | Purpose | Ports |
|---------|--------------|---------|-------|
| system-under-test | Build from `Dockerfile` | Azaion Admin API | 8080:8080 |
| test-db | `postgres:16-alpine` | PostgreSQL database | 4312:5432 |
| e2e-consumer | Build from `docker.test/Dockerfile` | Black-box test runner (xUnit) | — |
### Networks
| Network | Services | Purpose |
|---------|----------|---------|
| e2e-net | all | Isolated test network |
### Volumes
| Volume | Mounted to | Purpose |
|--------|-----------|---------|
| test-resources | system-under-test:/app/Content | Resource files for upload/download tests |
| db-init | test-db:/docker-entrypoint-initdb.d | Schema DDL + seed data |
### docker-compose structure
```yaml
services:
test-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: azaion
POSTGRES_USER: azaion_superadmin
POSTGRES_PASSWORD: test_password
volumes:
- ./env/db/01_permissions.sql:/docker-entrypoint-initdb.d/01.sql
- ./env/db/02_structure.sql:/docker-entrypoint-initdb.d/02.sql
- ./env/db/03_add_timestamp_columns.sql:/docker-entrypoint-initdb.d/03.sql
networks:
- e2e-net
system-under-test:
build: .
environment:
ASPNETCORE_ConnectionStrings__AzaionDb: "Host=test-db;Port=5432;Database=azaion;Username=azaion_reader;Password=test_password"
ASPNETCORE_ConnectionStrings__AzaionDbAdmin: "Host=test-db;Port=5432;Database=azaion;Username=azaion_admin;Password=test_password"
ASPNETCORE_JwtConfig__Secret: "test-jwt-secret-key-at-least-32-chars-long"
ASPNETCORE_ENVIRONMENT: Development
depends_on:
- test-db
networks:
- e2e-net
e2e-consumer:
build:
context: .
dockerfile: docker.test/Dockerfile
environment:
API_BASE_URL: "http://system-under-test:8080"
depends_on:
- system-under-test
networks:
- e2e-net
networks:
e2e-net:
```
## Consumer Application
**Tech stack**: C# / .NET 10.0, xUnit 2.9.2, FluentAssertions 6.12.2, HttpClient
**Entry point**: `dotnet test` in the e2e consumer project
### Communication with system under test
| Interface | Protocol | Endpoint | Authentication |
|-----------|----------|----------|----------------|
| REST API | HTTP | `http://system-under-test:8080/*` | JWT Bearer token |
### What the consumer does NOT have access to
- No direct database access to the test-db (queries go through the API only)
- No internal module imports from Azaion.Services or Azaion.Common
- No shared filesystem with the system-under-test (except via API upload/download)
## CI/CD Integration
**When to run**: On every push to `dev`, `main`, and `stage` branches
**Pipeline stage**: After build, before deploy
**Gate behavior**: Block merge on failure
**Timeout**: 5 minutes
## Reporting
**Format**: CSV
**Columns**: Test ID, Test Name, Execution Time (ms), Result (PASS/FAIL/SKIP), Error Message (if FAIL)
**Output path**: `./e2e-results/report.csv`
@@ -0,0 +1,83 @@
# Performance Tests
### NFT-PERF-01: Login Endpoint Latency
**Summary**: Login endpoint responds within acceptable latency under normal load.
**Traces to**: AC-1
**Metric**: Response time (p95)
**Preconditions**:
- System running with seed data
- 10 concurrent users
**Steps**:
| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Send 100 login requests (10 concurrent) | Measure p50, p95, p99 response times |
**Pass criteria**: p95 latency < 500ms
**Duration**: 30 seconds
---
### NFT-PERF-02: Resource Download Latency (Small File)
**Summary**: Encrypted resource download for a small file (1 KB) completes quickly.
**Traces to**: AC-14
**Metric**: Response time including encryption
**Preconditions**:
- 1 KB test file uploaded
- User authenticated with bound hardware
**Steps**:
| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Send 50 encrypted download requests (5 concurrent) | Measure p50, p95 response times |
**Pass criteria**: p95 latency < 1000ms
**Duration**: 30 seconds
---
### NFT-PERF-03: Resource Download Latency (Large File)
**Summary**: Encrypted resource download for a larger file (50 MB) completes within limits.
**Traces to**: AC-13, AC-14
**Metric**: Response time including encryption + transfer
**Preconditions**:
- 50 MB test file uploaded
- User authenticated with bound hardware
**Steps**:
| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Send 5 sequential encrypted download requests | Measure p50, p95 response times |
**Pass criteria**: p95 latency < 30000ms (30s)
**Duration**: 3 minutes
---
### NFT-PERF-04: User List Endpoint Under Load
**Summary**: User list endpoint responds within limits when DB has many users.
**Traces to**: AC-9
**Metric**: Response time
**Preconditions**:
- 500 users in database
- Caller is ApiAdmin
**Steps**:
| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Send 50 GET /users requests (10 concurrent) | Measure p50, p95 response times |
**Pass criteria**: p95 latency < 1000ms
**Duration**: 30 seconds
@@ -0,0 +1,71 @@
# Resilience Tests
### NFT-RES-01: Database Connection Loss Recovery
**Summary**: API returns appropriate errors when database is unavailable, and recovers when it comes back.
**Traces to**: AC-1, AC-9
**Preconditions**:
- System running normally with database connected
**Fault injection**:
- Stop the PostgreSQL container
**Steps**:
| Step | Action | Expected Behavior |
|------|--------|------------------|
| 1 | Stop test-db container | Database unavailable |
| 2 | Send POST /login request | HTTP 500 (database error, not crash) |
| 3 | Verify API process is still running | Process alive, accepting connections |
| 4 | Restart test-db container | Database available |
| 5 | Wait 5 seconds for connection recovery | — |
| 6 | Send POST /login request | HTTP 200 or HTTP 409 (normal behavior) |
**Pass criteria**: API does not crash on DB loss; recovers within 10s of DB restoration
---
### NFT-RES-02: Invalid JWT Token Handling
**Summary**: API rejects malformed JWT tokens gracefully without crashing.
**Traces to**: AC-18
**Preconditions**:
- System running normally
**Fault injection**:
- Send requests with malformed Authorization headers
**Steps**:
| Step | Action | Expected Behavior |
|------|--------|------------------|
| 1 | Send GET /users with `Authorization: Bearer invalid-token` | HTTP 401 |
| 2 | Send GET /users with `Authorization: Bearer ` (empty) | HTTP 401 |
| 3 | Send GET /users with `Authorization: NotBearer token` | HTTP 401 |
| 4 | Send normal login request | HTTP 200 (system unaffected) |
**Pass criteria**: All malformed tokens return HTTP 401; system remains operational
---
### NFT-RES-03: Concurrent Hardware Binding Attempt
**Summary**: Two simultaneous hardware check requests for the same user do not corrupt data.
**Traces to**: AC-10, AC-11
**Preconditions**:
- User with no hardware bound
**Fault injection**:
- Race condition: two concurrent POST /resources/check with same hardware
**Steps**:
| Step | Action | Expected Behavior |
|------|--------|------------------|
| 1 | Send two POST /resources/check simultaneously with same hardware | Both return HTTP 200 or one returns 200 and other returns 409 |
| 2 | Send a third POST /resources/check with same hardware | HTTP 200 (consistent state) |
**Pass criteria**: No database corruption; subsequent requests behave consistently
@@ -0,0 +1,69 @@
# Resource Limit Tests
### NFT-RES-LIM-01: Maximum Upload File Size (200 MB)
**Summary**: System accepts file uploads at the configured maximum size (200 MB).
**Traces to**: AC-13
**Preconditions**:
- System running with default Kestrel config (MaxRequestBodySize = 200 MB)
- Caller authenticated
**Monitoring**:
- API container memory usage
- Response status code
**Duration**: Single request
**Pass criteria**: HTTP 200 for 200 MB file; API memory stays below 1 GB during upload
---
### NFT-RES-LIM-02: Over-Maximum Upload File Size (201 MB)
**Summary**: System rejects file uploads exceeding the configured maximum size.
**Traces to**: AC-13
**Preconditions**:
- System running with default Kestrel config
- Caller authenticated
**Monitoring**:
- Response status code
**Duration**: Single request
**Pass criteria**: HTTP 413 (Request Entity Too Large) for 201 MB file
---
### NFT-RES-LIM-03: Memory Usage During Large File Encryption
**Summary**: Memory usage during encrypted resource download stays within acceptable bounds.
**Traces to**: AC-14
**Preconditions**:
- 100 MB test file uploaded
- User authenticated with bound hardware
**Monitoring**:
- API container memory usage (docker stats)
- Response time
**Duration**: Single download request
**Pass criteria**: API container memory peak < 500 MB; request completes within 60s
---
### NFT-RES-LIM-04: Concurrent User Connections
**Summary**: System handles multiple simultaneous authenticated requests without errors.
**Traces to**: AC-1, AC-18
**Preconditions**:
- 20 unique users in database
**Monitoring**:
- Response status codes
- Error rate
**Duration**: 60 seconds
**Pass criteria**: 20 concurrent login requests complete with 0% error rate (all HTTP 200)
+104
View File
@@ -0,0 +1,104 @@
# Security Tests
### NFT-SEC-01: Unauthenticated Access to Protected Endpoints
**Summary**: All protected endpoints reject requests without JWT token.
**Traces to**: AC-18
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | GET /users (no JWT) | HTTP 401 |
| 2 | POST /resources/get (no JWT) | HTTP 401 |
| 3 | POST /resources/check (no JWT) | HTTP 401 |
| 4 | GET /resources/get-installer (no JWT) | HTTP 401 |
| 5 | PUT /users/role (no JWT) | HTTP 401 |
| 6 | DELETE /users (no JWT) | HTTP 401 |
**Pass criteria**: All endpoints return HTTP 401 for unauthenticated requests
---
### NFT-SEC-02: Non-Admin Access to Admin Endpoints
**Summary**: Non-ApiAdmin users cannot access admin-only endpoints.
**Traces to**: AC-9
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Login as Operator role user | HTTP 200, JWT token |
| 2 | POST /users (register) with Operator JWT | HTTP 403 |
| 3 | PUT /users/role with Operator JWT | HTTP 403 |
| 4 | PUT /users/enable with Operator JWT | HTTP 403 |
| 5 | DELETE /users with Operator JWT | HTTP 403 |
**Pass criteria**: All admin endpoints return HTTP 403 for non-admin users
---
### NFT-SEC-03: Password Not Returned in User List
**Summary**: User list endpoint does not expose password hashes.
**Traces to**: AC-17
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | GET /users with ApiAdmin JWT | HTTP 200, JSON array |
| 2 | Inspect each user object in response | No `passwordHash` or `password` field present |
**Pass criteria**: Password hash is never included in API responses
---
### NFT-SEC-04: Expired JWT Token Rejection
**Summary**: Expired JWT tokens are rejected.
**Traces to**: AC-4, AC-18
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Craft a JWT with `exp` set to past timestamp (same signing key) | Token string |
| 2 | GET /users with expired JWT | HTTP 401 |
**Pass criteria**: Expired token returns HTTP 401
---
### NFT-SEC-05: Encryption Key Uniqueness
**Summary**: Different users produce different encryption keys for the same resource.
**Traces to**: AC-19
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Upload test file | HTTP 200 |
| 2 | Download encrypted file as User A | Encrypted bytes A |
| 3 | Download same file as User B (different credentials + hardware) | Encrypted bytes B |
| 4 | Compare encrypted bytes A and B | Different |
**Pass criteria**: Encrypted outputs differ between users
---
### NFT-SEC-06: Disabled User Cannot Login
**Summary**: A disabled user account cannot authenticate.
**Traces to**: AC-9
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Register user, disable via PUT /users/enable | HTTP 200 |
| 2 | Attempt POST /login with disabled user credentials | HTTP 409 or HTTP 403 |
**Pass criteria**: Disabled user cannot obtain a JWT token
+62
View File
@@ -0,0 +1,62 @@
# Test Data Management
## Seed Data Sets
| Data Set | Description | Used by Tests | How Loaded | Cleanup |
|----------|-------------|---------------|-----------|---------|
| seed-users | Default admin + uploader users from DDL | All tests requiring auth | SQL init scripts in docker-entrypoint-initdb.d | Fresh container per test run |
| test-resource-file | Small text file (1 KB) for upload/download tests | FT-P-08, FT-P-09, FT-P-10, FT-N-05 | Created by test via upload API | Deleted via ClearFolder API |
| large-resource-file | 200 MB test file for boundary testing | NFT-RES-LIM-01, NFT-RES-LIM-02 | Generated at test start | Deleted after test |
## Data Isolation Strategy
Each test run starts with a fresh Docker Compose environment. The database is initialized from scratch using the SQL scripts in `env/db/`. Tests that create users clean them up via the DELETE API. Tests that upload files clean up via the ClearFolder endpoint.
## Input Data Mapping
| Input Data File | Source Location | Description | Covers Scenarios |
|-----------------|----------------|-------------|-----------------|
| `data_parameters.md` | `_docs/00_problem/input_data/` | DB schema, API request types, config | Reference for all tests |
| `results_report.md` | `_docs/00_problem/input_data/expected_results/` | Input→expected result mapping | All tests |
## Expected Results Mapping
| Test Scenario ID | Input Data | Expected Result | Comparison Method | Tolerance | Expected Result Source |
|-----------------|------------|-----------------|-------------------|-----------|----------------------|
| FT-P-01 | Valid login request | HTTP 200, JWT token in body | exact (status), substring (token) | N/A | results_report.md #1 |
| FT-P-02 | Valid registration request | HTTP 200 | exact | N/A | results_report.md #5 |
| FT-P-03 | JWT token decode | iss, aud, exp claims | exact, numeric_tolerance | exp: ± 60s | results_report.md #4 |
| FT-P-04 | First hardware check | HTTP 200, true | exact | N/A | results_report.md #11 |
| FT-P-05 | Same hardware second check | HTTP 200, true | exact | N/A | results_report.md #12 |
| FT-P-06 | List users with admin JWT | HTTP 200, JSON array >= 1 | exact, threshold_min | N/A | results_report.md #19 |
| FT-P-07 | Filter users by email | HTTP 200, emails contain substring | exact, substring | N/A | results_report.md #20 |
| FT-P-08 | Upload resource file | HTTP 200 | exact | N/A | results_report.md #17 |
| FT-P-09 | Download encrypted resource | HTTP 200, octet-stream, non-empty | exact, threshold_min | N/A | results_report.md #14 |
| FT-P-10 | Decrypt downloaded resource | Byte-equals original | exact | N/A | results_report.md #15 |
| FT-P-11 | Change user role | HTTP 200 | exact | N/A | results_report.md #21 |
| FT-P-12 | Disable user | HTTP 200 | exact | N/A | results_report.md #22 |
| FT-P-13 | Delete user | HTTP 200 | exact | N/A | results_report.md #23 |
| FT-N-01 | Login unknown email | HTTP 409, code 10 | exact | N/A | results_report.md #2 |
| FT-N-02 | Login wrong password | HTTP 409, code 30 | exact | N/A | results_report.md #3 |
| FT-N-03 | Register short email | HTTP 400 | exact | N/A | results_report.md #6 |
| FT-N-04 | Register invalid email | HTTP 400 | exact | N/A | results_report.md #7 |
| FT-N-05 | Upload empty file | HTTP 409, code 70 | exact | N/A | results_report.md #18 |
| FT-N-06 | Hardware mismatch | HTTP 409, code 40 | exact | N/A | results_report.md #13 |
| FT-N-07 | Register duplicate email | HTTP 409, code 20 | exact | N/A | results_report.md #9 |
| FT-N-08 | Register short password | HTTP 400 | exact | N/A | results_report.md #8 |
## External Dependency Mocks
| External Service | Mock/Stub | How Provided | Behavior |
|-----------------|-----------|-------------|----------|
| PostgreSQL | Real instance | Docker container (postgres:16-alpine) | Production-equivalent behavior |
| Filesystem | Docker volume | Mounted volume on system-under-test | Real file I/O |
## Data Validation Rules
| Data Type | Validation | Invalid Examples | Expected System Behavior |
|-----------|-----------|-----------------|------------------------|
| Email | >= 8 chars, valid format | `"short"`, `"notanemail"` | HTTP 400 validation error |
| Password | >= 8 chars | `"short"` | HTTP 400 validation error |
| Hardware string | Not empty | `""` | HTTP 400 validation error |
| File upload | Non-null, <= 200 MB | null, 201 MB file | HTTP 409 (code 70), HTTP 413 |
@@ -0,0 +1,54 @@
# Traceability Matrix
## Acceptance Criteria Coverage
| AC ID | Acceptance Criterion | Test IDs | Coverage |
|-------|---------------------|----------|----------|
| AC-1 | Valid login returns JWT | FT-P-01, NFT-PERF-01, NFT-RES-01, NFT-RES-LIM-04 | Covered |
| AC-2 | Unknown email returns code 10 | FT-N-01 | Covered |
| AC-3 | Wrong password returns code 30 | FT-N-02 | Covered |
| AC-4 | JWT lifetime 4 hours | FT-P-03, NFT-SEC-04 | Covered |
| AC-5 | Email min 8 chars | FT-P-02, FT-N-03 | Covered |
| AC-6 | Email format validation | FT-P-02, FT-N-04 | Covered |
| AC-7 | Password min 8 chars | FT-P-02, FT-N-08 | Covered |
| AC-8 | Duplicate email returns code 20 | FT-N-07 | Covered |
| AC-9 | Only ApiAdmin can manage users | FT-P-06, FT-P-07, FT-P-11, FT-P-12, FT-P-13, NFT-SEC-02, NFT-SEC-06 | Covered |
| AC-10 | First hardware check stores | FT-P-04, NFT-RES-03 | Covered |
| AC-11 | Subsequent hardware check validates | FT-P-05, NFT-RES-03 | Covered |
| AC-12 | Hardware mismatch returns code 40 | FT-N-06 | Covered |
| AC-13 | Max upload 200 MB | FT-P-08, NFT-RES-LIM-01, NFT-RES-LIM-02, NFT-PERF-03 | Covered |
| AC-14 | AES-256-CBC encryption | FT-P-09, FT-P-10, NFT-PERF-02, NFT-PERF-03, NFT-RES-LIM-03 | Covered |
| AC-15 | Encrypt-decrypt round-trip | FT-P-10 | Covered |
| AC-16 | Empty file upload returns code 70 | FT-N-05 | Covered |
| AC-17 | SHA-384 password hashing | NFT-SEC-03 | Covered |
| AC-18 | All non-login endpoints require auth | FT-P-09, NFT-SEC-01, NFT-RES-02, NFT-RES-LIM-04 | Covered |
| AC-19 | Encryption key derived from email+password+hw | FT-P-10, NFT-SEC-05 | Covered |
## Restrictions Coverage
| Restriction ID | Restriction | Test IDs | Coverage |
|---------------|-------------|----------|----------|
| RESTRICT-SW-01 | .NET 10.0 runtime | All tests (implicit — Docker build uses .NET 10.0) | Covered |
| RESTRICT-SW-02 | PostgreSQL database | All DB tests (implicit — docker-compose uses PostgreSQL) | Covered |
| RESTRICT-SW-03 | Max request body 200 MB | NFT-RES-LIM-01, NFT-RES-LIM-02 | Covered |
| RESTRICT-SW-04 | JWT HMAC-SHA256 signing | FT-P-03, NFT-SEC-04 | Covered |
| RESTRICT-HW-01 | ARM64 target architecture | — | NOT COVERED — CI builds ARM64; tests run on dev x64 host |
| RESTRICT-ENV-01 | Secrets via env vars | All tests (implicit — docker-compose passes env vars) | Covered |
| RESTRICT-ENV-02 | CORS admin.azaion.com | — | NOT COVERED — CORS is browser-enforced, not testable at API level |
| RESTRICT-OP-01 | Serilog logging | — | NOT COVERED — log output verification not in scope |
## Coverage Summary
| Category | Total Items | Covered | Not Covered | Coverage % |
|----------|-----------|---------|-------------|-----------|
| Acceptance Criteria | 19 | 19 | 0 | 100% |
| Restrictions | 8 | 5 | 3 | 63% |
| **Total** | **27** | **24** | **3** | **89%** |
## Uncovered Items Analysis
| Item | Reason Not Covered | Risk | Mitigation |
|------|-------------------|------|-----------|
| RESTRICT-HW-01 (ARM64) | Tests run on x64 dev/CI host; cross-architecture testing requires ARM hardware | Low — .NET runtime handles arch differences; no arch-specific code in application | CI builds ARM64 image; manual smoke test on target device |
| RESTRICT-ENV-02 (CORS) | CORS is enforced by browsers, not by server-to-server HTTP calls | Low — CORS policy is declarative in Program.cs | Visual inspection of CORS configuration in code |
| RESTRICT-OP-01 (Logging) | Log output format/content verification adds complexity without proportional value | Low — Serilog configuration is declarative | Code review of Serilog setup |