A web-based asset inventory and lifecycle-tracking system for institutions (schools, universities, public offices, companies) that manage fleets of active equipment — computers, projectors, printers, network gear, and similar hardware.
Each asset is tracked through its functional state over time, every change is recorded in a tamper-evident audit journal, and the data can be exported as reports (Excel / PDF) or raw backups (JSON / CSV). User access is governed by role-based access control (RBAC), and an optional REST API lets external systems integrate with the inventory.
The end-user interface is in Romanian; this document and the codebase are in English.
- Features
- Tech stack
- Architecture
- Data model
- Roles & permissions
- Getting started (local)
- Deployment (Docker)
- Configuration
- REST API
- Security notes
- Create, edit, view, and delete equipment records (name, serial number, category, location, free-form notes).
- Functional-state tracking — every asset carries one of six lifecycle statuses:
Value Meaning (RO label) perfectStare perfectă okFunctional testingÎn testare repairÎn reparatie brokenDefect retiredScos din uz - Quick status-change action straight from the list/detail views (no full edit needed).
- Filtering & search by status, category, location, and free-text (name or serial).
- Categories management (create, rename, delete) to organize the asset fleet.
- Excel (
.xlsx) reports via ExcelJS — includes a data sheet (auto-filter enabled) plus a summary sheet with per-status counts and totals. - PDF reports via PDFKit — landscape, institution-branded header, filter summary, status breakdown, and a paginated table.
- Reports honor the same filters as the equipment list, so you can export exactly the subset you're looking at.
- Database backups: full JSON export of all tables, or per-table CSV export (UTF-8 BOM for Excel compatibility).
- First-run setup wizard — when the database has no users, the app forces creation of the initial admin account.
- Session-based login with Argon2id password hashing (with automatic rehash-on-login when parameters change).
- Three roles —
viewer,editor,admin— enforced on every route (see Roles & permissions). - Admin user management: create users, change roles, reset passwords, delete users (with guards against self-lockout).
- Self-service profile: each user can update their display name / email and change their own password.
- "Forgot password" flow generates a single-use, 30-minute, SHA-256-hashed token and emails a reset link via SMTP (Nodemailer).
- Privacy-preserving: the response is identical whether or not the email exists.
- If SMTP is not configured, the reset link is logged to the console instead — convenient for development.
- Admins can also reset any user's password directly from the admin panel.
- A full audit log records who did what and when: actor (user or API key), action, entity type, entity id, and a JSON details payload.
- Covers equipment, categories, users, API keys, and exports (create / update / delete / status_change / rename / reset_password / revoke / export …).
- Browsable in the admin UI with a configurable row limit.
- Versioned (
/api/v1/...) JSON API authenticated by API keys (sent viaX-API-KeyorAuthorization: Bearer). - API keys are created/revoked in the admin UI, stored only as SHA-256 hashes, and track
last_used_at. - Endpoints for listing statuses & categories, listing/reading equipment (with filters), creating equipment, and patching equipment status. All write actions are audited under the key's identity.
| Layer | Technology |
|---|---|
| Runtime | Node.js (v26-alpine in Docker) |
| Web framework | Express 5 |
| Views | EJS + express-ejs-layouts (server-rendered) |
| Database | PostgreSQL (via pg) |
| Cache / sessions | Redis (via ioredis + connect-redis) |
| Auth | express-session, Argon2id (argon2) |
| Nodemailer (SMTP) | |
| Reports | ExcelJS (xlsx), PDFKit (pdf) |
| Misc | morgan (logging), method-override, cookie-parser, dotenv |
| Deployment | Docker (multi-stage build) + Docker Compose |
┌──────────────────────────┐
Browser / API ───► │ Express app (Node.js) │
│ - EJS server-rendered UI│
│ - REST API (/api/v1) │
└───────┬───────────┬──────┘
│ │
sessions & │ │ data
cache ▼ ▼
┌─────────┐ ┌──────────────┐
│ Redis │ │ PostgreSQL │
└─────────┘ └──────────────┘
src/app.js— bootstrap: loads config, initializes DB / cache / mailer, wires up middleware (sessions, logging, method-override) and all route modules, then starts the HTTP server. It also exposesres.locals.userand aneedsSetupflag (true when no users exist) to every view.src/db.js— PostgreSQL pool, idempotent schema creation (CREATE TABLE IF NOT EXISTS …) on startup, default-category seeding, and the canonicalEQUIPMENT_STATUSESlist.src/cache.js— lazy Redis client; gracefully degrades to in-memory sessions and no caching ifREDIS_URLis unset or Redis is unreachable.src/auth.js—requireAuth/requireRole(...)middleware and thehasWritehelper.src/password.js— Argon2id hashing / verification / rehash detection.src/mailer.js— Nodemailer transport + password-reset email (console fallback when SMTP is absent).src/audit.js—logAudit(...)plus actor helpers for sessions and API keys.src/routes/*.routes.js— feature-scoped routers:auth,equipment,categories,admin,backup,reports,profile,api.src/views/— EJS templates (layout, partials/nav, and one folder per feature area).
Routes are organized so that each module receives the shared db pool via a factory function (equipmentRoutes(db), etc.), keeping dependencies explicit and testable.
Tables are auto-created on first start (see src/db.js):
users—username(unique),password_hash,display_name,email(case-insensitive unique),role(viewer/editor/admin),created_at.categories—name(unique); seeded with a defaultGeneralcategory.equipment—name,serial,category_id(FK → categories,ON DELETE SET NULL),status(checked enum),location,notes,created_at,updated_at.api_keys—name,key_hash,created_by(FK → users),created_at,last_used_at,revoked.password_resets—user_id(FK → users,ON DELETE CASCADE),token_hash(unique),expires_at,used_at.audit_log—created_at,actor_id,actor_label,action,entity_type,entity_id,details(indexed oncreated_at).
| Capability | viewer | editor | admin |
|---|---|---|---|
| View equipment / categories / reports | ✅ | ✅ | ✅ |
| Export reports (xlsx / pdf) | ✅ | ✅ | ✅ |
| Create / edit equipment | — | ✅ | ✅ |
| Change equipment status | — | ✅ | ✅ |
| Create / rename categories | — | ✅ | ✅ |
| Delete equipment / categories | — | — | ✅ |
| Manage users & roles | — | — | ✅ |
| Manage API keys | — | — | ✅ |
| View audit log | — | — | ✅ |
| Database export / backup | — | — | ✅ |
Every authenticated user can edit their own profile and password.
- Node.js 18+ (the Docker image uses Node 26)
- A running PostgreSQL instance
- (Optional) a running Redis instance — without it, sessions fall back to in-memory and caching is disabled
# 1. Install dependencies
npm install
# 2. Create your environment file
cp .env.example .env
# then edit .env — at minimum set DATABASE_URL and a strong SESSION_SECRET
# 3. Run
npm run dev # development (auto-reload via nodemon)
# or
npm start # productionOpen http://localhost:3000. On first launch (no users yet) you'll be redirected to /setup to create the initial admin account.
The project ships with a multi-stage Dockerfile and a Docker Compose stack that provisions the full infrastructure (app + PostgreSQL + Redis).
- builder stage (
node:26-alpine) — installs production dependencies only (npm ci --omit=dev). - runtime stage (
node:26-alpine) — copies installednode_modulesand the app source, runs as the unprivilegednodeuser, exposes port3000, and startsnode src/app.js.
This keeps the final image small and free of build-only tooling.
docker-compose.yaml defines three services:
app— the inventory application (build/tag the image asinventory_base_image), published on3000:3000, reading.env.db— PostgreSQL, with data persisted to a host volume./postgres_data.cache— Redis.
All services use restart: unless-stopped.
# 1. Build the application image (tag must match docker-compose.yaml)
docker build -t inventory_base_image .
# 2. Prepare environment
cp .env.example .env
# For Compose, the service hostnames are 'db' and 'cache', e.g.:
# DATABASE_URL=postgresql://user:password@db:5432/inventory_app
# REDIS_URL=redis://cache:6379
# Also set Postgres' own vars (POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB)
# in .env so the db service and DATABASE_URL stay in sync.
# 3. Launch the stack
docker compose up -d
# 4. Tail logs / check status
docker compose logs -f appThe app will be available on http://localhost:3000. The database schema is created automatically on first boot, and PostgreSQL data survives restarts thanks to the ./postgres_data volume.
Note: because
dbandappboth read.env, make sure the credentials inDATABASE_URLmatch thePOSTGRES_*variables the Postgres image expects.
All configuration is via environment variables (see .env.example):
| Variable | Description | Default |
|---|---|---|
PORT |
HTTP port the app listens on | 3000 |
SESSION_SECRET |
Secret used to sign session cookies — set a strong value | dev_secret (dev only) |
DATABASE_URL |
PostgreSQL connection string | local default in code |
REDIS_URL |
Redis connection string; empty disables Redis (in-memory sessions) | (empty) |
INSTITUTION_NAME |
Header shown on generated PDF reports | Inventar IT |
SMTP_HOST |
SMTP server host; if empty, reset links are logged to console | (empty) |
SMTP_PORT |
SMTP port | 587 |
SMTP_SECURE |
true for port 465 (SMTPS), false for 587/25 (STARTTLS) |
false |
SMTP_USER |
SMTP username | (empty) |
SMTP_PASS |
SMTP password | (empty) |
MAIL_FROM |
"From" address used for outgoing email | no-reply@localhost |
Base path: /api/v1. Authenticate with an API key (created in Admin → API Keys) sent as either header:
X-API-Key: <key>
# or
Authorization: Bearer <key>
| Method & path | Description |
|---|---|
GET /api/v1/statuses |
List the equipment status enum |
GET /api/v1/categories |
List categories |
GET /api/v1/equipment |
List equipment (filters: status, categoryId, location, q) |
GET /api/v1/equipment/:id |
Get a single equipment record |
POST /api/v1/equipment |
Create equipment (name required) |
PATCH /api/v1/equipment/:id/status |
Update an equipment's status |
Example:
curl -H "X-API-Key: $KEY" \
"http://localhost:3000/api/v1/equipment?status=repair"
curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"Projector A1","serial":"PRJ-001","status":"ok"}' \
http://localhost:3000/api/v1/equipmentAll write operations through the API are recorded in the audit log under the API key's identity.
- Passwords are hashed with Argon2id (19 MiB memory cost, time cost 2) and rehashed automatically on login if parameters change.
- Session cookies are
httpOnly,sameSite=lax, and signed; sessions are stored in Redis when available. - API keys and password-reset tokens are persisted only as SHA-256 hashes — the plaintext key is shown once at creation time.
- Password-reset tokens are single-use and expire after 30 minutes.
- The "forgot password" endpoint returns an identical response regardless of whether the email exists (no account enumeration).
- All SQL uses parameterized queries; export endpoints restrict columns/tables to an explicit allow-list.
For production, always set a strong
SESSION_SECRET, serve behind HTTPS (and consider enablingcookie.secure), and use real SMTP credentials.