@xeplr/auth 1.0.0 → 1.0.1

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.
Files changed (59) hide show
  1. package/bin/migrate.js +43 -24
  2. package/bin/server.js +12 -30
  3. package/index.js +135 -51
  4. package/lib/adminRouter.js +20 -92
  5. package/lib/authHelper.js +26 -3
  6. package/lib/authMiddleware.js +39 -13
  7. package/lib/authRouter.js +140 -11
  8. package/lib/authService.js +327 -9
  9. package/lib/hooks.js +50 -0
  10. package/lib/mtMembershipMiddleware.js +61 -0
  11. package/lib/seed.js +74 -0
  12. package/lib/sessionService.js +54 -6
  13. package/lib/ticketService.js +88 -0
  14. package/lib/tokenDecision.js +45 -0
  15. package/migrations/0001_extensions.sql +9 -0
  16. package/migrations/0002_users.sql +31 -0
  17. package/migrations/0003_catalog_tables.sql +82 -0
  18. package/migrations/0004_role_mappings.sql +78 -0
  19. package/migrations/0005_user_tenants_mapping.sql +39 -0
  20. package/migrations/0006_seed_catalog.sql +120 -0
  21. package/models/Api.js +3 -0
  22. package/models/ApisRolesMapping.js +3 -0
  23. package/models/Menu.js +3 -0
  24. package/models/MenuRolesMapping.js +3 -0
  25. package/models/Role.js +8 -0
  26. package/models/UiElement.js +3 -0
  27. package/models/UiElementsRolesMapping.js +3 -0
  28. package/models/UiPage.js +3 -0
  29. package/models/UiPagesRolesMapping.js +3 -0
  30. package/models/User.js +3 -13
  31. package/models/UserRolesMapping.js +3 -0
  32. package/models/UserTenantsMapping.js +20 -2
  33. package/models/index.js +0 -2
  34. package/package.json +26 -5
  35. package/migrations/0001_users.js +0 -22
  36. package/migrations/0002_menus.js +0 -16
  37. package/migrations/0003_apis.js +0 -16
  38. package/migrations/0004_uiPages.js +0 -16
  39. package/migrations/0005_uiElements.js +0 -16
  40. package/migrations/0006_roles.js +0 -15
  41. package/migrations/0007_apisRolesMapping.js +0 -16
  42. package/migrations/0008_uiPagesRolesMapping.js +0 -16
  43. package/migrations/0009_uiElementsRolesMapping.js +0 -16
  44. package/migrations/0010_menuRolesMapping.js +0 -16
  45. package/migrations/0011_userRolesMapping.js +0 -16
  46. package/migrations/0012_users_add_reset_token.js +0 -13
  47. package/migrations/0013_add_isPublic.js +0 -27
  48. package/migrations/0014_users_add_activation_token.js +0 -13
  49. package/migrations/0015_add_mt_columns.js +0 -41
  50. package/migrations/0016_tenants.js +0 -23
  51. package/migrations/0017_userTenantsMapping.js +0 -20
  52. package/models/Tenant.js +0 -63
  53. package/seeds/001_roles.js +0 -26
  54. package/seeds/002_apis.js +0 -70
  55. package/seeds/003_pages.js +0 -41
  56. package/seeds/004_elements.js +0 -46
  57. package/seeds/005_menus.js +0 -35
  58. package/seeds/006_default_tenant.js +0 -50
  59. package/seeds/zzz_admin_access.js +0 -49
package/bin/migrate.js CHANGED
@@ -1,18 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Reads process.env ONLY. The consuming app loads its .env (e.g. via dotenv-cli
4
+ // in the npm script) — @xeplr/* packages never read .env files.
5
+
3
6
  const path = require('path');
4
- const { up, rollback, status } = require('@xeplr/db').migrator;
7
+ const { up, status } = require('@xeplr/db').sqlMigrator;
8
+ const { resolveConfig } = require('@xeplr/db');
5
9
 
6
10
  /**
7
11
  * xeplr-auth-migrate
8
12
  *
9
- * Runs auth migrations bundled with this package.
10
- * Reuses xeplr-db's migrator — no duplicated knex logic.
13
+ * Runs auth's bundled .sql migrations, followed by the consumer app's
14
+ * EXTENSION migrations dir (schema + data auth has no separate seed step;
15
+ * base data lives in migrations so it runs exactly once). Reuses xeplr-db's
16
+ * sqlMigrator — hand-written .sql files, no down(), ledger-tracked.
17
+ *
18
+ * The app points at its extension dir ONCE via env: AUTH_EXT_MIGRATIONS_DIR
19
+ * (or --extDir). Base migrations run first, then the app's — one ledger.
11
20
  *
12
21
  * Usage:
13
- * xeplr-auth-migrate up [--db <database>]
14
- * xeplr-auth-migrate rollback [--db <database>]
15
- * xeplr-auth-migrate status [--db <database>]
22
+ * xeplr-auth-migrate up [--extDir <dir>] [--db <database>]
23
+ * xeplr-auth-migrate status [--extDir <dir>] [--db <database>]
16
24
  */
17
25
 
18
26
  function parseArgs(argv) {
@@ -33,30 +41,33 @@ async function main() {
33
41
  const args = parseArgs(process.argv.slice(2));
34
42
  const command = args._[0];
35
43
 
36
- // Always use bundled migrations directory
44
+ // Bundled auth migrations (base) + the app's extension migrations dir, which
45
+ // the app configures ONCE via AUTH_EXT_MIGRATIONS_DIR (or --extDir). Base runs
46
+ // first, then the extension — one shared ledger.
37
47
  const options = {
38
48
  ...args,
39
- db: args.db || process.env.DB_AUTH || process.env.DB_NAME,
40
- dir: path.join(__dirname, '..', 'migrations')
49
+ db: args.db || process.env.AUTH_DB_NAME,
50
+ dir: path.join(__dirname, '..', 'migrations'),
51
+ extDir: args.extDir || args['ext-dir'] || process.env.AUTH_EXT_MIGRATIONS_DIR,
52
+ connectionName: args['connection-name'] || args.connectionName || 'auth'
41
53
  };
42
54
 
55
+ if (['up', 'status'].indexOf(command) !== -1) {
56
+ // App tells us which secret to use (decoupled from the connection name):
57
+ // --connection <encrypted> | --connection-env <ENV_NAME>. Omit → legacy
58
+ // <connectionName>_CONNECTION fallback inside resolveConfig.
59
+ var connSource = args.connection ||
60
+ (args['connection-env'] && process.env[args['connection-env']]) || undefined;
61
+ await resolveConfig(options.connectionName, connSource);
62
+ }
63
+
43
64
  switch (command) {
44
65
  case 'up': {
45
66
  const result = await up(options);
46
67
  if (result.migrations.length === 0) {
47
68
  console.log('Already up to date');
48
69
  } else {
49
- console.log(`Batch ${result.batch} ran ${result.migrations.length} migrations:`);
50
- result.migrations.forEach(m => console.log(` - ${m}`));
51
- }
52
- break;
53
- }
54
- case 'rollback': {
55
- const result = await rollback(options);
56
- if (result.migrations.length === 0) {
57
- console.log('Nothing to rollback');
58
- } else {
59
- console.log(`Rolled back ${result.migrations.length} migrations:`);
70
+ console.log(`Ran ${result.migrations.length} migrations:`);
60
71
  result.migrations.forEach(m => console.log(` - ${m}`));
61
72
  }
62
73
  break;
@@ -71,21 +82,29 @@ async function main() {
71
82
  } else {
72
83
  console.log('No pending migrations');
73
84
  }
85
+ if (result.drift.length) {
86
+ console.log('WARNING - applied migrations edited after the fact (checksum mismatch):');
87
+ result.drift.forEach(m => console.log(` ! ${m}`));
88
+ }
74
89
  break;
75
90
  }
76
91
  default:
77
92
  console.log('xeplr-auth-migrate - Auth database migrations');
78
93
  console.log('');
79
94
  console.log('Commands:');
80
- console.log(' up [--db <name>] Run pending migrations');
81
- console.log(' rollback [--db <name>] Rollback last batch');
82
- console.log(' status [--db <name>] Show migration status');
95
+ console.log(' up [--extDir <dir>] Run base + app extension migrations');
96
+ console.log(' status [--extDir <dir>] Show migration status');
97
+ console.log('');
98
+ console.log(' App extension dir: --extDir or AUTH_EXT_MIGRATIONS_DIR env');
83
99
  console.log('');
84
100
  console.log('Options:');
85
- console.log(' --db Database name (or DB_AUTH env)');
101
+ console.log(' --db Database name (or AUTH_DB_NAME env)');
86
102
  console.log(' --host DB host (default: DB_HOST env or localhost)');
87
103
  console.log(' --user DB user (default: DB_USER env or root)');
88
104
  console.log(' --password DB password (default: DB_PASSWORD env)');
105
+ console.log('');
106
+ console.log('Migrations are hand-written .sql files (see migrations/), applied once,');
107
+ console.log('no down(). 0007_seed_super_admin.sql requires SUPER_ADMIN_PASSWORD in env.');
89
108
  }
90
109
  }
91
110
 
package/bin/server.js CHANGED
@@ -1,35 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Standalone auth server entry point.
5
- * Designed to be forked as a child process or run directly.
4
+ * Standalone auth service. Reads AUTH_* env — the LAUNCHER provides it; this bin
5
+ * reads process.env only and loads no .env of its own. The consuming app does:
6
+ * "start-auth": "dotenv -e development.env -- xeplr-auth-server"
6
7
  *
7
- * Reads config from AUTH_CONFIG env var (JSON) or individual env vars.
8
+ * env: ENCRYPTION_KEY · AUTH_DB_CONNECTION_INFO_ENCRYPTED · AUTH_DB_NAME ·
9
+ * AUTH_JWT_SECRET · AUTH_PORT · AUTH_ACTIVATION_BASE_URL ·
10
+ * AUTH_ACCESS_TOKEN_TTL_MINUTES · AUTH_EXT_MIGRATIONS_DIR · email vars · REDIS_*
8
11
  */
9
- var auth = require('../index');
10
-
11
- var config = {};
12
-
13
- if (process.env.AUTH_CONFIG) {
14
- try {
15
- config = JSON.parse(process.env.AUTH_CONFIG);
16
- } catch (e) {
17
- console.error('Failed to parse AUTH_CONFIG:', e.message);
18
- process.exit(1);
19
- }
20
- }
21
-
22
- config.port = config.port || process.env.AUTH_PORT || 19001;
23
- config.database = config.database || process.env.DB_AUTH || 'architects_auth';
24
-
25
- if (process.env.JWT_SECRET) {
26
- config.jwt = config.jwt || {};
27
- config.jwt.secret = config.jwt.secret || process.env.JWT_SECRET;
28
- }
29
-
30
- auth.start(config);
31
-
32
- // Notify parent process (if forked) that auth is ready
33
- if (process.send) {
34
- process.send({ status: 'ready', port: config.port });
35
- }
12
+ require('../index').boot().then(function () {
13
+ if (process.send) process.send({ status: 'ready' });
14
+ }).catch(function (err) {
15
+ console.error('[auth] startup failed:', err.stack || err.message);
16
+ process.exit(1);
17
+ });
package/index.js CHANGED
@@ -1,12 +1,16 @@
1
1
  const path = require('path');
2
- const { getConnection, bindModels, resolveConfig, migrator } = require('@xeplr/db');
2
+ const { getConnection, bindModels, resolveConfig, sqlMigrator } = require('@xeplr/db');
3
3
  const { createApp } = require('@xeplr/base-apis');
4
4
  const authHelper = require('./lib/authHelper');
5
+ const { seedSuperAdmin } = require('./lib/seed');
5
6
  const authMiddleware = require('./lib/authMiddleware');
6
7
  const authService = require('./lib/authService');
7
8
  const accessService = require('./lib/accessService');
8
9
  const { accessMiddleware, requireRole } = require('./lib/accessMiddleware');
9
10
  const sessionService = require('./lib/sessionService');
11
+ const ticketService = require('./lib/ticketService');
12
+ const hooks = require('./lib/hooks');
13
+ const mtMembershipMiddleware = require('./lib/mtMembershipMiddleware');
10
14
  const createAuthRouter = require('./lib/authRouter');
11
15
  const createAdminRouter = require('./lib/adminRouter');
12
16
  const models = require('./models');
@@ -23,12 +27,12 @@ let _initialized = false;
23
27
  * @param {object} [config.email] - Email config { provider, smtp, aws, azure, brevo }
24
28
  * @param {string} [config.resetBaseUrl] - Base URL for password reset links
25
29
  */
26
- function init(config = {}) {
27
- // Setup database connection
28
- const dbName = config.database || process.env.DB_AUTH || 'auth';
29
- const dbOptions = Object.assign({ connectionName: config.connectionName || 'auth' }, config.db || {});
30
- const connection = getConnection(dbName, dbOptions);
31
- bindModels(connection);
30
+ async function init(config = {}) {
31
+ // One call: decrypt the app-supplied connection, connect, and bind the models.
32
+ const dbName = config.database || process.env.AUTH_DB_NAME || 'auth';
33
+ const connection = await getConnection(dbName, config.connection || config.db, {
34
+ connectionName: config.connectionName || 'auth'
35
+ });
32
36
 
33
37
  // Configure JWT
34
38
  if (config.jwt) {
@@ -78,8 +82,8 @@ function adminRouter() {
78
82
  * @param {Function[]} [config.middleware] - Additional middleware
79
83
  * @returns {http.Server}
80
84
  */
81
- function start(config = {}) {
82
- init(config);
85
+ async function start(config = {}) {
86
+ await init(config);
83
87
 
84
88
  const port = config.port || process.env.AUTH_PORT || 19001;
85
89
  const authRouter = createAuthRouter({ resetBaseUrl: config.resetBaseUrl });
@@ -93,63 +97,139 @@ function start(config = {}) {
93
97
  }
94
98
 
95
99
  /**
96
- * Boot xeplr-auth as a fully independent service.
97
- * Resolves DB config, runs migrations/seeds, and starts the server.
100
+ * Boot xeplr-auth as a fully independent service. ENV-DRIVEN — no config; there
101
+ * is always one auth system, so it reads everything from AUTH_* env. Resolves the
102
+ * connection, self-migrates, configures email/jwt/activation, starts the server.
98
103
  *
99
- * @param {object} config
100
- * @param {number|string} config.port - Port to listen on (default: AUTH_PORT env or 19001)
101
- * @param {string} config.database - Database name for auth tables
102
- * @param {string} [config.connectionName] - Named connection identifier (default: 'auth')
103
- * @param {object} [config.db] - DB connection options { host, user, password, port }
104
- * @param {object} [config.jwt] - JWT options { secret, accessTokenExpiresIn }
105
- * @param {object} [config.email] - Email config { provider, smtp, aws, azure, brevo }
106
- * @param {string} [config.resetBaseUrl] - Base URL for reset links
107
- * @param {string} [config.migrationsDir] - Additional project-specific migrations directory
108
- * @param {string} [config.seedsDir] - Additional project-specific seeds directory
109
- * @param {object} [config.corsOptions] - CORS options
110
- * @param {Function[]} [config.middleware] - Additional middleware
111
104
  * @returns {Promise<http.Server>}
112
105
  */
113
- async function boot(config = {}) {
114
- var connName = config.connectionName || 'auth';
115
- var database = config.database || process.env.DB_AUTH || 'auth';
116
- var authPkgDir = path.join(__dirname);
117
-
118
- await resolveConfig(connName);
119
-
120
- // Run migrations
121
- var migrationResult = await migrator.up({
106
+ // Boot the auth service. ENV-DRIVEN, no config there is always a single auth
107
+ // system, so it reads its whole setup from AUTH_* env (see requiredEnv + README):
108
+ // connection AUTH_DB_CONNECTION_INFO_ENCRYPTED · db AUTH_DB_NAME · AUTH_JWT_SECRET
109
+ // AUTH_PORT · AUTH_ACTIVATION_BASE_URL · AUTH_ACCESS_TOKEN_TTL_MINUTES
110
+ // AUTH_EXT_MIGRATIONS_DIR · email vars · REDIS_*
111
+ async function boot() {
112
+ var connName = 'auth';
113
+ var database = process.env.AUTH_DB_NAME || 'auth';
114
+
115
+ await resolveConfig(connName, process.env.AUTH_DB_CONNECTION_INFO_ENCRYPTED);
116
+
117
+ // Self-migrate: auth's own .sql migrations (schema + base data), then the
118
+ // app's extension dir. Idempotent — same as `xeplr-auth-migrate up`.
119
+ var migrationResult = await sqlMigrator.up({
122
120
  db: database,
123
- dir: path.join(authPkgDir, 'migrations'),
124
- extDir: config.migrationsDir || null,
121
+ dir: path.join(__dirname, 'migrations'),
122
+ extDir: process.env.AUTH_EXT_MIGRATIONS_DIR || null,
125
123
  type: 'precede',
126
124
  connectionName: connName
127
125
  });
126
+ console.log(migrationResult.migrations.length
127
+ ? '[xeplr-auth] ran ' + migrationResult.migrations.length + ' migrations'
128
+ : '[xeplr-auth] migrations up to date');
129
+
130
+ // Configure email (via @xeplr/utils, the engine auth uses) + activation links.
131
+ require('@xeplr/utils').configureFromEnv();
132
+ if (process.env.AUTH_ACTIVATION_BASE_URL) authService.configureActivation(process.env.AUTH_ACTIVATION_BASE_URL);
133
+
134
+ var server = await start({
135
+ database: database,
136
+ connectionName: connName,
137
+ connection: process.env.AUTH_DB_CONNECTION_INFO_ENCRYPTED,
138
+ port: process.env.AUTH_PORT,
139
+ jwt: {
140
+ secret: process.env.AUTH_JWT_SECRET,
141
+ accessTokenExpiresIn: (process.env.AUTH_ACCESS_TOKEN_TTL_MINUTES || '15') + 'm'
142
+ }
143
+ });
128
144
 
129
- if (migrationResult.migrations.length > 0) {
130
- console.log('[xeplr-auth] ran ' + migrationResult.migrations.length + ' migrations');
131
- } else {
132
- console.log('[xeplr-auth] migrations up to date');
133
- }
145
+ banner();
146
+ return server;
147
+ }
134
148
 
135
- // Run seeds (auth's own seeds always run, project seeds if provided)
136
- var seedResult = await migrator.seed({
137
- db: database,
138
- seedsDir: path.join(authPkgDir, 'seeds'),
139
- extSeedsDir: config.seedsDir || null,
140
- type: 'precede',
141
- connectionName: connName
142
- });
149
+ // Startup summary the EFFECTIVE config (defaults resolved) so you can see what
150
+ // the service is actually running with. Secrets are masked, never printed.
151
+ function banner() {
152
+ var set = function (v) { return v ? '✓ set' : '✗ MISSING'; };
153
+ var rows = [
154
+ ['port', process.env.AUTH_PORT || '19001'],
155
+ ['database', process.env.AUTH_DB_NAME || 'auth'],
156
+ ['ext migrations', process.env.AUTH_EXT_MIGRATIONS_DIR || '(none)'],
157
+ ['activation url', process.env.AUTH_ACTIVATION_BASE_URL || '(none)'],
158
+ ['access token ttl', (process.env.AUTH_ACCESS_TOKEN_TTL_MINUTES || '15') + 'm'],
159
+ ['refresh token ttl', (process.env.AUTH_REFRESH_TOKEN_TTL_DAYS || '7') + 'd'],
160
+ ['slide tolerance', (process.env.AUTH_ACCESS_TOKEN_TOLERANCE_SECONDS || '0') + 's'],
161
+ ['max sessions/user', process.env.AUTH_MAX_SESSIONS_PER_USER || '5'],
162
+ ['redis', (process.env.REDIS_HOST || 'localhost') + ':' + (process.env.REDIS_PORT || '6379')],
163
+ ['email', process.env.EMAIL_PROVIDER || '(not configured)'],
164
+ ['connection', set(process.env.AUTH_DB_CONNECTION_INFO_ENCRYPTED)],
165
+ ['encryption key', set(process.env.ENCRYPTION_KEY)],
166
+ ['jwt secret', process.env.AUTH_JWT_SECRET ? '✓ set' : '✗ INSECURE DEFAULT']
167
+ ];
168
+ var lines = rows.map(function (r) { return r[0].padEnd(19) + String(r[1]); });
169
+ var title = 'xeplr-auth · running';
170
+ var inner = Math.max.apply(null, [title.length].concat(lines.map(function (l) { return l.length; }))) + 2;
171
+ var bar = '─'.repeat(inner);
172
+ console.log('\n┌' + bar + '┐');
173
+ console.log('│ ' + title.padEnd(inner - 1) + '│');
174
+ console.log('├' + bar + '┤');
175
+ lines.forEach(function (l) { console.log('│ ' + l.padEnd(inner - 1) + '│'); });
176
+ console.log('└' + bar + '┘\n');
177
+ }
143
178
 
144
- if (seedResult.length > 0) {
145
- console.log('[xeplr-auth] ran ' + seedResult.length + ' seeds');
179
+ /**
180
+ * Attach to the auth database from ANOTHER process (e.g. the api service), so it
181
+ * can act on auth tables — invite users, assign roles. ENV-DRIVEN
182
+ * (AUTH_DB_NAME + AUTH_DB_CONNECTION_INFO_ENCRYPTED). Opens a SECONDARY connection
183
+ * (bind:false, so it never hijacks the api's global model binding) and hands back
184
+ * model/service accessors.
185
+ *
186
+ * const auth = require('@xeplr/auth').attach();
187
+ * await auth.ready();
188
+ * await auth.model('User').query()... // auth-package model bound to the auth DB
189
+ * auth.service.invite(...) // authService
190
+ *
191
+ * @returns {{ ready: () => Promise, conn: () => Knex, model: (name:string) => Model, service: object }}
192
+ */
193
+ function attach() {
194
+ var _conn = null;
195
+ var _ready = (async function () {
196
+ _conn = await getConnection(
197
+ process.env.AUTH_DB_NAME || 'auth',
198
+ process.env.AUTH_DB_CONNECTION_INFO_ENCRYPTED,
199
+ { bind: false, connectionName: 'auth' }
200
+ );
201
+ if (process.env.AUTH_ACTIVATION_BASE_URL) authService.configureActivation(process.env.AUTH_ACTIVATION_BASE_URL);
202
+ return _conn;
203
+ })();
204
+
205
+ function conn() {
206
+ if (!_conn) throw new Error('auth.attach(): connection not ready — await ready() first');
207
+ return _conn;
146
208
  }
147
-
148
- return start(Object.assign({ database, connectionName: connName }, config));
209
+ function model(name) {
210
+ var M = models[name];
211
+ if (!M) throw new Error('auth.attach(): unknown auth model "' + name + '"');
212
+ return M.bindKnex(conn());
213
+ }
214
+ return { ready: function () { return _ready; }, conn: conn, model: model, service: authService };
149
215
  }
150
216
 
217
+ // Env vars this library needs — apps spread this into their env.required.js so
218
+ // the names live here (change once, every app picks it up), not re-listed per app.
219
+ var requiredEnv = [
220
+ 'ENCRYPTION_KEY',
221
+ 'AUTH_JWT_SECRET',
222
+ 'AUTH_ACTIVATION_BASE_URL',
223
+ 'AUTH_PORT',
224
+ 'AUTH_DB_NAME', // auth db (api reaches it via attach())
225
+ 'AUTH_DB_CONNECTION_INFO_ENCRYPTED', // auth DB login (for attach())
226
+ 'AUTH_EXT_MIGRATIONS_DIR', // migrations run via migrate:up
227
+ ];
228
+
151
229
  module.exports = {
230
+ requiredEnv,
152
231
  init,
232
+ attach,
153
233
  router,
154
234
  adminRouter,
155
235
  start,
@@ -157,9 +237,13 @@ module.exports = {
157
237
  authMiddleware,
158
238
  accessMiddleware,
159
239
  requireRole,
240
+ mtMembershipMiddleware,
160
241
  authHelper,
242
+ seedSuperAdmin,
161
243
  authService,
162
244
  accessService,
163
245
  sessionService,
246
+ ticketService,
247
+ hooks,
164
248
  models
165
249
  };
@@ -1,6 +1,8 @@
1
1
  var express = require('express');
2
2
  var { generateId } = require('./authHelper');
3
3
  var { clearUserAccess, clearAccessRules } = require('./accessService');
4
+ var authMiddleware = require('./authMiddleware');
5
+ var hooks = require('./hooks');
4
6
  var User = require('../models/User');
5
7
  var Role = require('../models/Role');
6
8
  var Api = require('../models/Api');
@@ -12,8 +14,6 @@ var ApisRolesMapping = require('../models/ApisRolesMapping');
12
14
  var UiPagesRolesMapping = require('../models/UiPagesRolesMapping');
13
15
  var UiElementsRolesMapping = require('../models/UiElementsRolesMapping');
14
16
  var MenuRolesMapping = require('../models/MenuRolesMapping');
15
- var Tenant = require('../models/Tenant');
16
- var UserTenantsMapping = require('../models/UserTenantsMapping');
17
17
 
18
18
  // Group field names per type
19
19
  var GROUP_FIELDS = {
@@ -33,12 +33,15 @@ function isModuleGroup(group) {
33
33
  function createAdminRouter() {
34
34
  var router = express.Router();
35
35
 
36
+ // Every admin route requires a valid JWT — populates req.user (with roles).
37
+ router.use(authMiddleware);
38
+
36
39
  // ─── Users with their roles ───
37
40
  router.get('/users', async function(req, res) {
38
41
  try {
39
42
  var users = await User.query()
40
43
  .select('id', 'email', 'name', 'isActive', 'isActivated')
41
- .withGraphFetched('[roles, tenants]');
44
+ .withGraphFetched('roles');
42
45
  res.json(users);
43
46
  } catch (err) {
44
47
  res.status(500).json({ error: 'Something went wrong' });
@@ -97,6 +100,7 @@ function createAdminRouter() {
97
100
  }
98
101
 
99
102
  await clearUserAccess(userId);
103
+ await hooks.fire('userRolesMapping', assign ? 'create' : 'delete', userId);
100
104
  res.json({ ok: true });
101
105
  } catch (err) {
102
106
  res.status(500).json({ error: 'Something went wrong' });
@@ -143,6 +147,7 @@ function createAdminRouter() {
143
147
  }
144
148
 
145
149
  await clearAccessRules();
150
+ await hooks.fire(type + 'RolesMapping', assign ? 'create' : 'delete', itemId);
146
151
  res.json({ ok: true });
147
152
  } catch (err) {
148
153
  res.status(500).json({ error: 'Something went wrong' });
@@ -162,10 +167,10 @@ function createAdminRouter() {
162
167
  var createdBy = req.user ? req.user.id : null;
163
168
 
164
169
  var types = [
165
- { Model: Api, MappingModel: ApisRolesMapping, fk: 'apiId', groupField: 'apiGroup' },
166
- { Model: UiPage, MappingModel: UiPagesRolesMapping, fk: 'uiPageId', groupField: 'uiPagesGroup' },
167
- { Model: UiElement, MappingModel: UiElementsRolesMapping, fk: 'uiElementId', groupField: 'uiElementsGroup' },
168
- { Model: Menu, MappingModel: MenuRolesMapping, fk: 'menuId', groupField: 'menuGroup' }
170
+ { type: 'apis', Model: Api, MappingModel: ApisRolesMapping, fk: 'apiId', groupField: 'apiGroup' },
171
+ { type: 'pages', Model: UiPage, MappingModel: UiPagesRolesMapping, fk: 'uiPageId', groupField: 'uiPagesGroup' },
172
+ { type: 'elements', Model: UiElement, MappingModel: UiElementsRolesMapping, fk: 'uiElementId', groupField: 'uiElementsGroup' },
173
+ { type: 'menus', Model: Menu, MappingModel: MenuRolesMapping, fk: 'menuId', groupField: 'menuGroup' }
169
174
  ];
170
175
 
171
176
  var promises = types.map(async function(cfg) {
@@ -187,6 +192,8 @@ function createAdminRouter() {
187
192
  } else {
188
193
  await cfg.MappingModel.query().delete().where(itemWhere);
189
194
  }
195
+
196
+ await hooks.fire(cfg.type + 'RolesMapping', assign ? 'create' : 'delete', items[i].id);
190
197
  }
191
198
  });
192
199
 
@@ -215,10 +222,12 @@ function createAdminRouter() {
215
222
 
216
223
  if (id) {
217
224
  await Role.query().findById(id).patch({ name: name });
225
+ await hooks.fire('roles', 'update', id);
218
226
  res.json({ id: id });
219
227
  } else {
220
228
  var newId = generateId();
221
229
  await Role.query().insert({ id: newId, name: name });
230
+ await hooks.fire('roles', 'create', newId);
222
231
  res.json({ id: newId });
223
232
  }
224
233
  } catch (err) {
@@ -232,6 +241,7 @@ function createAdminRouter() {
232
241
  if (!id) return res.status(400).json({ error: 'id is required' });
233
242
  await Role.query().deleteById(id);
234
243
  await clearAccessRules();
244
+ await hooks.fire('roles', 'delete', id);
235
245
  res.json({ ok: true });
236
246
  } catch (err) {
237
247
  res.status(500).json({ error: 'Something went wrong' });
@@ -271,11 +281,13 @@ function createAdminRouter() {
271
281
 
272
282
  if (id) {
273
283
  await cfg.Model.query().findById(id).patch(data);
284
+ await hooks.fire(typeKey, 'update', id);
274
285
  res.json({ id: id });
275
286
  } else {
276
287
  var newId = generateId();
277
288
  data.id = newId;
278
289
  await cfg.Model.query().insert(data);
290
+ await hooks.fire(typeKey, 'create', newId);
279
291
  res.json({ id: newId });
280
292
  }
281
293
  } catch (err) {
@@ -289,6 +301,7 @@ function createAdminRouter() {
289
301
  if (!id) return res.status(400).json({ error: 'id is required' });
290
302
  await cfg.Model.query().deleteById(id);
291
303
  await clearAccessRules();
304
+ await hooks.fire(typeKey, 'delete', id);
292
305
  res.json({ ok: true });
293
306
  } catch (err) {
294
307
  res.status(500).json({ error: 'Something went wrong' });
@@ -296,91 +309,6 @@ function createAdminRouter() {
296
309
  });
297
310
  });
298
311
 
299
- // ─── Tenants (Super Admin only) ───
300
-
301
- function requireSuperAdmin(req, res, next) {
302
- if (!req.user || !req.user.roles || req.user.roles.indexOf('Super Admin') === -1) {
303
- return res.status(403).json({ error: 'Super Admin access required' });
304
- }
305
- next();
306
- }
307
-
308
- router.get('/tenants', requireSuperAdmin, async function(req, res) {
309
- try {
310
- var level = req.query.level ? parseInt(req.query.level) : null;
311
- var query = Tenant.query().select('id', 'name', 'code', 'level', 'parentId', 'description');
312
- if (level) query = query.where('level', level);
313
- var tenants = await query;
314
- res.json(tenants);
315
- } catch (err) {
316
- res.status(500).json({ error: 'Something went wrong' });
317
- }
318
- });
319
-
320
- router.post('/tenants', requireSuperAdmin, async function(req, res) {
321
- try {
322
- var { id, name, code, level, parentId, description } = req.body;
323
- if (!name) return res.status(400).json({ error: 'name is required' });
324
-
325
- var data = { name: name };
326
- if (code !== undefined) data.code = code;
327
- if (level !== undefined) data.level = level;
328
- if (parentId !== undefined) data.parentId = parentId;
329
- if (description !== undefined) data.description = description;
330
-
331
- if (id) {
332
- await Tenant.query().findById(id).patch(data);
333
- res.json({ id: id });
334
- } else {
335
- var newId = generateId();
336
- data.id = newId;
337
- await Tenant.query().insert(data);
338
- res.json({ id: newId });
339
- }
340
- } catch (err) {
341
- res.status(500).json({ error: 'Something went wrong' });
342
- }
343
- });
344
-
345
- router.post('/tenants/delete', requireSuperAdmin, async function(req, res) {
346
- try {
347
- var { id } = req.body;
348
- if (!id) return res.status(400).json({ error: 'id is required' });
349
- await Tenant.query().findById(id).patch({ isActive: false });
350
- res.json({ ok: true });
351
- } catch (err) {
352
- res.status(500).json({ error: 'Something went wrong' });
353
- }
354
- });
355
-
356
- // Assign/unassign tenant to a user (via mapping table)
357
- // Access controlled via Access Matrix (tenants:edit group), not hardcoded to Super Admin
358
- router.post('/tenants/assign-user', async function(req, res) {
359
- try {
360
- var { userId, tenantId, assign } = req.body;
361
- if (!userId || !tenantId) return res.status(400).json({ error: 'userId and tenantId are required' });
362
-
363
- if (assign === false) {
364
- await UserTenantsMapping.query().delete().where({ userId: userId, tenantId: tenantId });
365
- } else {
366
- var existing = await UserTenantsMapping.query().findOne({ userId: userId, tenantId: tenantId });
367
- if (!existing) {
368
- await UserTenantsMapping.query().insert({
369
- id: generateId(),
370
- userId: userId,
371
- tenantId: tenantId,
372
- recordCreatedBy: req.user ? req.user.id : null
373
- });
374
- }
375
- }
376
-
377
- await clearUserAccess(userId);
378
- res.json({ ok: true });
379
- } catch (err) {
380
- res.status(500).json({ error: 'Something went wrong' });
381
- }
382
- });
383
-
384
312
  return router;
385
313
  }
386
314
 
package/lib/authHelper.js CHANGED
@@ -16,7 +16,7 @@ function configure(config) {
16
16
  }
17
17
 
18
18
  function getSecret() {
19
- return _config.jwtSecret || process.env.JWT_SECRET || 'change_me_in_production';
19
+ return _config.jwtSecret || process.env.AUTH_JWT_SECRET || 'change_me_in_production';
20
20
  }
21
21
 
22
22
  function generateId() {
@@ -35,10 +35,13 @@ async function comparePassword(password, hash) {
35
35
  }
36
36
 
37
37
  function generateAccessToken(user) {
38
+ // Caller is responsible for providing `roles` as an array of role names
39
+ // (see authService.login → withGraphFetched('roles'))
38
40
  const payload = {
39
41
  id: user.id,
40
42
  email: user.email,
41
- name: user.name
43
+ name: user.name,
44
+ roles: Array.isArray(user.roles) ? user.roles : []
42
45
  };
43
46
  const expiresIn = _config.accessTokenExpiresIn || '15m';
44
47
  return jwt.sign(payload, getSecret(), { expiresIn });
@@ -52,6 +55,24 @@ function verifyToken(token) {
52
55
  return jwt.verify(token, getSecret());
53
56
  }
54
57
 
58
+ // Verify the SIGNATURE but ignore expiry — the middleware decides on expiry
59
+ // itself (so it can apply the tolerance window). Returns { decoded, signatureValid }.
60
+ function decodeVerified(token) {
61
+ try {
62
+ return { decoded: jwt.verify(token, getSecret(), { ignoreExpiration: true }), signatureValid: true };
63
+ } catch (err) {
64
+ return { decoded: null, signatureValid: false };
65
+ }
66
+ }
67
+
68
+ // Grace window (seconds) during which an expired access token is still honored
69
+ // and slid to a fresh one. 0 = strict (reject on expiry) = default / back-compat.
70
+ function getToleranceSeconds() {
71
+ var t = _config.accessTokenToleranceSeconds;
72
+ if (t === undefined || t === null) t = parseInt(process.env.AUTH_ACCESS_TOKEN_TOLERANCE_SECONDS || '0', 10);
73
+ return (Number.isFinite(t) && t > 0) ? t : 0;
74
+ }
75
+
55
76
  module.exports = {
56
77
  configure,
57
78
  generateId,
@@ -59,5 +80,7 @@ module.exports = {
59
80
  comparePassword,
60
81
  generateAccessToken,
61
82
  generateRefreshToken,
62
- verifyToken
83
+ verifyToken,
84
+ decodeVerified,
85
+ getToleranceSeconds
63
86
  };