@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.
- package/bin/migrate.js +43 -24
- package/bin/server.js +12 -30
- package/index.js +135 -51
- package/lib/adminRouter.js +20 -92
- package/lib/authHelper.js +26 -3
- package/lib/authMiddleware.js +39 -13
- package/lib/authRouter.js +140 -11
- package/lib/authService.js +327 -9
- package/lib/hooks.js +50 -0
- package/lib/mtMembershipMiddleware.js +61 -0
- package/lib/seed.js +74 -0
- package/lib/sessionService.js +54 -6
- package/lib/ticketService.js +88 -0
- package/lib/tokenDecision.js +45 -0
- package/migrations/0001_extensions.sql +9 -0
- package/migrations/0002_users.sql +31 -0
- package/migrations/0003_catalog_tables.sql +82 -0
- package/migrations/0004_role_mappings.sql +78 -0
- package/migrations/0005_user_tenants_mapping.sql +39 -0
- package/migrations/0006_seed_catalog.sql +120 -0
- package/models/Api.js +3 -0
- package/models/ApisRolesMapping.js +3 -0
- package/models/Menu.js +3 -0
- package/models/MenuRolesMapping.js +3 -0
- package/models/Role.js +8 -0
- package/models/UiElement.js +3 -0
- package/models/UiElementsRolesMapping.js +3 -0
- package/models/UiPage.js +3 -0
- package/models/UiPagesRolesMapping.js +3 -0
- package/models/User.js +3 -13
- package/models/UserRolesMapping.js +3 -0
- package/models/UserTenantsMapping.js +20 -2
- package/models/index.js +0 -2
- package/package.json +26 -5
- package/migrations/0001_users.js +0 -22
- package/migrations/0002_menus.js +0 -16
- package/migrations/0003_apis.js +0 -16
- package/migrations/0004_uiPages.js +0 -16
- package/migrations/0005_uiElements.js +0 -16
- package/migrations/0006_roles.js +0 -15
- package/migrations/0007_apisRolesMapping.js +0 -16
- package/migrations/0008_uiPagesRolesMapping.js +0 -16
- package/migrations/0009_uiElementsRolesMapping.js +0 -16
- package/migrations/0010_menuRolesMapping.js +0 -16
- package/migrations/0011_userRolesMapping.js +0 -16
- package/migrations/0012_users_add_reset_token.js +0 -13
- package/migrations/0013_add_isPublic.js +0 -27
- package/migrations/0014_users_add_activation_token.js +0 -13
- package/migrations/0015_add_mt_columns.js +0 -41
- package/migrations/0016_tenants.js +0 -23
- package/migrations/0017_userTenantsMapping.js +0 -20
- package/models/Tenant.js +0 -63
- package/seeds/001_roles.js +0 -26
- package/seeds/002_apis.js +0 -70
- package/seeds/003_pages.js +0 -41
- package/seeds/004_elements.js +0 -46
- package/seeds/005_menus.js +0 -35
- package/seeds/006_default_tenant.js +0 -50
- 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,
|
|
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
|
|
10
|
-
*
|
|
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
|
|
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
|
-
//
|
|
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.
|
|
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(`
|
|
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 [--
|
|
81
|
-
console.log('
|
|
82
|
-
console.log('
|
|
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
|
|
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
|
|
5
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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,
|
|
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
|
-
//
|
|
28
|
-
const dbName = config.database || process.env.
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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(
|
|
124
|
-
extDir:
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
console.log('[xeplr-auth] migrations up to date');
|
|
133
|
-
}
|
|
145
|
+
banner();
|
|
146
|
+
return server;
|
|
147
|
+
}
|
|
134
148
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
};
|
package/lib/adminRouter.js
CHANGED
|
@@ -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('
|
|
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.
|
|
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
|
};
|