@xeplr/auth 1.0.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/LICENSE +21 -0
- package/bin/migrate.js +95 -0
- package/bin/server.js +35 -0
- package/index.js +165 -0
- package/lib/accessMiddleware.js +79 -0
- package/lib/accessService.js +188 -0
- package/lib/adminRouter.js +387 -0
- package/lib/authHelper.js +63 -0
- package/lib/authMiddleware.js +31 -0
- package/lib/authRouter.js +192 -0
- package/lib/authService.js +194 -0
- package/lib/sessionService.js +210 -0
- package/migrations/0001_users.js +22 -0
- package/migrations/0002_menus.js +16 -0
- package/migrations/0003_apis.js +16 -0
- package/migrations/0004_uiPages.js +16 -0
- package/migrations/0005_uiElements.js +16 -0
- package/migrations/0006_roles.js +15 -0
- package/migrations/0007_apisRolesMapping.js +16 -0
- package/migrations/0008_uiPagesRolesMapping.js +16 -0
- package/migrations/0009_uiElementsRolesMapping.js +16 -0
- package/migrations/0010_menuRolesMapping.js +16 -0
- package/migrations/0011_userRolesMapping.js +16 -0
- package/migrations/0012_users_add_reset_token.js +13 -0
- package/migrations/0013_add_isPublic.js +27 -0
- package/migrations/0014_users_add_activation_token.js +13 -0
- package/migrations/0015_add_mt_columns.js +41 -0
- package/migrations/0016_tenants.js +23 -0
- package/migrations/0017_userTenantsMapping.js +20 -0
- package/models/Api.js +45 -0
- package/models/ApisRolesMapping.js +29 -0
- package/models/Menu.js +45 -0
- package/models/MenuRolesMapping.js +29 -0
- package/models/Role.js +89 -0
- package/models/Tenant.js +63 -0
- package/models/UiElement.js +44 -0
- package/models/UiElementsRolesMapping.js +29 -0
- package/models/UiPage.js +45 -0
- package/models/UiPagesRolesMapping.js +29 -0
- package/models/User.js +76 -0
- package/models/UserRolesMapping.js +29 -0
- package/models/UserTenantsMapping.js +30 -0
- package/models/index.js +29 -0
- package/package.json +31 -0
- package/seeds/001_roles.js +26 -0
- package/seeds/002_apis.js +70 -0
- package/seeds/003_pages.js +41 -0
- package/seeds/004_elements.js +46 -0
- package/seeds/005_menus.js +35 -0
- package/seeds/006_default_tenant.js +50 -0
- package/seeds/zzz_admin_access.js +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Xeplr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/migrate.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { up, rollback, status } = require('@xeplr/db').migrator;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* xeplr-auth-migrate
|
|
8
|
+
*
|
|
9
|
+
* Runs auth migrations bundled with this package.
|
|
10
|
+
* Reuses xeplr-db's migrator — no duplicated knex logic.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* xeplr-auth-migrate up [--db <database>]
|
|
14
|
+
* xeplr-auth-migrate rollback [--db <database>]
|
|
15
|
+
* xeplr-auth-migrate status [--db <database>]
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
function parseArgs(argv) {
|
|
19
|
+
const args = { _: [] };
|
|
20
|
+
for (let i = 0; i < argv.length; i++) {
|
|
21
|
+
if (argv[i].startsWith('--')) {
|
|
22
|
+
const key = argv[i].slice(2);
|
|
23
|
+
args[key] = argv[i + 1] || true;
|
|
24
|
+
i++;
|
|
25
|
+
} else {
|
|
26
|
+
args._.push(argv[i]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return args;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function main() {
|
|
33
|
+
const args = parseArgs(process.argv.slice(2));
|
|
34
|
+
const command = args._[0];
|
|
35
|
+
|
|
36
|
+
// Always use bundled migrations directory
|
|
37
|
+
const options = {
|
|
38
|
+
...args,
|
|
39
|
+
db: args.db || process.env.DB_AUTH || process.env.DB_NAME,
|
|
40
|
+
dir: path.join(__dirname, '..', 'migrations')
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
switch (command) {
|
|
44
|
+
case 'up': {
|
|
45
|
+
const result = await up(options);
|
|
46
|
+
if (result.migrations.length === 0) {
|
|
47
|
+
console.log('Already up to date');
|
|
48
|
+
} 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:`);
|
|
60
|
+
result.migrations.forEach(m => console.log(` - ${m}`));
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case 'status': {
|
|
65
|
+
const result = await status(options);
|
|
66
|
+
console.log('Completed migrations:');
|
|
67
|
+
result.completed.forEach(m => console.log(` ✓ ${m}`));
|
|
68
|
+
if (result.pending.length) {
|
|
69
|
+
console.log('Pending migrations:');
|
|
70
|
+
result.pending.forEach(m => console.log(` ○ ${m}`));
|
|
71
|
+
} else {
|
|
72
|
+
console.log('No pending migrations');
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
console.log('xeplr-auth-migrate - Auth database migrations');
|
|
78
|
+
console.log('');
|
|
79
|
+
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');
|
|
83
|
+
console.log('');
|
|
84
|
+
console.log('Options:');
|
|
85
|
+
console.log(' --db Database name (or DB_AUTH env)');
|
|
86
|
+
console.log(' --host DB host (default: DB_HOST env or localhost)');
|
|
87
|
+
console.log(' --user DB user (default: DB_USER env or root)');
|
|
88
|
+
console.log(' --password DB password (default: DB_PASSWORD env)');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
main().catch(err => {
|
|
93
|
+
console.error(err.message);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
});
|
package/bin/server.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standalone auth server entry point.
|
|
5
|
+
* Designed to be forked as a child process or run directly.
|
|
6
|
+
*
|
|
7
|
+
* Reads config from AUTH_CONFIG env var (JSON) or individual env vars.
|
|
8
|
+
*/
|
|
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
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { getConnection, bindModels, resolveConfig, migrator } = require('@xeplr/db');
|
|
3
|
+
const { createApp } = require('@xeplr/base-apis');
|
|
4
|
+
const authHelper = require('./lib/authHelper');
|
|
5
|
+
const authMiddleware = require('./lib/authMiddleware');
|
|
6
|
+
const authService = require('./lib/authService');
|
|
7
|
+
const accessService = require('./lib/accessService');
|
|
8
|
+
const { accessMiddleware, requireRole } = require('./lib/accessMiddleware');
|
|
9
|
+
const sessionService = require('./lib/sessionService');
|
|
10
|
+
const createAuthRouter = require('./lib/authRouter');
|
|
11
|
+
const createAdminRouter = require('./lib/adminRouter');
|
|
12
|
+
const models = require('./models');
|
|
13
|
+
|
|
14
|
+
let _initialized = false;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize xeplr-auth.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} config
|
|
20
|
+
* @param {string} config.database - Database name for auth tables
|
|
21
|
+
* @param {object} [config.db] - DB connection options { host, user, password, port }
|
|
22
|
+
* @param {object} [config.jwt] - JWT options { secret, expiresIn }
|
|
23
|
+
* @param {object} [config.email] - Email config { provider, smtp, aws, azure, brevo }
|
|
24
|
+
* @param {string} [config.resetBaseUrl] - Base URL for password reset links
|
|
25
|
+
*/
|
|
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);
|
|
32
|
+
|
|
33
|
+
// Configure JWT
|
|
34
|
+
if (config.jwt) {
|
|
35
|
+
authHelper.configure({
|
|
36
|
+
jwtSecret: config.jwt.secret,
|
|
37
|
+
accessTokenExpiresIn: config.jwt.accessTokenExpiresIn || '15m'
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Configure email provider
|
|
42
|
+
if (config.email) {
|
|
43
|
+
authService.configureEmail(config.email);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_initialized = true;
|
|
47
|
+
|
|
48
|
+
return connection;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the Express router for auth API endpoints.
|
|
53
|
+
* Must call init() first.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} [options]
|
|
56
|
+
* @param {string} [options.resetBaseUrl] - Override reset password base URL
|
|
57
|
+
*/
|
|
58
|
+
function router(options = {}) {
|
|
59
|
+
return createAuthRouter(options);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function adminRouter() {
|
|
63
|
+
return createAdminRouter();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Start xeplr-auth as a standalone Express API server.
|
|
68
|
+
* Calls init() internally, then boots Express via xeplr-base-apis.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} config
|
|
71
|
+
* @param {number|string} config.port - Port to listen on (default: AUTH_PORT env or 19001)
|
|
72
|
+
* @param {string} [config.database] - Database name
|
|
73
|
+
* @param {object} [config.db] - DB connection options
|
|
74
|
+
* @param {object} [config.jwt] - JWT options
|
|
75
|
+
* @param {string} [config.emailServiceUrl] - URL of xeplr-email service
|
|
76
|
+
* @param {string} [config.resetBaseUrl] - Base URL for reset links
|
|
77
|
+
* @param {object} [config.corsOptions] - CORS options
|
|
78
|
+
* @param {Function[]} [config.middleware] - Additional middleware
|
|
79
|
+
* @returns {http.Server}
|
|
80
|
+
*/
|
|
81
|
+
function start(config = {}) {
|
|
82
|
+
init(config);
|
|
83
|
+
|
|
84
|
+
const port = config.port || process.env.AUTH_PORT || 19001;
|
|
85
|
+
const authRouter = createAuthRouter({ resetBaseUrl: config.resetBaseUrl });
|
|
86
|
+
const adminRtr = createAdminRouter();
|
|
87
|
+
|
|
88
|
+
return createApp(port, 'xeplr-auth', {
|
|
89
|
+
corsOptions: config.corsOptions,
|
|
90
|
+
middleware: config.middleware,
|
|
91
|
+
routes: { '/auth/api': authRouter, '/auth/api/admin': adminRtr }
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Boot xeplr-auth as a fully independent service.
|
|
97
|
+
* Resolves DB config, runs migrations/seeds, and starts the server.
|
|
98
|
+
*
|
|
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
|
+
* @returns {Promise<http.Server>}
|
|
112
|
+
*/
|
|
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({
|
|
122
|
+
db: database,
|
|
123
|
+
dir: path.join(authPkgDir, 'migrations'),
|
|
124
|
+
extDir: config.migrationsDir || null,
|
|
125
|
+
type: 'precede',
|
|
126
|
+
connectionName: connName
|
|
127
|
+
});
|
|
128
|
+
|
|
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
|
+
}
|
|
134
|
+
|
|
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
|
+
});
|
|
143
|
+
|
|
144
|
+
if (seedResult.length > 0) {
|
|
145
|
+
console.log('[xeplr-auth] ran ' + seedResult.length + ' seeds');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return start(Object.assign({ database, connectionName: connName }, config));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
init,
|
|
153
|
+
router,
|
|
154
|
+
adminRouter,
|
|
155
|
+
start,
|
|
156
|
+
boot,
|
|
157
|
+
authMiddleware,
|
|
158
|
+
accessMiddleware,
|
|
159
|
+
requireRole,
|
|
160
|
+
authHelper,
|
|
161
|
+
authService,
|
|
162
|
+
accessService,
|
|
163
|
+
sessionService,
|
|
164
|
+
models
|
|
165
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const { verifyToken } = require('./authHelper');
|
|
2
|
+
const { getApiRule, userHasApiAccess } = require('./accessService');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Access-aware middleware.
|
|
6
|
+
* Uses Redis-cached API rules: isPublic → unmapped → role check.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* app.use('/api', accessMiddleware());
|
|
10
|
+
*
|
|
11
|
+
* Options:
|
|
12
|
+
* apiNameResolver(req) - function that returns the API name to check.
|
|
13
|
+
* Defaults to req.baseUrl + req.path (e.g. "/api/orders")
|
|
14
|
+
*/
|
|
15
|
+
function accessMiddleware(options = {}) {
|
|
16
|
+
const resolveApiName = options.apiNameResolver || ((req) => {
|
|
17
|
+
return (req.baseUrl + req.path).replace(/\/+$/, '') || '/';
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return async function(req, res, next) {
|
|
21
|
+
const apiName = resolveApiName(req);
|
|
22
|
+
|
|
23
|
+
// Single cached lookup for API rule (public flag + role IDs)
|
|
24
|
+
const rule = await getApiRule(apiName);
|
|
25
|
+
|
|
26
|
+
// Not in DB, or marked public, or no role mappings = open
|
|
27
|
+
if (!rule.exists || rule.isPublic || rule.unmapped) return next();
|
|
28
|
+
|
|
29
|
+
// From here, auth is required
|
|
30
|
+
const authHeader = req.headers.authorization;
|
|
31
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
32
|
+
return res.status(401).json({ error: 'No token provided' });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const token = authHeader.split(' ')[1];
|
|
36
|
+
let decoded;
|
|
37
|
+
try {
|
|
38
|
+
decoded = verifyToken(token);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
return res.status(401).json({ error: 'Invalid or expired token' });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
req.user = decoded;
|
|
44
|
+
|
|
45
|
+
// Check role-based access (also cached)
|
|
46
|
+
const hasAccess = await userHasApiAccess(decoded.id, apiName);
|
|
47
|
+
if (!hasAccess) {
|
|
48
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
next();
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Simple role-check middleware (no DB lookup, uses roles from JWT or req.user).
|
|
57
|
+
* Use after authMiddleware.
|
|
58
|
+
*
|
|
59
|
+
* Usage:
|
|
60
|
+
* app.use('/admin', authMiddleware, requireRole('admin'));
|
|
61
|
+
*/
|
|
62
|
+
function requireRole(...roles) {
|
|
63
|
+
return function(req, res, next) {
|
|
64
|
+
if (!req.user || !req.user.roles) {
|
|
65
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const userRoles = Array.isArray(req.user.roles) ? req.user.roles : [];
|
|
69
|
+
const hasRole = roles.some(r => userRoles.includes(r));
|
|
70
|
+
|
|
71
|
+
if (!hasRole) {
|
|
72
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
next();
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { accessMiddleware, requireRole };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const { cache } = require('@xeplr/utils');
|
|
2
|
+
const User = require('../models/User');
|
|
3
|
+
const Api = require('../models/Api');
|
|
4
|
+
const UiPage = require('../models/UiPage');
|
|
5
|
+
const Menu = require('../models/Menu');
|
|
6
|
+
const UiElement = require('../models/UiElement');
|
|
7
|
+
|
|
8
|
+
// Cache TTLs (seconds)
|
|
9
|
+
const USER_ACCESS_TTL = 300; // 5 min — user's full access object
|
|
10
|
+
const API_RULES_TTL = 600; // 10 min — api public/role data (changes rarely)
|
|
11
|
+
|
|
12
|
+
// Cache key helpers
|
|
13
|
+
const userAccessKey = (userId) => `access:user:${userId}`;
|
|
14
|
+
const apiRulesKey = (apiName) => `access:api:${encodeURIComponent(apiName)}`;
|
|
15
|
+
const publicItemsKey = 'access:public';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Fetch the full access object for a user.
|
|
19
|
+
* Cached in Redis. Use clearUserAccess() to invalidate.
|
|
20
|
+
*/
|
|
21
|
+
async function getUserAccess(userId) {
|
|
22
|
+
// Check cache first
|
|
23
|
+
const cached = await cache.get(userAccessKey(userId));
|
|
24
|
+
if (cached) return cached;
|
|
25
|
+
|
|
26
|
+
const user = await User.query()
|
|
27
|
+
.findById(userId)
|
|
28
|
+
.withGraphFetched('roles.[apis, uiPages, uiElements, menus]');
|
|
29
|
+
|
|
30
|
+
if (!user || !user.roles) {
|
|
31
|
+
const empty = { roles: [], pages: [], apis: [], menus: [], elements: [] };
|
|
32
|
+
await cache.set(userAccessKey(userId), empty, USER_ACCESS_TTL);
|
|
33
|
+
return empty;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const roleNames = new Set();
|
|
37
|
+
const pages = new Set();
|
|
38
|
+
const apis = new Set();
|
|
39
|
+
const menus = new Set();
|
|
40
|
+
const elements = new Set();
|
|
41
|
+
|
|
42
|
+
for (const role of user.roles) {
|
|
43
|
+
roleNames.add(role.name);
|
|
44
|
+
|
|
45
|
+
if (role.apis) {
|
|
46
|
+
for (const api of role.apis) apis.add(api.name);
|
|
47
|
+
}
|
|
48
|
+
if (role.uiPages) {
|
|
49
|
+
for (const page of role.uiPages) pages.add(page.name);
|
|
50
|
+
}
|
|
51
|
+
if (role.menus) {
|
|
52
|
+
for (const menu of role.menus) menus.add(menu.name);
|
|
53
|
+
}
|
|
54
|
+
if (role.uiElements) {
|
|
55
|
+
for (const el of role.uiElements) elements.add(el.name);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Add public items
|
|
60
|
+
const publicItems = await getPublicItems();
|
|
61
|
+
for (const api of publicItems.apis) apis.add(api);
|
|
62
|
+
for (const page of publicItems.pages) pages.add(page);
|
|
63
|
+
for (const menu of publicItems.menus) menus.add(menu);
|
|
64
|
+
|
|
65
|
+
const access = {
|
|
66
|
+
roles: [...roleNames],
|
|
67
|
+
pages: [...pages],
|
|
68
|
+
apis: [...apis],
|
|
69
|
+
menus: [...menus],
|
|
70
|
+
elements: [...elements]
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
await cache.set(userAccessKey(userId), access, USER_ACCESS_TTL);
|
|
74
|
+
return access;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get all public items (cached separately, shared across users).
|
|
79
|
+
*/
|
|
80
|
+
async function getPublicItems() {
|
|
81
|
+
const cached = await cache.get(publicItemsKey);
|
|
82
|
+
if (cached) return cached;
|
|
83
|
+
|
|
84
|
+
const [publicApis, publicPages, publicMenus] = await Promise.all([
|
|
85
|
+
Api.query().where({ isPublic: 1 }),
|
|
86
|
+
UiPage.query().where({ isPublic: 1 }),
|
|
87
|
+
Menu.query().where({ isPublic: 1 })
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
const result = {
|
|
91
|
+
apis: publicApis.map(a => a.name),
|
|
92
|
+
pages: publicPages.map(p => p.name),
|
|
93
|
+
menus: publicMenus.map(m => m.name)
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
await cache.set(publicItemsKey, result, API_RULES_TTL);
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get cached API rule (public flag + role IDs).
|
|
102
|
+
* Avoids repeated DB lookups in middleware.
|
|
103
|
+
*/
|
|
104
|
+
async function getApiRule(apiName) {
|
|
105
|
+
const cacheKey = apiRulesKey(apiName);
|
|
106
|
+
const cached = await cache.get(cacheKey);
|
|
107
|
+
if (cached) return cached;
|
|
108
|
+
|
|
109
|
+
const api = await Api.query()
|
|
110
|
+
.findOne({ name: apiName })
|
|
111
|
+
.withGraphFetched('roles');
|
|
112
|
+
|
|
113
|
+
if (!api) {
|
|
114
|
+
// Not registered = open
|
|
115
|
+
const rule = { exists: false, isPublic: true, roleIds: [] };
|
|
116
|
+
await cache.set(cacheKey, rule, API_RULES_TTL);
|
|
117
|
+
return rule;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const roleIds = (api.roles || []).map(r => r.id);
|
|
121
|
+
const rule = {
|
|
122
|
+
exists: true,
|
|
123
|
+
isPublic: !!api.isPublic,
|
|
124
|
+
unmapped: roleIds.length === 0,
|
|
125
|
+
roleIds
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
await cache.set(cacheKey, rule, API_RULES_TTL);
|
|
129
|
+
return rule;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if a user has access to a specific API.
|
|
134
|
+
* Uses cached API rules and cached user roles.
|
|
135
|
+
*/
|
|
136
|
+
async function userHasApiAccess(userId, apiName) {
|
|
137
|
+
const rule = await getApiRule(apiName);
|
|
138
|
+
|
|
139
|
+
// Not registered in DB, or marked public, or no role mappings = open
|
|
140
|
+
if (!rule.exists || rule.isPublic || rule.unmapped) return true;
|
|
141
|
+
|
|
142
|
+
// Get user's role IDs (from user access cache)
|
|
143
|
+
const userRoleCacheKey = `access:userRoles:${userId}`;
|
|
144
|
+
let userRoleIds = await cache.get(userRoleCacheKey);
|
|
145
|
+
|
|
146
|
+
if (!userRoleIds) {
|
|
147
|
+
const user = await User.query()
|
|
148
|
+
.findById(userId)
|
|
149
|
+
.withGraphFetched('roles');
|
|
150
|
+
|
|
151
|
+
userRoleIds = (user && user.roles) ? user.roles.map(r => r.id) : [];
|
|
152
|
+
await cache.set(userRoleCacheKey, userRoleIds, USER_ACCESS_TTL);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const userRoleSet = new Set(userRoleIds);
|
|
156
|
+
return rule.roleIds.some(id => userRoleSet.has(id));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Clear all cached access data for a user.
|
|
161
|
+
* Call this on login so the user gets fresh permissions.
|
|
162
|
+
*/
|
|
163
|
+
async function clearUserAccess(userId) {
|
|
164
|
+
await Promise.all([
|
|
165
|
+
cache.del(userAccessKey(userId)),
|
|
166
|
+
cache.del(`access:userRoles:${userId}`)
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Clear all cached API rules and public items.
|
|
172
|
+
* Call this when you modify role mappings, isPublic flags, etc.
|
|
173
|
+
*/
|
|
174
|
+
async function clearAccessRules() {
|
|
175
|
+
await Promise.all([
|
|
176
|
+
cache.del(publicItemsKey),
|
|
177
|
+
cache.delPattern('access:api:*')
|
|
178
|
+
]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
getUserAccess,
|
|
183
|
+
userHasApiAccess,
|
|
184
|
+
clearUserAccess,
|
|
185
|
+
clearAccessRules,
|
|
186
|
+
getApiRule,
|
|
187
|
+
getPublicItems
|
|
188
|
+
};
|