Files
boc/aamos-admin-upgrade/README.md
T

1173 lines
28 KiB
Markdown
Raw Normal View History

# AAMOS Admin
Administration console for the AAMOS platform — system dashboard, module control, user management, and real-time metrics.
**Version:** 1.0.0
**Stack:** Go 1.25 · Rust 2021 · SwiftUI (iOS) · Jetpack Compose (Android) · Vanilla HTML/JS (Web)
---
## Table of Contents
1. [Architecture](#architecture)
2. [Prerequisites](#prerequisites)
3. [Environment Variables](#environment-variables)
4. [Go Backend — Setup & Run](#go-backend)
5. [Rust Core — Build](#rust-core)
6. [Web — Deploy](#web-deploy)
7. [iOS — Xcode Setup](#ios-xcode-setup)
8. [Android — Android Studio Setup](#android-android-studio-setup)
9. [API Reference](#api-reference)
10. [WebSocket](#websocket)
11. [Database Schema](#database-schema)
12. [Deployment Guide](#deployment-guide)
---
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ AAMOS Admin │
│ │
│ Clients Backend │
│ ───────── ───────── │
│ │
│ ┌──────────┐ ┌─────────────────────────────┐ │
│ │ Web UI │─── HTTP/WS ──▶ │ Go HTTP Server :8080 │ │
│ │ (static) │ │ │ │
│ └──────────┘ │ /health HealthH. │ │
│ │ /auth/* AuthH. │ │
│ ┌──────────┐ │ /api/v1/ ─────────── │ │
│ │ iOS App │─── HTTPS ───▶ │ dashboard DashboardH. │ │
│ │ SwiftUI │ │ metrics MetricsH. │ │
│ └──────────┘ │ modules ModulesH. │ │
│ │ audit AuditH. │ │
│ ┌──────────┐ │ settings SettingsH. │ │
│ │ Android │─── HTTPS ───▶ │ users UsersH. │ │
│ │ Compose │ │ onboarding OnboardH. │ │
│ └──────────┘ │ /ws WebSocketH. │ │
│ └─────────────┬───────────────┘ │
│ │ │
│ ┌─────────────▼───────────────┐ │
│ │ PostgreSQL │ │
│ │ users · modules · audit │ │
│ │ settings · onboarding │ │
│ └─────────────────────────────┘ │
│ │
│ Security Layer │
│ ────────────── │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Rust Core (aamos-core.so) │ │
│ │ │ │
│ │ crypto.rs argon2 + random bytes │ │
│ │ jwt.rs token sign / verify (HS256) │ │
│ │ ratelimit.rs sliding-window rate limiting │ │
│ │ sanitize.rs input sanitisation / escaping │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Middleware chain (every request) │
│ Request ──▶ CORS ──▶ Logging ──▶ JWTAuth* ──▶ Handler │
│ (* public routes bypass JWTAuth) │
└─────────────────────────────────────────────────────────────────┘
```
### Directory Layout
```
aamos-admin/
├── backend/ Go HTTP API
│ ├── config/ Environment config loader
│ ├── db/ PostgreSQL connection + auto-migration
│ ├── handlers/ HTTP handler implementations
│ ├── middleware/ CORS, logging, JWT auth
│ ├── models/ Shared Go types
│ ├── go.mod
│ └── go.sum
├── core/ Rust security library
│ ├── src/
│ │ ├── lib.rs
│ │ ├── crypto.rs
│ │ ├── jwt.rs
│ │ ├── ratelimit.rs
│ │ └── sanitize.rs
│ └── Cargo.toml
├── web/ Static admin console
│ ├── index.html Main dashboard
│ ├── modules.html Module management
│ └── assets/
├── ios/ iOS native app (SwiftUI)
│ └── AamosAdmin/
│ ├── AamosAdmin.xcodeproj
│ ├── Services/ AuthService (Keychain)
│ ├── ViewModels/ Auth, Dashboard
│ ├── Views/ Login, Modules, Audit, Settings
│ ├── Components/ HealthRing, MetricCard, ModuleToggleRow
│ └── Models/
├── android/ Android native app (Compose)
│ ├── app/
│ │ └── src/main/java/com/aamos/admin/
│ ├── app/build.gradle.kts
│ └── settings.gradle.kts
└── shared/ Shared TypeScript types & constants
```
---
## Prerequisites
| Tool | Minimum Version | Install |
|------|----------------|---------|
| Go | 1.25.0 | `brew install go` / go.dev/dl |
| Rust + Cargo | 1.78 | `curl https://sh.rustup.rs -sSf \| sh` |
| PostgreSQL | 14 | `brew install postgresql@14` |
| Xcode | 16.0 | Mac App Store |
| Android Studio | Meerkat (2025.1) | developer.android.com |
| Node.js | 20+ (web serve only) | `brew install node` |
---
## Environment Variables
All variables are read at startup. Missing variables fall back to the listed defaults.
| Variable | Default | Required in prod | Description |
|----------|---------|-----------------|-------------|
| `PORT` | `8080` | — | HTTP listen port |
| `DB_URL` | `postgres://postgres:postgres@localhost:5432/aamos?sslmode=disable` | **YES** | PostgreSQL connection string |
| `JWT_SECRET` | `change-me-in-production` | **YES** | HMAC-SHA256 signing secret (min 32 chars) |
| `AMOS_BASE_URL` | `http://localhost:9000` | — | Base URL for upstream AMOS service calls |
| `CORS_ORIGINS` | `http://localhost:3000` | — | Comma-separated allowed CORS origins |
Create a local `.env` file (not committed):
```bash
PORT=8080
DB_URL=postgres://postgres:secret@localhost:5432/aamos?sslmode=disable
JWT_SECRET=replace-with-64-char-random-string
AMOS_BASE_URL=http://localhost:9000
CORS_ORIGINS=http://localhost:3000,http://localhost:8080
```
---
## Go Backend
### 1. Install dependencies
```bash
cd backend
go mod download
```
### 2. Start PostgreSQL
```bash
# macOS (Homebrew)
brew services start postgresql@14
# Linux
sudo systemctl start postgresql
# Create database
createdb aamos
```
The backend runs auto-migration on startup — no manual schema steps required.
### 3. Run in development
```bash
cd backend
# Export env vars, then:
go run ./cmd/main.go
```
Or inline:
```bash
DB_URL="postgres://postgres:secret@localhost:5432/aamos?sslmode=disable" \
JWT_SECRET="dev-secret-change-me" \
go run ./cmd/main.go
```
Server starts on `http://localhost:8080`.
### 4. Build binary
```bash
cd backend
CGO_ENABLED=0 GOOS=linux go build -o aamos-admin ./cmd/main.go
./aamos-admin
```
### 5. Run tests
```bash
cd backend
go test ./...
```
---
## Rust Core
The `aamos-core` crate compiles as both a dynamic library (`cdylib`) and a Rust library (`rlib`). The Go backend links against the compiled `.so` at runtime for cryptographic operations.
### Build
```bash
cd core
# Development
cargo build
# Production (optimised — LTO, abort on panic)
cargo build --release
```
Output artifacts:
| File | Platform |
|------|---------|
| `target/release/libaamos_core.so` | Linux |
| `target/release/libaamos_core.dylib` | macOS |
| `target/release/libaamos_core.a` | Static archive |
### Run tests
```bash
cd core
cargo test
```
### Modules
| Module | Purpose |
|--------|---------|
| `crypto` | Argon2id password hashing, random byte generation |
| `jwt` | HS256 token sign and verify |
| `ratelimit` | Sliding-window rate limiting (no external state) |
| `sanitize` | Input sanitisation: HTML escape, trim, length enforcement |
---
## Web Deploy
The web frontend is a static site — no build step required.
### Local development
```bash
cd web
python3 -m http.server 3000
# or
npx serve .
```
Open `http://localhost:3000`.
### Production (nginx)
```nginx
server {
listen 80;
server_name admin.example.com;
root /opt/aamos-admin/web;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /auth/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /ws {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
```
---
## iOS — Xcode Setup
### Requirements
- Mac with Apple silicon or Intel
- Xcode 16.0 or later
- iOS 18.0+ device or simulator
### Step-by-step
**1. Open the project**
```
File > Open...
```
Navigate to `aamos-admin/ios/` and select `AamosAdmin.xcodeproj`.
**2. Set the development team**
- In the Project navigator, select **AamosAdmin** (root)
- Select the **AamosAdmin** target
- Open the **Signing & Capabilities** tab
- Under **Signing**, set **Team** to your Apple Developer account
**3. Configure the API base URL**
The base URL is set in `AuthViewModel.swift`:
```
AamosAdmin/ViewModels/AuthViewModel.swift
```
Change the `baseURL` constant:
```swift
private let baseURL = "https://admin-api.example.com"
```
For local development against the Go backend:
```swift
private let baseURL = "http://localhost:8080"
```
> iOS blocks HTTP by default. For local HTTP targets, add an App Transport Security exception in `Info.plist`:
>
> ```xml
> <key>NSAppTransportSecurity</key>
> <dict>
> <key>NSAllowsArbitraryLoads</key>
> <true/>
> </dict>
> ```
**4. Select a simulator or device**
- Click the device selector in the Xcode toolbar
- Choose **iPhone 16 Pro** (or any iOS 18+ simulator)
- Or connect a physical device
**5. Build and run**
```
Product > Run (⌘R)
```
**6. Authentication notes**
The app stores JWT tokens in the iOS Keychain with `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`. The token persists across restarts. To clear it during development, delete and reinstall the app.
### Project reference
| Setting | Value |
|---------|-------|
| Bundle ID | `com.aamos.admin` |
| Keychain service | `com.aamos.admin` |
| Min deployment target | iOS 18.0 |
| UI framework | SwiftUI |
### App structure
```
AamosAdmin/
├── Services/
│ └── AuthService.swift Keychain JWT storage (read/write/delete)
├── ViewModels/
│ ├── AuthViewModel.swift Login / logout state machine
│ └── DashboardViewModel.swift Live metric polling
├── Views/
│ ├── LoginView.swift
│ ├── DashboardView.swift
│ ├── ModulesView.swift
│ ├── AuditView.swift
│ └── SettingsView.swift
└── Components/
├── HealthRing.swift System health ring visualisation
├── MetricCard.swift CPU / RAM / disk display card
└── ModuleToggleRow.swift Module enable / disable row
```
---
## Android — Android Studio Setup
### Requirements
- Android Studio Meerkat (2025.1) or later
- JDK 17 (bundled with Android Studio)
- Android SDK API 35 (install via SDK Manager)
- Device or emulator running API 26+ (Android 8.0)
### Step-by-step
**1. Open the project**
```
File > Open...
```
Select the `aamos-admin/android/` folder. Android Studio detects `settings.gradle.kts` and imports automatically. Wait for the initial Gradle sync (first run downloads ~500 MB).
**2. Gradle sync**
If sync does not start:
```
File > Sync Project with Gradle Files
```
**3. Configure the API base URL**
Locate the Retrofit base URL constant in the DI network module (e.g. `di/NetworkModule.kt`):
```kotlin
private const val BASE_URL = "https://admin-api.example.com/"
```
For the Android Emulator (routes `10.0.2.2` to host `localhost`):
```kotlin
private const val BASE_URL = "http://10.0.2.2:8080/"
```
For a physical device on the same LAN as the dev machine:
```kotlin
private const val BASE_URL = "http://192.168.1.x:8080/"
```
**4. Create or start an emulator**
```
Tools > Device Manager > Create Device
```
Choose **Pixel 9 Pro****API 35** system image → Finish.
**5. Build and run**
```
Run > Run 'app' (Shift+F10)
```
**6. Build release APK / AAB**
```
Build > Generate Signed App Bundle / APK...
```
Release builds enable ProGuard (`isMinifyEnabled = true`) and resource shrinking.
### Project configuration
| Setting | Value |
|---------|-------|
| Application ID | `com.aamos.admin` |
| Min SDK | API 26 (Android 8.0) |
| Target SDK | API 35 (Android 15) |
| Kotlin | 2.1.0 |
| Compose BOM | 2025.04.01 |
| Hilt | 2.56.1 |
| Retrofit | 2.11.0 |
| OkHttp | 4.12.0 |
| Navigation Compose | 2.8.9 |
| Lifecycle / ViewModel | 2.9.0 |
---
## API Reference
**Base URL (development):** `http://localhost:8080`
All endpoints that require authentication expect:
```
Authorization: Bearer <jwt-token>
```
JWT tokens expire after **24 hours**.
---
### Health
#### `GET /health`
No authentication required.
```bash
curl http://localhost:8080/health
```
```json
{
"ok": true,
"version": "1.0.0",
"uptime": "4h32m15s"
}
```
---
### Authentication
#### `POST /auth/login`
```bash
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "admin@example.com", "password": "secret"}'
```
**200 OK**
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "01234567-89ab-cdef-0123-456789abcdef",
"email": "admin@example.com",
"name": "Admin User",
"role": "admin"
}
}
```
**401 Unauthorized** — invalid credentials
```json
{ "error": "invalid credentials" }
```
---
#### `POST /auth/logout`
JWT is stateless — the client discards the token. Server confirms.
```bash
curl -X POST http://localhost:8080/auth/logout \
-H "Authorization: Bearer $TOKEN"
```
```json
{ "message": "logged out" }
```
---
#### `GET /auth/me`
```bash
curl http://localhost:8080/auth/me \
-H "Authorization: Bearer $TOKEN"
```
```json
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"email": "admin@example.com",
"name": "Admin User",
"role": "admin"
}
```
---
### Dashboard
#### `GET /api/v1/dashboard`
Aggregated view: system metrics, module states, and recent audit summary.
```bash
curl http://localhost:8080/api/v1/dashboard \
-H "Authorization: Bearer $TOKEN"
```
---
### System Metrics
#### `GET /api/v1/metrics`
Live CPU, RAM, disk, and process counts. CPU is sampled twice with a 200 ms interval.
```bash
curl http://localhost:8080/api/v1/metrics \
-H "Authorization: Bearer $TOKEN"
```
```json
{
"timestamp": "2026-05-10T14:23:00Z",
"cpu": { "usage_percent": 12.50 },
"ram": {
"total_bytes": 8589934592,
"used_bytes": 4123456789,
"free_bytes": 1073741824,
"available_bytes": 2147483648,
"usage_percent": 48.01
},
"disk": {
"path": "/",
"total_bytes": 107374182400,
"used_bytes": 53687091200,
"free_bytes": 53687091200,
"usage_percent": 50.00
},
"processes": { "total": 312, "running": 4 }
}
```
---
### Modules
The 10 Ouroboros modules are seeded automatically on first start.
#### `GET /api/v1/modules`
```bash
curl http://localhost:8080/api/v1/modules \
-H "Authorization: Bearer $TOKEN"
```
```json
[
{
"id": "academy",
"name": "Academy",
"description": "Learning & Training Platform",
"icon": "graduation-cap",
"enabled": false,
"status": "stopped"
},
{
"id": "finance",
"name": "Finance",
"description": "Financial Management & Reporting",
"icon": "chart-line",
"enabled": true,
"status": "running"
}
]
```
**Module catalogue:**
| ID | Name | Description |
|----|------|-------------|
| `academy` | Academy | Learning & Training Platform |
| `finance` | Finance | Financial Management & Reporting |
| `compliance` | Compliance | Regulatory Compliance & Auditing |
| `people` | People | HR & People Management |
| `operations` | Operations | Operations & Process Management |
| `commerce` | Commerce | Sales & Commerce Platform |
| `identity` | Identity | Identity & Access Management |
| `connect` | Connect | Communications & Integration Hub |
| `governance` | Governance | Corporate Governance & Policy |
| `analytics` | Analytics | Data Analytics & Business Intelligence |
#### `PUT /api/v1/modules/{id}/toggle`
Flips `enabled` and syncs `status` (`running``stopped`).
```bash
curl -X PUT http://localhost:8080/api/v1/modules/finance/toggle \
-H "Authorization: Bearer $TOKEN"
```
```json
{
"id": "finance",
"name": "Finance",
"description": "Financial Management & Reporting",
"icon": "chart-line",
"enabled": true,
"status": "running"
}
```
---
### Audit Log
#### `GET /api/v1/audit`
Paginated, newest first.
**Query parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `page` | int | `1` | Page number |
| `limit` | int | `20` | Page size (max 100) |
| `user` | string | — | Filter by `user_id` |
| `action` | string | — | Filter by action name |
| `resource` | string | — | Filter by resource name |
```bash
curl "http://localhost:8080/api/v1/audit?page=1&limit=5&action=login" \
-H "Authorization: Bearer $TOKEN"
```
```json
{
"data": [
{
"id": "a1b2c3d4-...",
"user_id": "01234567-...",
"action": "login",
"resource": "auth",
"ip": "127.0.0.1",
"timestamp": "2026-05-10T14:00:00Z",
"details": ""
}
],
"total": 42,
"page": 1,
"limit": 5
}
```
---
### Settings
#### `GET /api/v1/settings`
```bash
curl http://localhost:8080/api/v1/settings \
-H "Authorization: Bearer $TOKEN"
```
```json
{
"maintenance_mode": "false",
"max_login_attempts": "5"
}
```
#### `PUT /api/v1/settings`
Upserts one or more key-value pairs atomically.
```bash
curl -X PUT http://localhost:8080/api/v1/settings \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"maintenance_mode": "true", "max_login_attempts": "3"}'
```
Returns the updated keys:
```json
{
"maintenance_mode": "true",
"max_login_attempts": "3"
}
```
---
### Users
#### `GET /api/v1/users?page=1&limit=20`
```bash
curl "http://localhost:8080/api/v1/users?page=1&limit=20" \
-H "Authorization: Bearer $TOKEN"
```
```json
{
"users": [
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"email": "admin@example.com",
"name": "Admin User",
"role": "admin",
"created_at": "2026-01-01T00:00:00Z",
"last_login": "2026-05-10T14:00:00Z"
}
],
"total": 1,
"page": 1,
"limit": 20
}
```
#### `POST /api/v1/users`
Role must be `admin` or `user` (defaults to `user`).
```bash
curl -X POST http://localhost:8080/api/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"name": "Alice",
"role": "user",
"password": "strong-password-here"
}'
```
**201 Created**
```json
{
"id": "abcdef01-...",
"email": "alice@example.com",
"name": "Alice",
"role": "user",
"created_at": "2026-05-10T14:30:00Z",
"last_login": "2026-05-10T14:30:00Z"
}
```
**409 Conflict** — email already exists
#### `GET /api/v1/users/{id}`
```bash
curl http://localhost:8080/api/v1/users/abcdef01-... \
-H "Authorization: Bearer $TOKEN"
```
#### `PUT /api/v1/users/{id}`
Partial update. Omit `role` to preserve the existing value.
```bash
curl -X PUT http://localhost:8080/api/v1/users/abcdef01-... \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Alice Smith", "role": "admin"}'
```
#### `DELETE /api/v1/users/{id}`
Soft delete — sets `deleted_at`. The user disappears from all list and lookup queries.
```bash
curl -X DELETE http://localhost:8080/api/v1/users/abcdef01-... \
-H "Authorization: Bearer $TOKEN"
```
**204 No Content**
---
### Onboarding
#### `POST /api/v1/onboarding/start`
Creates an onboarding session.
```bash
curl -X POST http://localhost:8080/api/v1/onboarding/start \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"steps": ["create_admin", "enable_modules", "configure_settings"]}'
```
**201 Created**
```json
{
"id": "d4e5f6a7b8c9...",
"steps": ["create_admin", "enable_modules", "configure_settings"],
"started_at": "2026-05-10T14:00:00Z",
"status": "pending"
}
```
---
## WebSocket
Real-time system metrics are pushed to every connected client every **5 seconds**.
**Endpoint:** `ws://localhost:8080/ws`
**Frame format** — same shape as `GET /api/v1/metrics`:
```json
{
"timestamp": "2026-05-10T14:23:05Z",
"cpu": { "usage_percent": 8.25 },
"ram": {
"total_bytes": 8589934592,
"used_bytes": 4000000000,
"free_bytes": 1000000000,
"available_bytes": 2000000000,
"usage_percent": 46.57
},
"disk": {
"path": "/",
"total_bytes": 107374182400,
"used_bytes": 50000000000,
"free_bytes": 50000000000,
"usage_percent": 46.57
},
"processes": { "total": 310, "running": 3 }
}
```
**Browser example:**
```javascript
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onmessage = (event) => {
const m = JSON.parse(event.data);
console.log('CPU:', m.cpu.usage_percent, '%');
};
```
**Keepalive:** Server sends a WebSocket `PING` every 54 seconds. Clients that do not respond within 60 seconds are disconnected.
---
## Database Schema
Auto-migrated on every backend startup (idempotent — safe to restart).
```sql
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL DEFAULT '',
role TEXT NOT NULL DEFAULT 'user', -- 'admin' | 'user'
password_hash TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_login TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ -- soft delete
);
CREATE TABLE IF NOT EXISTS modules (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
enabled BOOLEAN NOT NULL DEFAULT FALSE,
description TEXT NOT NULL DEFAULT '',
icon TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'stopped' -- 'running' | 'stopped'
);
CREATE TABLE IF NOT EXISTS audit_logs (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE SET NULL,
action TEXT NOT NULL,
resource TEXT NOT NULL DEFAULT '',
ip TEXT NOT NULL DEFAULT '',
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
details TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_logs_timestamp ON audit_logs(timestamp DESC);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL DEFAULT '',
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS onboarding_sessions (
id TEXT PRIMARY KEY,
steps TEXT NOT NULL, -- JSON array
started_at TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL DEFAULT 'pending'
);
```
Connection pool (configured in `db/postgres.go`):
| Setting | Value |
|---------|-------|
| Max open connections | 25 |
| Max idle connections | 10 |
| Max connection lifetime | 5 minutes |
---
## Deployment Guide
### Docker (single container)
```dockerfile
# backend/Dockerfile
FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /aamos-admin ./cmd/main.go
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /aamos-admin /aamos-admin
EXPOSE 8080
ENTRYPOINT ["/aamos-admin"]
```
```bash
docker build -t aamos-admin:latest ./backend
docker run -d \
--name aamos-admin \
-p 8080:8080 \
-e DB_URL="postgres://user:pass@db:5432/aamos?sslmode=disable" \
-e JWT_SECRET="$(openssl rand -base64 48)" \
-e CORS_ORIGINS="https://admin.example.com" \
aamos-admin:latest
```
### Docker Compose (backend + database)
```yaml
services:
db:
image: postgres:15-alpine
restart: unless-stopped
environment:
POSTGRES_DB: aamos
POSTGRES_USER: aamos
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U aamos"]
interval: 5s
timeout: 3s
retries: 5
backend:
build: ./backend
restart: unless-stopped
depends_on:
db:
condition: service_healthy
ports:
- "8080:8080"
environment:
DB_URL: postgres://aamos:${DB_PASSWORD}@db:5432/aamos?sslmode=disable
JWT_SECRET: ${JWT_SECRET}
CORS_ORIGINS: ${CORS_ORIGINS}
PORT: "8080"
volumes:
pgdata:
```
```bash
# .env
DB_PASSWORD=strong-db-password
JWT_SECRET=$(openssl rand -base64 48)
CORS_ORIGINS=https://admin.example.com
docker compose up -d
docker compose logs -f backend
```
### systemd (bare metal / VM)
```ini
# /etc/systemd/system/aamos-admin.service
[Unit]
Description=AAMOS Admin Backend
After=network.target postgresql.service
[Service]
Type=simple
User=aamos
WorkingDirectory=/opt/aamos-admin/backend
EnvironmentFile=/etc/aamos-admin/env
ExecStart=/opt/aamos-admin/backend/aamos-admin
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
```
```bash
# /etc/aamos-admin/env
PORT=8080
DB_URL=postgres://aamos:secret@localhost:5432/aamos?sslmode=disable
JWT_SECRET=replace-with-64-char-random-string
CORS_ORIGINS=https://admin.example.com
sudo systemctl daemon-reload
sudo systemctl enable --now aamos-admin
sudo systemctl status aamos-admin
```
### First-run: create admin user
After the backend starts for the first time, create an admin account:
```bash
curl -X POST http://localhost:8080/api/v1/users \
-H "Content-Type: application/json" \
-d '{
"email": "admin@example.com",
"name": "Administrator",
"role": "admin",
"password": "change-me-immediately"
}'
```
Obtain a token:
```bash
TOKEN=$(curl -s -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"change-me-immediately"}' \
| jq -r .token)
```
Verify:
```bash
curl http://localhost:8080/health
# {"ok":true,"version":"1.0.0","uptime":"0s"}
```
### iOS — App Store / TestFlight
1. **Product > Archive** in Xcode
2. In Organizer: **Distribute App > App Store Connect**
3. Follow the upload wizard — select the provisioning profile for `com.aamos.admin`
4. In App Store Connect, submit the build to TestFlight for internal testing
### Android — Google Play
```bash
cd android
./gradlew :app:bundleRelease
# Output: app/build/outputs/bundle/release/app-release.aab
```
Sign the AAB with your upload key and upload via the Google Play Console.