# Arsenal IQ Arsenal IQ is a self-hosted ERP-style inventory app for recording firearms and ammunition. It tracks serial numbers, acquisition cost, estimated value, ammunition on hand by caliber, and ammo cost, with a React frontend and a Docker Compose deployment model that supports both local development and Traefik-backed production. ## Stack - Frontend: React + TypeScript + Vite - Backend: Node.js + Express + TypeScript - Database: PostgreSQL 16 - Runtime: Docker Compose - Proxy: optional Traefik production override ## Features - Firearm registry with manufacturer, model, serial number, caliber, category, storage location, and cost basis - Ammunition lot tracking with caliber, grain, quantity on hand, vendor, cost per round, and reorder thresholds - Dashboard summaries for inventory counts, invested value, and low-stock alerts - Seed data so the UI is useful immediately after first startup ## Quick start 1. Copy the environment file. ```bash cp .env.example .env ``` 2. Create the shared Traefik Docker network if you do not already have it. ```bash docker network create traefik_proxy ``` 3. Build and start the local development stack. ```bash docker compose up --build ``` ## Local endpoints - Frontend: `http://localhost:3000` - API: `http://localhost:5000/api` - Health check: `http://localhost:5000/health` - PostgreSQL: `localhost:5432` ## Development storage This project now uses local bind mounts instead of named Docker volumes: - PostgreSQL data: `./data/postgres` - Backend dependencies: `./backend/node_modules` - Frontend dependencies: `./frontend/node_modules` On first start, the backend and frontend containers will install dependencies into those local `node_modules` folders before launching. ## Production with Traefik For production, use the dedicated production compose file so Traefik labels and external networking are only enabled there. ```bash docker compose -f docker-compose.prod.yml up --build -d ``` Configure these values in `.env` for your production domain names: ```env TRAEFIK_NETWORK=traefik_proxy TRAEFIK_ENTRYPOINT=websecure TRAEFIK_WEB_HOST=arsenal.example.com TRAEFIK_API_HOST=api.arsenal.example.com ``` If the frontend should call the API through Traefik in production, set: ```env VITE_API_BASE_URL=https://api.arsenal.example.com/api FRONTEND_URL=https://arsenal.example.com ``` To disable self-service account creation and allow only existing users or SSO sign-in, set: ```env ALLOW_REGISTRATION=false ``` ## API The app includes an Express API in [backend/src/app.ts](/home/corey/github/Arsenal_IQ/backend/src/app.ts). In local development, the frontend calls: - Base URL: `http://localhost:5000/api` - Health check: `http://localhost:5000/health` ### Authentication model - Authenticated requests use `Authorization: Bearer ` - The token is returned by local login/register or after a successful SSO callback - The app is currently single-profile per user, even though profile endpoints still exist for compatibility - Most inventory routes also accept `x-profile-id`, but in the current app this resolves to the user’s single arsenal/profile ### Environment flags - `ALLOW_REGISTRATION=true|false` - Controls whether `POST /api/auth/register` is available - When `false`, the login UI hides self-service account creation ### Response shape notes - Validation and business-rule errors generally return: ```json { "error": "Human readable message" } ``` - Successful login/register responses return: ```json { "token": "session-token", "user": { "id": "uuid", "email": "owner@example.com", "name": "Owner Name" }, "profiles": [ { "id": "uuid", "name": "Owner" } ], "activeProfileId": "uuid" } ``` ### Core routes #### Service and discovery - `GET /health` - Returns service/database health - `GET /api` - Returns API metadata and `allowRegistration` Example: ```json { "name": "Arsenal IQ API", "version": "3.0.0", "allowRegistration": true, "resources": [ "/api/auth/login", "/api/auth/register", "/api/dashboard", "/api/firearms", "/api/calibers", "/api/ammo" ] } ``` #### Authentication - `GET /api/auth/providers` - Public list of enabled SSO providers for the login page - `POST /api/auth/register` - Creates a local account when registration is enabled - `POST /api/auth/login` - Signs in with local email/password - `POST /api/auth/logout` - Invalidates the current session token - `GET /api/auth/me` - Returns the current authenticated user and active profile - `GET /api/auth/sso/:providerKey/start` - Starts the OIDC login flow and returns an authorization URL - `GET /api/auth/sso/:providerKey/callback` - Handles the provider callback, creates or links a user, then redirects back to the frontend with a token Register request: ```json { "name": "Owner Name", "email": "owner@example.com", "password": "change-me" } ``` Login request: ```json { "email": "owner@example.com", "password": "change-me" } ``` SSO behavior: - If the SSO provider returns an email that matches an existing user, the SSO identity is linked to that user - If the email does not exist yet, a new user is created automatically - If the account was created via SSO only, local password login is rejected for that user #### Profiles - `GET /api/profiles` - Returns the user’s profile list and active profile ID - `POST /api/profiles` - Currently disabled and returns `403` - `POST /api/profiles/select` - Returns the active profile ID Note: - The backend still exposes these endpoints, but the product now behaves as one user per arsenal/profile #### Dashboard - `GET /api/dashboard` - Returns the current user, active profile, firearms, calibers, ammo inventory, defaults, and summary metrics used by the React UI Summary fields: - `totalFirearms` - `totalAmmoRounds` - `firearmsInvestment` - `ammoInvestment` - `configuredCalibers` #### Firearms - `GET /api/firearms` - Lists firearms for the current profile - `POST /api/firearms` - Creates a firearm - `PUT /api/firearms/:id` - Updates a firearm - `DELETE /api/firearms/:id` - Deletes a firearm Firearm body: ```json { "manufacturer": "Glock", "model": "19", "category": "Handgun", "caliber": "9mm", "serialNumber": "ABC123", "purchasePrice": 550, "acquiredOn": "2025-06-01", "imageUrl": "", "notes": "Carry pistol" } ``` Valid firearm categories: - `Handgun` - `Rifle` - `Shotgun` - `PCC` - `Other` Firearm response: ```json { "id": "uuid", "manufacturer": "Glock", "model": "19", "category": "Handgun", "caliber": "9mm", "serialNumber": "ABC123", "purchasePrice": 550, "acquiredOn": "2025-06-01", "imageUrl": null, "notes": "Carry pistol" } ``` #### Calibers - `GET /api/calibers` - Returns configured active calibers plus unused default caliber names - `POST /api/calibers` - Adds or re-enables a caliber for the current profile - `PATCH /api/calibers/:id` - Enables or disables a caliber Create caliber request: ```json { "name": "300 BLK" } ``` Toggle caliber request: ```json { "isActive": false } ``` Caliber response: ```json { "id": "uuid", "name": "9mm", "isDefault": true, "isActive": true } ``` #### Ammo - `GET /api/ammo` - Returns active caliber inventory rows for the current profile - `PATCH /api/ammo/:caliberId` - Adjusts rounds on hand and optionally updates cost per round Ammo patch request: ```json { "rounds": 250, "costPerRound": 0.24 } ``` Notes: - Positive `rounds` adds to the current count - Negative `rounds` removes from the current count - The backend clamps the final total to `0` - If `costPerRound` is omitted or `null`, the existing value is preserved Ammo response: ```json { "caliberId": "uuid", "caliber": "9mm", "roundsOnHand": 750, "costPerRound": 0.24, "totalValue": 180 } ``` #### Auth provider settings - `GET /api/settings/auth-providers` - Returns the full editable provider configuration for the authenticated settings page - `PUT /api/settings/auth-providers/:providerKey` - Updates an auth provider config such as Google, Entra, or another OIDC-compatible provider Provider update request: ```json { "displayName": "Google", "protocol": "oidc", "clientId": "your-client-id", "clientSecret": "your-client-secret", "authorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", "tokenEndpoint": "https://oauth2.googleapis.com/token", "userinfoEndpoint": "https://openidconnect.googleapis.com/v1/userinfo", "issuer": "https://accounts.google.com", "scopes": "openid profile email", "enabled": true } ``` ### Current limitations - There is no password reset or account recovery flow yet - There is no API versioning beyond the current route structure - Profile endpoints remain present, but multiple profiles are intentionally disabled in the product