Skip to content

About

Backend API for an organizational request and approval management system.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ReqFlow Backend

This file did not exist before F6.5 — many inline code comments across the codebase (M0–M11) reference a "README" that was never actually written. Rather than retroactively reconstruct that history, this file documents only what F6.5 (Admin Bootstrap + User Management) added. Earlier milestones remain documented solely in their own inline code comments and tests.

Demo Dataset (npm run seed:demo)

Run the dedicated local demo seed after migrations:

npm run prisma:deploy
npm run seed:demo

The command reuses the Acme Indonesia organization, upserts the five standard departments and stable demo users, then replaces only requests whose numbers start with DEMO-REQ-. It never deletes unrelated local requests or users, and it is safe to rerun without accumulating duplicates. The dataset contains 27 requests spread across more than one year, valid approval cycles, comments, read/unread notifications, and chronological audit activity. Attachment fixtures are deliberately omitted because this deployment stores attachment bytes on disk; metadata without owned files would create broken downloads.

All accounts below use the shared password ReqFlowDemo123! and have mustChangePassword=false. These credentials are DEVELOPMENT / DEMO ONLY and must not be used in production.

Role Email
Admin siti.demo@reqflow.local
Employee andi.demo@reqflow.local
Manager rina.demo@reqflow.local
Finance dewi.demo@reqflow.local
Director bambang.demo@reqflow.local

Admin Bootstrap (npm run create:admin)

Purpose

Bootstraps the very first ReqFlow Administrator (and, if needed, its Organization) from a trusted backend terminal. This is not a public registration endpoint — ReqFlow has no /register route and no self-service sign-up page. After the first Admin exists, every other user is created by an Admin through the authenticated /users API (user.service.ts), never through this script.

The command never starts an HTTP server, never bypasses password hashing, and never prints a password, password hash, or DATABASE_URL.

Usage

npm run create:admin

Prompts interactively for organization name, admin name, admin email, and password (entered with hidden/starred input, with a confirmation prompt).

Non-interactive / scripted bootstrap:

npm run create:admin -- --organizationName "Acme Inc" --name "System Admin" --email admin@example.com

The password is never accepted as a command-line flag (it would leak into shell history). When stdin is a real terminal, it's prompted for directly. When stdin is not a terminal (CI, scripted deploys), the entire stdin stream is used as the password instead:

echo "the-password" | npm run create:admin -- --organizationName "Acme Inc" --name "System Admin" --email admin@example.com

Organization behavior

The organization name is matched by a deterministic, counter-free slug (the same upsert-by-slug pattern prisma/seed.ts already uses): running the command twice with the same organization name reuses that organization rather than creating a duplicate. A new name always creates a new organization — this never happens silently from the operator's point of view, since the name is always explicitly provided.

Duplicate / safety behavior

  • Running the command again with an email that already exists fails safely with a clear message and a non-zero exit code — it never silently creates a duplicate and never resets an existing user's password (this is create, not reset-password).
  • Organization creation (or reuse) and Admin user creation happen inside a single database transaction, so a failure partway through never leaves an organization with no administrator.
  • Password hashing reuses hashPassword() (Argon2id) — the exact function normal registration/login uses — so a CLI-created Admin authenticates exactly like any other user.

Testability

The business logic lives in src/scripts/lib/create-admin.ts (createAdminUser()), independent of any terminal I/O, and is covered directly by tests/create-admin.test.ts. src/scripts/create-admin.ts is only the thin CLI adapter (argument parsing, prompts, console output).

User Management (F6.5)

Authority & tenant isolation

Every User Management endpoint below is Admin-only (requireRole(Role.ADMIN)) and always scoped to the authenticated Admin's own organizationId — a request body's organizationId (if present) is always ignored; tenant scope is never client-controlled. Cross-organization targets resolve to 404 USER_NOT_FOUND, matching this codebase's existing "don't reveal what you can't access" convention elsewhere (e.g. Request visibility).

Endpoints

Method Path Purpose
GET /users/me Own profile (any authenticated user)
GET /users List, paginated, filterable (Admin)
POST /users Create a user (Admin)
GET /users/:id Detail (Admin)
PATCH /users/:id Edit name/email (Admin)
PATCH /users/:id/status Activate/deactivate (Admin)
PATCH /users/:id/department Assign/unassign department (Admin)
PATCH /users/:id/role Change role (Admin)

GET /users filters: q (matches name or email, case-insensitive), role, departmentId, isActive, plus page/pageSize. All optional and additive over the base organization scope — never fetched unfiltered and filtered client-side.

The user DTO (AdminUserSummary) never includes passwordHash or any other authentication internal.

User lifecycle: activation, not hard-delete

User has no hard-delete endpoint. Request.createdBy, Approval.approver, Comment.author, and Attachment.uploadedBy are all onDelete: Restrict in the schema — a User with any history literally cannot be hard-deleted at the database level. User.isActive (default true, additive migration) is the only lifecycle action User Management exposes.

Inactive-user authentication: an inactive user can neither log in (loginUser in auth.service.ts rejects with the same generic 401 INVALID_CREDENTIALS a wrong password would — never a distinct "account deactivated" message, so an unauthenticated caller can't use this to enumerate account state) nor keep using an existing session (getUserBySessionToken re-checks isActive on every authenticated request, so deactivation takes effect immediately on that user's very next request — no separate session-revocation system needed).

Role change safety

Approval.approverId is a fixed user id decided once at submit/resubmit time (request.service.ts's submitOrResubmit) — a role change never retroactively touches an already-created Approval row. The guard here is deliberately conservative anyway: any role change (or deactivation) for a user who currently has a PENDING or WAITING Approval assigned to them is refused outright with 409 USER_HAS_PENDING_APPROVALS. There is no automatic reassignment — the Admin must resolve the approval first (have it decided, or reassign the underlying policy) before the role/status change is allowed.

Last active Admin invariant

An organization must always retain at least one active Admin. assertNotLastActiveAdmin() blocks a deactivation or role change away from ADMIN that would leave zero active Admins (409 LAST_ACTIVE_ADMIN_REQUIRED). Self-deactivation is separately forbidden outright (403 SELF_DEACTIVATION_NOT_ALLOWED), mirroring the existing self-role-change rule (403 SELF_ROLE_CHANGE_NOT_ALLOWED).

In practice, because only an Admin can call these routes at all and self-action is already blocked, the acting Admin always remains active after any action targeting a different user — so LAST_ACTIVE_ADMIN_REQUIRED is only reachable sequentially through the service layer directly (see its test coverage). The real, narrow residual risk is a concurrent race: two Admins simultaneously demoting/deactivating each other under PostgreSQL's default READ COMMITTED isolation. The guard runs inside the same transaction as the write it protects, which narrows that window, but does not fully close it without explicit row locking (SELECT ... FOR UPDATE) — a level of concurrency engineering this codebase has no evidence of needing yet. Documented here as a known, accepted limitation rather than built speculatively.

Audit integration

AuditAction gained USER_CREATED, USER_UPDATED, USER_ACTIVATED, USER_DEACTIVATED, and ROLE_CHANGED; AuditEntityType gained USER. Every User Management mutation writes an audit event through the same audit.service.ts record() function every other domain uses — requestId is always null for these (they are organization-scoped, not request-scoped), entityId is the affected User's id. A no-op write (e.g. PATCH /users/:id with a field equal to its current value, or PATCH /users/:id/status toggling to the status it's already in) writes no audit event, mirroring request.service.ts's existing no-op-PATCH convention.

Password management (M11)

POST /users no longer accepts an Admin-selected password. It generates a cryptographically random temporary password, stores only its Argon2id hash, sets mustChangePassword = true, and returns the plaintext value only in the immediate create response. POST /users/:id/reset-password follows the same one-time handoff pattern and invalidates every target-user session.

While mustChangePassword is true, authenticated middleware permits only current-user bootstrap, logout, and POST /auth/complete-password-change. Normal business endpoints return 403 PASSWORD_CHANGE_REQUIRED. A regular user can later call PATCH /users/me/password; successful self-service changes preserve the current session and invalidate all other sessions.

The npm run create:admin contract remains intentionally unchanged: its interactive/stdin password is treated as permanent and the resulting Admin has mustChangePassword = false. This keeps non-HTTP bootstrap compatible.

Dashboard Analytics Ranges (M12)

GET /dashboard/requests-over-time?range=... accepts 1m, 2m, 6m, 1y, or all. Six months is the frontend default. Calendar subtraction and bucket boundaries use UTC, matching the existing UTC createdAt and PostgreSQL date_trunc convention; no organization timezone exists yet.

  • 1m: daily buckets from one UTC calendar month ago until now.
  • 2m: weekly buckets from two UTC calendar months ago until now.
  • 6m: monthly buckets from six UTC calendar months ago until now.
  • 1y: monthly buckets from one UTC calendar year ago until now.
  • all: monthly buckets from the earliest request visible to the caller until now. This means all-time-to-date, not year-to-date.

The lower boundary is inclusive and the current-time upper boundary is exclusive. Empty buckets are omitted and an empty range returns points: []. The earlier explicit from/to/bucket query remains supported for backward compatibility.

About

Backend API for an organizational request and approval management system.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages