@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
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
var express = require('express');
|
|
2
|
+
var { generateId } = require('./authHelper');
|
|
3
|
+
var { clearUserAccess, clearAccessRules } = require('./accessService');
|
|
4
|
+
var User = require('../models/User');
|
|
5
|
+
var Role = require('../models/Role');
|
|
6
|
+
var Api = require('../models/Api');
|
|
7
|
+
var UiPage = require('../models/UiPage');
|
|
8
|
+
var UiElement = require('../models/UiElement');
|
|
9
|
+
var Menu = require('../models/Menu');
|
|
10
|
+
var UserRolesMapping = require('../models/UserRolesMapping');
|
|
11
|
+
var ApisRolesMapping = require('../models/ApisRolesMapping');
|
|
12
|
+
var UiPagesRolesMapping = require('../models/UiPagesRolesMapping');
|
|
13
|
+
var UiElementsRolesMapping = require('../models/UiElementsRolesMapping');
|
|
14
|
+
var MenuRolesMapping = require('../models/MenuRolesMapping');
|
|
15
|
+
var Tenant = require('../models/Tenant');
|
|
16
|
+
var UserTenantsMapping = require('../models/UserTenantsMapping');
|
|
17
|
+
|
|
18
|
+
// Group field names per type
|
|
19
|
+
var GROUP_FIELDS = {
|
|
20
|
+
apis: 'apiGroup',
|
|
21
|
+
pages: 'uiPagesGroup',
|
|
22
|
+
elements: 'uiElementsGroup',
|
|
23
|
+
menus: 'menuGroup'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Checks if a group value follows the module:action convention
|
|
27
|
+
function isModuleGroup(group) {
|
|
28
|
+
if (!group) return false;
|
|
29
|
+
var parts = group.split(':');
|
|
30
|
+
return parts.length === 2 && parts[0] && parts[1];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createAdminRouter() {
|
|
34
|
+
var router = express.Router();
|
|
35
|
+
|
|
36
|
+
// ─── Users with their roles ───
|
|
37
|
+
router.get('/users', async function(req, res) {
|
|
38
|
+
try {
|
|
39
|
+
var users = await User.query()
|
|
40
|
+
.select('id', 'email', 'name', 'isActive', 'isActivated')
|
|
41
|
+
.withGraphFetched('[roles, tenants]');
|
|
42
|
+
res.json(users);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ─── Roles ───
|
|
49
|
+
router.get('/roles', async function(req, res) {
|
|
50
|
+
try {
|
|
51
|
+
var roles = await Role.query().select('id', 'name');
|
|
52
|
+
res.json(roles);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ─── Access items (apis, pages, elements, menus) with role mappings ───
|
|
59
|
+
router.get('/access-items', async function(req, res) {
|
|
60
|
+
try {
|
|
61
|
+
var [apis, pages, elements, menus] = await Promise.all([
|
|
62
|
+
Api.query().select('id', 'name', 'apiGroup', 'isPublic').withGraphFetched('roles'),
|
|
63
|
+
UiPage.query().select('id', 'name', 'uiPagesGroup', 'isPublic').withGraphFetched('roles'),
|
|
64
|
+
UiElement.query().select('id', 'name', 'uiElementsGroup').withGraphFetched('roles'),
|
|
65
|
+
Menu.query().select('id', 'name', 'menuGroup', 'isPublic').withGraphFetched('roles')
|
|
66
|
+
]);
|
|
67
|
+
res.json({ apis, pages, elements, menus });
|
|
68
|
+
} catch (err) {
|
|
69
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ─── Toggle user-role mapping ───
|
|
74
|
+
router.post('/user-role', async function(req, res) {
|
|
75
|
+
try {
|
|
76
|
+
var { userId, roleId, assign } = req.body;
|
|
77
|
+
|
|
78
|
+
if (!userId || !roleId) {
|
|
79
|
+
return res.status(400).json({ error: 'userId and roleId are required' });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (assign) {
|
|
83
|
+
var existing = await UserRolesMapping.query()
|
|
84
|
+
.findOne({ userId: userId, roleId: roleId });
|
|
85
|
+
if (!existing) {
|
|
86
|
+
await UserRolesMapping.query().insert({
|
|
87
|
+
id: generateId(),
|
|
88
|
+
userId: userId,
|
|
89
|
+
roleId: roleId,
|
|
90
|
+
recordCreatedBy: req.user ? req.user.id : null
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
await UserRolesMapping.query()
|
|
95
|
+
.delete()
|
|
96
|
+
.where({ userId: userId, roleId: roleId });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await clearUserAccess(userId);
|
|
100
|
+
res.json({ ok: true });
|
|
101
|
+
} catch (err) {
|
|
102
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ─── Toggle access-role mapping (single item) ───
|
|
107
|
+
router.post('/access-role', async function(req, res) {
|
|
108
|
+
try {
|
|
109
|
+
var { type, itemId, roleId, assign } = req.body;
|
|
110
|
+
|
|
111
|
+
if (!type || !itemId || !roleId) {
|
|
112
|
+
return res.status(400).json({ error: 'type, itemId, and roleId are required' });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
var config = {
|
|
116
|
+
apis: { Model: ApisRolesMapping, fk: 'apiId' },
|
|
117
|
+
pages: { Model: UiPagesRolesMapping, fk: 'uiPageId' },
|
|
118
|
+
elements: { Model: UiElementsRolesMapping, fk: 'uiElementId' },
|
|
119
|
+
menus: { Model: MenuRolesMapping, fk: 'menuId' }
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
var mapping = config[type];
|
|
123
|
+
if (!mapping) {
|
|
124
|
+
return res.status(400).json({ error: 'Invalid type. Use: apis, pages, elements, menus' });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
var where = { roleId: roleId };
|
|
128
|
+
where[mapping.fk] = itemId;
|
|
129
|
+
|
|
130
|
+
if (assign) {
|
|
131
|
+
var existing = await mapping.Model.query().findOne(where);
|
|
132
|
+
if (!existing) {
|
|
133
|
+
var row = {
|
|
134
|
+
id: generateId(),
|
|
135
|
+
roleId: roleId,
|
|
136
|
+
recordCreatedBy: req.user ? req.user.id : null
|
|
137
|
+
};
|
|
138
|
+
row[mapping.fk] = itemId;
|
|
139
|
+
await mapping.Model.query().insert(row);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
await mapping.Model.query().delete().where(where);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await clearAccessRules();
|
|
146
|
+
res.json({ ok: true });
|
|
147
|
+
} catch (err) {
|
|
148
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ─── Toggle module:action for a role (bulk — toggles all items with that group across all types) ───
|
|
153
|
+
router.post('/module-role', async function(req, res) {
|
|
154
|
+
try {
|
|
155
|
+
var { module: moduleName, action, roleId, assign } = req.body;
|
|
156
|
+
|
|
157
|
+
if (!moduleName || !action || !roleId) {
|
|
158
|
+
return res.status(400).json({ error: 'module, action, and roleId are required' });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
var groupValue = moduleName + ':' + action;
|
|
162
|
+
var createdBy = req.user ? req.user.id : null;
|
|
163
|
+
|
|
164
|
+
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' }
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
var promises = types.map(async function(cfg) {
|
|
172
|
+
var where = {};
|
|
173
|
+
where[cfg.groupField] = groupValue;
|
|
174
|
+
var items = await cfg.Model.query().select('id').where(where);
|
|
175
|
+
|
|
176
|
+
for (var i = 0; i < items.length; i++) {
|
|
177
|
+
var itemWhere = { roleId: roleId };
|
|
178
|
+
itemWhere[cfg.fk] = items[i].id;
|
|
179
|
+
|
|
180
|
+
if (assign) {
|
|
181
|
+
var existing = await cfg.MappingModel.query().findOne(itemWhere);
|
|
182
|
+
if (!existing) {
|
|
183
|
+
var row = { id: generateId(), roleId: roleId, recordCreatedBy: createdBy };
|
|
184
|
+
row[cfg.fk] = items[i].id;
|
|
185
|
+
await cfg.MappingModel.query().insert(row);
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
await cfg.MappingModel.query().delete().where(itemWhere);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await Promise.all(promises);
|
|
194
|
+
await clearAccessRules();
|
|
195
|
+
res.json({ ok: true });
|
|
196
|
+
} catch (err) {
|
|
197
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ─── Master CRUD: Roles ───
|
|
202
|
+
router.get('/master/roles', async function(req, res) {
|
|
203
|
+
try {
|
|
204
|
+
var items = await Role.query().select('id', 'name');
|
|
205
|
+
res.json(items);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
router.post('/master/roles', async function(req, res) {
|
|
212
|
+
try {
|
|
213
|
+
var { id, name } = req.body;
|
|
214
|
+
if (!name) return res.status(400).json({ error: 'name is required' });
|
|
215
|
+
|
|
216
|
+
if (id) {
|
|
217
|
+
await Role.query().findById(id).patch({ name: name });
|
|
218
|
+
res.json({ id: id });
|
|
219
|
+
} else {
|
|
220
|
+
var newId = generateId();
|
|
221
|
+
await Role.query().insert({ id: newId, name: name });
|
|
222
|
+
res.json({ id: newId });
|
|
223
|
+
}
|
|
224
|
+
} catch (err) {
|
|
225
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
router.post('/master/roles/delete', async function(req, res) {
|
|
230
|
+
try {
|
|
231
|
+
var { id } = req.body;
|
|
232
|
+
if (!id) return res.status(400).json({ error: 'id is required' });
|
|
233
|
+
await Role.query().deleteById(id);
|
|
234
|
+
await clearAccessRules();
|
|
235
|
+
res.json({ ok: true });
|
|
236
|
+
} catch (err) {
|
|
237
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ─── Master CRUD factory for APIs, Pages, Elements, Menus ───
|
|
242
|
+
var masterTypes = {
|
|
243
|
+
apis: { Model: Api, fields: ['name', 'apiGroup', 'isPublic'] },
|
|
244
|
+
pages: { Model: UiPage, fields: ['name', 'uiPagesGroup', 'isPublic'] },
|
|
245
|
+
elements: { Model: UiElement, fields: ['name', 'uiElementsGroup'] },
|
|
246
|
+
menus: { Model: Menu, fields: ['name', 'menuGroup', 'isPublic'] }
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
Object.keys(masterTypes).forEach(function(typeKey) {
|
|
250
|
+
var cfg = masterTypes[typeKey];
|
|
251
|
+
|
|
252
|
+
router.get('/master/' + typeKey, async function(req, res) {
|
|
253
|
+
try {
|
|
254
|
+
var selectFields = ['id'].concat(cfg.fields);
|
|
255
|
+
var items = await cfg.Model.query().select(selectFields);
|
|
256
|
+
res.json(items);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
router.post('/master/' + typeKey, async function(req, res) {
|
|
263
|
+
try {
|
|
264
|
+
var id = req.body.id;
|
|
265
|
+
var data = {};
|
|
266
|
+
cfg.fields.forEach(function(f) {
|
|
267
|
+
if (req.body[f] !== undefined) data[f] = req.body[f];
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (!data.name) return res.status(400).json({ error: 'name is required' });
|
|
271
|
+
|
|
272
|
+
if (id) {
|
|
273
|
+
await cfg.Model.query().findById(id).patch(data);
|
|
274
|
+
res.json({ id: id });
|
|
275
|
+
} else {
|
|
276
|
+
var newId = generateId();
|
|
277
|
+
data.id = newId;
|
|
278
|
+
await cfg.Model.query().insert(data);
|
|
279
|
+
res.json({ id: newId });
|
|
280
|
+
}
|
|
281
|
+
} catch (err) {
|
|
282
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
router.post('/master/' + typeKey + '/delete', async function(req, res) {
|
|
287
|
+
try {
|
|
288
|
+
var id = req.body.id;
|
|
289
|
+
if (!id) return res.status(400).json({ error: 'id is required' });
|
|
290
|
+
await cfg.Model.query().deleteById(id);
|
|
291
|
+
await clearAccessRules();
|
|
292
|
+
res.json({ ok: true });
|
|
293
|
+
} catch (err) {
|
|
294
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
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
|
+
return router;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
module.exports = createAdminRouter;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const bcrypt = require('bcrypt');
|
|
3
|
+
const jwt = require('jsonwebtoken');
|
|
4
|
+
|
|
5
|
+
let _config = {};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Configure auth helper.
|
|
9
|
+
* @param {object} config
|
|
10
|
+
* @param {string} config.jwtSecret
|
|
11
|
+
* @param {string} [config.jwtExpiresIn='24h']
|
|
12
|
+
* @param {number} [config.saltRounds=10]
|
|
13
|
+
*/
|
|
14
|
+
function configure(config) {
|
|
15
|
+
_config = { ..._config, ...config };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getSecret() {
|
|
19
|
+
return _config.jwtSecret || process.env.JWT_SECRET || 'change_me_in_production';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function generateId() {
|
|
23
|
+
return crypto.randomBytes(12).toString('hex').substring(0, 25);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function hashPassword(password) {
|
|
27
|
+
const rounds = _config.saltRounds || 10;
|
|
28
|
+
const salt = await bcrypt.genSalt(rounds);
|
|
29
|
+
const hash = await bcrypt.hash(password, salt);
|
|
30
|
+
return { hash, salt };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function comparePassword(password, hash) {
|
|
34
|
+
return bcrypt.compare(password, hash);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function generateAccessToken(user) {
|
|
38
|
+
const payload = {
|
|
39
|
+
id: user.id,
|
|
40
|
+
email: user.email,
|
|
41
|
+
name: user.name
|
|
42
|
+
};
|
|
43
|
+
const expiresIn = _config.accessTokenExpiresIn || '15m';
|
|
44
|
+
return jwt.sign(payload, getSecret(), { expiresIn });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function generateRefreshToken() {
|
|
48
|
+
return crypto.randomBytes(40).toString('hex');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function verifyToken(token) {
|
|
52
|
+
return jwt.verify(token, getSecret());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
configure,
|
|
57
|
+
generateId,
|
|
58
|
+
hashPassword,
|
|
59
|
+
comparePassword,
|
|
60
|
+
generateAccessToken,
|
|
61
|
+
generateRefreshToken,
|
|
62
|
+
verifyToken
|
|
63
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { verifyToken } = require('./authHelper');
|
|
2
|
+
const { validateAccessToken } = require('./sessionService');
|
|
3
|
+
|
|
4
|
+
async function authMiddleware(req, res, next) {
|
|
5
|
+
const authHeader = req.headers.authorization;
|
|
6
|
+
|
|
7
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
8
|
+
return res.status(401).json({ error: 'No token provided' });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const token = authHeader.split(' ')[1];
|
|
12
|
+
|
|
13
|
+
// Step 1: Verify JWT signature + expiry
|
|
14
|
+
let decoded;
|
|
15
|
+
try {
|
|
16
|
+
decoded = verifyToken(token);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
return res.status(401).json({ error: 'Invalid or expired token' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Step 2: Check Redis whitelist (token not revoked)
|
|
22
|
+
const session = await validateAccessToken(token);
|
|
23
|
+
if (!session) {
|
|
24
|
+
return res.status(401).json({ error: 'Session expired or revoked' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
req.user = decoded;
|
|
28
|
+
next();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = authMiddleware;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const { register, activate, login, forgotPassword, resetPassword } = require('./authService');
|
|
3
|
+
const { getUserAccess } = require('./accessService');
|
|
4
|
+
const { getSession, rotateSession, destroySession } = require('./sessionService');
|
|
5
|
+
const authMiddleware = require('./authMiddleware');
|
|
6
|
+
const User = require('../models/User');
|
|
7
|
+
|
|
8
|
+
function logError(req, err) {
|
|
9
|
+
if (req.log) req.log.error(err.message, { stack: err.stack });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createAuthRouter(options = {}) {
|
|
13
|
+
const router = express.Router();
|
|
14
|
+
const resetBaseUrl = options.resetBaseUrl || null;
|
|
15
|
+
|
|
16
|
+
router.post('/register', async function(req, res) {
|
|
17
|
+
try {
|
|
18
|
+
const { email, phoneNumber, name, password } = req.body;
|
|
19
|
+
|
|
20
|
+
if (!email || !password) {
|
|
21
|
+
return res.status(400).json({ error: 'Email and password are required' });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const user = await register({ email, phoneNumber, name, password });
|
|
25
|
+
res.status(201).json({ message: 'Registration successful. Account pending activation.', user });
|
|
26
|
+
} catch (err) {
|
|
27
|
+
logError(req, err);
|
|
28
|
+
if (err.message === 'Email already registered') {
|
|
29
|
+
return res.status(409).json({ error: err.message });
|
|
30
|
+
}
|
|
31
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
router.get('/activate', async function(req, res) {
|
|
36
|
+
try {
|
|
37
|
+
var token = req.query.token;
|
|
38
|
+
if (!token) {
|
|
39
|
+
return res.status(400).json({ error: 'Activation token is required' });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var result = await activate(token);
|
|
43
|
+
res.json({ message: 'Account activated successfully', user: result });
|
|
44
|
+
} catch (err) {
|
|
45
|
+
logError(req, err);
|
|
46
|
+
res.status(400).json({ error: err.message });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
router.post('/login', async function(req, res) {
|
|
51
|
+
try {
|
|
52
|
+
const { email, password } = req.body;
|
|
53
|
+
|
|
54
|
+
if (!email || !password) {
|
|
55
|
+
return res.status(400).json({ error: 'Email and password are required' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const result = await login({ email, password });
|
|
59
|
+
res.json(result);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
logError(req, err);
|
|
62
|
+
if (err.message === 'NOT_ACTIVATED') {
|
|
63
|
+
return res.status(403).json({ error: 'Please wait, someone will activate you.' });
|
|
64
|
+
}
|
|
65
|
+
if (err.message === 'Invalid email or password' || err.message === 'Account is deactivated') {
|
|
66
|
+
return res.status(401).json({ error: err.message });
|
|
67
|
+
}
|
|
68
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
router.post('/forgot-password', async function(req, res) {
|
|
73
|
+
try {
|
|
74
|
+
const { email } = req.body;
|
|
75
|
+
|
|
76
|
+
if (!email) {
|
|
77
|
+
return res.status(400).json({ error: 'Email is required' });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const baseUrl = resetBaseUrl || req.protocol + '://' + req.get('host');
|
|
81
|
+
await forgotPassword({ email }, baseUrl);
|
|
82
|
+
res.json({ message: 'If that email exists, a reset link has been sent.' });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
logError(req, err);
|
|
85
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
router.post('/reset-password', async function(req, res) {
|
|
90
|
+
try {
|
|
91
|
+
const { token, newPassword } = req.body;
|
|
92
|
+
|
|
93
|
+
if (!token || !newPassword) {
|
|
94
|
+
return res.status(400).json({ error: 'Token and new password are required' });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await resetPassword({ token, newPassword });
|
|
98
|
+
res.json({ message: 'Password reset successful.' });
|
|
99
|
+
} catch (err) {
|
|
100
|
+
logError(req, err);
|
|
101
|
+
res.status(400).json({ error: err.message });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
router.post('/refresh', async function(req, res) {
|
|
106
|
+
try {
|
|
107
|
+
const { refreshToken } = req.body;
|
|
108
|
+
|
|
109
|
+
if (!refreshToken) {
|
|
110
|
+
return res.status(400).json({ error: 'Refresh token is required' });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const session = await getSession(refreshToken);
|
|
114
|
+
if (!session || !session.userId) {
|
|
115
|
+
return res.status(401).json({ error: 'Invalid or expired refresh token' });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const user = await User.query().findById(session.userId);
|
|
119
|
+
if (!user || !user.isActive) {
|
|
120
|
+
return res.status(401).json({ error: 'Account not found or deactivated' });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = await rotateSession(refreshToken, user);
|
|
124
|
+
if (!result) {
|
|
125
|
+
return res.status(401).json({ error: 'Invalid or expired refresh token' });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const access = await getUserAccess(user.id);
|
|
129
|
+
|
|
130
|
+
res.json({
|
|
131
|
+
accessToken: result.accessToken,
|
|
132
|
+
refreshToken: result.refreshToken,
|
|
133
|
+
user: {
|
|
134
|
+
id: user.id,
|
|
135
|
+
email: user.email,
|
|
136
|
+
name: user.name,
|
|
137
|
+
isActivated: user.isActivated
|
|
138
|
+
},
|
|
139
|
+
access
|
|
140
|
+
});
|
|
141
|
+
} catch (err) {
|
|
142
|
+
logError(req, err);
|
|
143
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
router.post('/logout', async function(req, res) {
|
|
148
|
+
try {
|
|
149
|
+
const { refreshToken } = req.body;
|
|
150
|
+
|
|
151
|
+
if (refreshToken) {
|
|
152
|
+
await destroySession(refreshToken);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
res.json({ message: 'Logged out successfully' });
|
|
156
|
+
} catch (err) {
|
|
157
|
+
logError(req, err);
|
|
158
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
router.get('/me', authMiddleware, async function(req, res) {
|
|
163
|
+
try {
|
|
164
|
+
const access = await getUserAccess(req.user.id);
|
|
165
|
+
res.json({
|
|
166
|
+
user: req.user,
|
|
167
|
+
access
|
|
168
|
+
});
|
|
169
|
+
} catch (err) {
|
|
170
|
+
logError(req, err);
|
|
171
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Get tenants for the current logged-in user
|
|
176
|
+
router.get('/my-tenants', authMiddleware, async function(req, res) {
|
|
177
|
+
try {
|
|
178
|
+
var user = await User.query().findById(req.user.id).withGraphFetched('tenants');
|
|
179
|
+
var tenants = (user && user.tenants) ? user.tenants : [];
|
|
180
|
+
res.json(tenants.map(function(t) {
|
|
181
|
+
return { id: t.id, name: t.name, code: t.code, description: t.description };
|
|
182
|
+
}));
|
|
183
|
+
} catch (err) {
|
|
184
|
+
logError(req, err);
|
|
185
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return router;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = createAuthRouter;
|