Files
Arsenal_IQ/README.md
T
2026-03-26 09:59:40 -04:00

389 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <token>`
- 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 users 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
- `ALLOW_DEMO_ACCOUNT=true|false`
- Enables a backend-managed demo account and a demo sign-in button on the login page
- `DEMO_ACCOUNT_EMAIL`
- `DEMO_ACCOUNT_PASSWORD`
- `DEMO_ACCOUNT_NAME`
### 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/demo`
- Signs in to the configured demo account when demo mode is enabled
- `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 users 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