@astralibx/staff-engine 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # @astralibx/staff-engine
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@astralibx/staff-engine.svg)](https://www.npmjs.com/package/@astralibx/staff-engine)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Staff management backend with JWT authentication, role-based token expiry, IP-based rate limiting, runtime-configurable permission groups, and a REST admin API. No hardcoded permissions -- all groups and entries are defined at runtime via API.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install @astralibx/staff-engine
12
+ ```
13
+
14
+ ### Peer Dependencies
15
+
16
+ | Package | Required |
17
+ |---------|----------|
18
+ | `express` | Yes |
19
+ | `mongoose` | Yes |
20
+
21
+ ```bash
22
+ npm install express mongoose
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```ts
28
+ import { createStaffEngine } from '@astralibx/staff-engine';
29
+ import mongoose from 'mongoose';
30
+ import bcrypt from 'bcryptjs';
31
+ import express from 'express';
32
+
33
+ const app = express();
34
+ app.use(express.json());
35
+
36
+ const connection = mongoose.createConnection('mongodb://localhost:27017/myapp');
37
+
38
+ const engine = createStaffEngine({
39
+ db: { connection },
40
+ auth: {
41
+ jwtSecret: process.env.JWT_SECRET!,
42
+ },
43
+ adapters: {
44
+ hashPassword: (plain) => bcrypt.hash(plain, 12),
45
+ comparePassword: (plain, hash) => bcrypt.compare(plain, hash),
46
+ },
47
+ });
48
+
49
+ app.use('/api/staff', engine.routes);
50
+ app.listen(3000);
51
+ ```
52
+
53
+ ## Features
54
+
55
+ ### Authentication
56
+
57
+ - **JWT with role-based expiry** -- login returns a JWT signed with `jwtSecret`. Owners get `ownerTokenExpiry` (default `30d`); staff members get `staffTokenExpiry` (default `24h`). Both are configurable.
58
+ - **Login with IP rate limiting** -- `POST /login` tracks failed attempts per IP using Redis sorted sets or in-memory fallback. Locks out after `maxAttempts` failures within `windowMs` (default: 5 attempts / 15 min). Returns `STAFF_RATE_LIMITED` on lockout.
59
+ - **Setup route auto-locks** -- `POST /setup` creates the initial owner account and then permanently locks itself. Any subsequent call returns `STAFF_SETUP_ALREADY_COMPLETE`. This route is always public and never requires a token.
60
+ - **Token distinguishes expired vs invalid** -- `verifyToken` middleware checks `TokenExpiredError` separately from all other JWT errors and returns `STAFF_TOKEN_EXPIRED` vs `STAFF_TOKEN_INVALID` so clients can show the correct message.
61
+
62
+ ### Staff Management
63
+
64
+ - **Create staff with permissions** -- owner creates staff with name, email, password hash (via adapter), optional initial permissions, and optional `externalUserId` for linking to external identity systems.
65
+ - **Email uniqueness** -- duplicate email rejected with `STAFF_EMAIL_EXISTS`. Can be disabled via `requireEmailUniqueness: false`.
66
+ - **Paginated list with filters** -- `GET /` supports `status`, `role`, `page`, and `limit` query params. Returns `data[]` + `pagination` with total counts.
67
+ - **Owner-only access** -- all staff CRUD routes (`GET /`, `POST /`, `PUT /:id`, `PUT /:id/permissions`, `PUT /:id/status`, `PUT /:id/password`) require owner role. `GET /me` and `PUT /me/password` are staff-accessible.
68
+ - **No-delete policy** -- staff records are never hard-deleted. Deactivate to revoke access. Inactive staff tokens are rejected on every authenticated request even before JWT expiry.
69
+
70
+ ### Permissions
71
+
72
+ - **Runtime-configurable groups via API** -- `POST /permission-groups` creates a named group with permission entries. Groups have `groupId`, `label`, `sortOrder`, and an array of entries (key, label, type). No redeploy required to define new permissions.
73
+ - **Edit-to-view cascade** -- when granting a permission ending in `.edit`, the corresponding `.view` key is automatically required. The engine validates this on `PUT /:id/permissions`.
74
+ - **Permission cache (Redis or in-memory)** -- each staff member's resolved permission list is cached after the first lookup. TTL is `permissionCacheTtlMs` (default 5 min). Cache is invalidated immediately on `updatePermissions` or `updateStatus`.
75
+ - **Owner bypasses all checks** -- `requirePermission` middleware skips the permission check entirely when `req.user.role === 'owner'`.
76
+
77
+ ### Security
78
+
79
+ - **Rate limiting (configurable window/max)** -- `windowMs` and `maxAttempts` are configurable at engine creation time. Uses Redis `ZADD`/`ZREMRANGEBYSCORE` for accurate per-IP tracking across multiple processes.
80
+ - **Last-owner guard** -- `PUT /:id/status` with `status: 'inactive'` checks that at least one other active owner exists before allowing the change. Returns `STAFF_LAST_OWNER_GUARD` if blocked.
81
+ - **Inactive token rejection** -- every call through `verifyToken` re-reads staff status from MongoDB (with optional tenant filter). Inactive or pending accounts are rejected with `STAFF_TOKEN_INVALID` even with an otherwise valid JWT.
82
+ - **Status checks on every request** -- `verifyToken` loads status and role fresh on each request. There is no session store; the database is the source of truth.
83
+
84
+ ### Integration
85
+
86
+ - **`resolveStaff` for programmatic token resolution** -- `engine.auth.resolveStaff(token)` returns `{ staffId, role, permissions }` or `null` without sending an HTTP response. Useful for WebSocket authentication or programmatic access in other modules.
87
+ - **`requirePermission` middleware for consumer routes** -- `engine.auth.requirePermission('contacts.view', 'contacts.edit')` returns an Express middleware that checks the current user's permissions. Owners always pass. Non-owners are rejected with `STAFF_INSUFFICIENT_PERMISSIONS` and a list of missing keys.
88
+ - **`requireRole` middleware** -- `engine.auth.requireRole('owner', 'staff')` checks the token's resolved role against the provided list.
89
+
90
+ ## Routes
91
+
92
+ | Method | Path | Auth | Description |
93
+ |--------|------|------|-------------|
94
+ | `POST` | `/setup` | Public | Create initial owner account (auto-locks after first use) |
95
+ | `POST` | `/login` | Public | Authenticate and receive JWT token |
96
+ | `GET` | `/me` | Staff | Get current staff profile and resolved permissions |
97
+ | `PUT` | `/me/password` | Staff | Change own password (only when `allowSelfPasswordChange: true`) |
98
+ | `GET` | `/` | Owner | List staff with pagination and status/role filters |
99
+ | `POST` | `/` | Owner | Create a new staff member |
100
+ | `PUT` | `/:staffId` | Owner | Update staff name, email, metadata |
101
+ | `PUT` | `/:staffId/permissions` | Owner | Replace staff permission set |
102
+ | `PUT` | `/:staffId/status` | Owner | Activate or deactivate a staff member |
103
+ | `PUT` | `/:staffId/password` | Owner | Reset a staff member's password |
104
+ | `GET` | `/permission-groups` | Staff | List all permission groups |
105
+ | `POST` | `/permission-groups` | Owner | Create a new permission group |
106
+ | `PUT` | `/permission-groups/:groupId` | Owner | Update a permission group's entries or label |
107
+ | `DELETE` | `/permission-groups/:groupId` | Owner | Delete a permission group |
108
+
109
+ ## Architecture
110
+
111
+ The factory function returns a single `StaffEngine` object:
112
+
113
+ | Export | Purpose |
114
+ |--------|---------|
115
+ | `engine.routes` | Express router -- mount at `/api/staff` or similar |
116
+ | `engine.auth.verifyToken` | Middleware to authenticate any route |
117
+ | `engine.auth.requirePermission(...keys)` | Middleware for permission-gated routes |
118
+ | `engine.auth.ownerOnly` | Middleware to restrict to owner role |
119
+ | `engine.auth.resolveStaff(token)` | Programmatic token resolution (no HTTP response) |
120
+ | `engine.staff` | Direct access to `StaffService` for programmatic use |
121
+ | `engine.permissions` | Direct access to `PermissionService` |
122
+ | `engine.models` | Mongoose models (`Staff`, `PermissionGroup`) |
123
+ | `engine.destroy()` | Flush permission cache and clean up resources |
124
+
125
+ ## Redis Key Prefix (Required for Multi-Project Deployments)
126
+
127
+ > **WARNING:** If multiple projects share the same Redis server, you MUST set a unique `keyPrefix` per project. Without this, rate limiter state and permission cache entries will collide across projects.
128
+
129
+ ```ts
130
+ const engine = createStaffEngine({
131
+ redis: {
132
+ connection: redis,
133
+ keyPrefix: 'myproject:staff:', // REQUIRED if sharing Redis
134
+ },
135
+ // ...
136
+ });
137
+ ```
138
+
139
+ ## Links
140
+
141
+ - [GitHub](https://github.com/Hariprakash1997/astralib/tree/main/packages/staff/staff-engine)
142
+ - [staff-types](https://github.com/Hariprakash1997/astralib/tree/main/packages/staff/staff-types)
143
+ - [staff-ui](https://github.com/Hariprakash1997/astralib/tree/main/packages/staff/staff-ui)
144
+
145
+ ## License
146
+
147
+ MIT