Skip to content
This repository was archived by the owner on Jan 4, 2026. It is now read-only.

Commit 80a021c

Browse files
author
Bo Motlagh
authored
Merge pull request #170 from UnitedEffects/issue169
resolves #169
2 parents bd24e0a + 7d6d591 commit 80a021c

12 files changed

Lines changed: 227 additions & 30 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ue-auth",
33
"altName": "UE-Auth",
4-
"version": "0.40.0",
4+
"version": "1.0.0",
55
"description": "UE Auth is a multi-tenant OIDC Provider, User Management, B2B Product Access, and Roles/Permissions Management system intended to create a single hybrid solution to serve as Identity and Access for both self-registered B2C Apps and Enterprise B2B Solutions",
66
"private": false,
77
"license": "SEE LICENSE IN ./LICENSE.md",

src/api/accounts/account.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,48 @@ const cryptoRandomString = require('crypto-random-string');
1616
const config = require('../../config');
1717

1818
export default {
19+
async importAccounts(authGroup, global, array, creator, customDomain) {
20+
let failed = [];
21+
let success = [];
22+
let ok = 0;
23+
const attempted = array.length;
24+
const accounts = [];
25+
array.map((acc) => {
26+
const data = {
27+
email: acc.email,
28+
authGroup: authGroup.id,
29+
username: acc.username || acc.email,
30+
password: cryptoRandomString({length: 16, type: 'url-safe'}),
31+
phone: acc.phone,
32+
modifiedBy: creator
33+
};
34+
if(acc.id) data._id = acc.id;
35+
accounts.push(data);
36+
});
37+
try {
38+
const result = await dal.writeMany(accounts);
39+
ok = result?.length || 0;
40+
success = JSON.parse(JSON.stringify(result));
41+
} catch (error) {
42+
ok = error?.insertedDocs?.length || 0;
43+
failed = error?.writeErrors;
44+
success = error?.insertedDocs;
45+
}
46+
// todo - build bulk notification system to make this possible
47+
/*
48+
if (global.notifications.enabled === true &&
49+
authGroup.pluginOptions.notification.enabled === true &&
50+
authGroup.config.autoVerify === true) {
51+
try {
52+
await this.bulkResetOrVerify(authGroup, global, success, creator, false, customDomain);
53+
} catch (er) {
54+
console.error(er);
55+
failed.push({ warning: er.message, message: 'some accounts may not have received a notification' });
56+
}
57+
}
58+
*/
59+
return { warning: 'Auto verify does not work with bulk imports. You will need to send password reset notifications or direct your users to the self-service password reset page.', attempted, ok, failed, success };
60+
},
1961
async writeAccount(data, creator = undefined) {
2062
data.email = data.email.toLowerCase();
2163
if(!data.username) data.username = data.email;
@@ -131,6 +173,24 @@ export default {
131173
const aliasDns = authGroup.aliasDnsOIDC || undefined;
132174
return this.resetOrVerify(authGroup, settings, user, ['email'], undefined, true, aliasDns);
133175
},
176+
177+
// @notTested
178+
// todo - incomplete implementation for bulk user import
179+
async bulkResetOrVerify(authGroup, globalSettings, users, activeUser = undefined, aliasDns = undefined) {
180+
const iAccessTokens = await iat.generateManyIAT(900, ['auth_group'], authGroup, users);
181+
await Promise.all(users.map(async(user) => {
182+
const findToken = iAccessTokens.filter((t) => {
183+
return (t.payload.sub === user._id);
184+
});
185+
if(findToken.length !== 0) {
186+
const data = this.verifyAccountOptions(authGroup, user, findToken[0].payload.jti, [], activeUser, aliasDns);
187+
// todo - you'll still need to create a bulk notification system and only hit DB once for auth + notify objects
188+
// todo n.notify will not work well for this...
189+
return n.notify(globalSettings, data, authGroup);
190+
}
191+
return user;
192+
}));
193+
},
134194
// @notTested
135195
async resetOrVerify(authGroup, globalSettings, user, formats = [], activeUser = undefined, reset=true, aliasDns = undefined) {
136196
let iAccessToken;
@@ -144,7 +204,7 @@ export default {
144204
let data;
145205
if(reset === true){
146206
data = this.resetPasswordOptions(authGroup, user, iAccessToken, formats, activeUser, aliasDns);
147-
} else data = this.verifyAccountOptions(authGroup, user, iAccessToken, formats = [], activeUser, aliasDns);
207+
} else data = this.verifyAccountOptions(authGroup, user, iAccessToken, formats, activeUser, aliasDns);
148208

149209
return n.notify(globalSettings, data, authGroup);
150210
} catch (error) {

src/api/accounts/api.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,25 @@ const config = require('../../config');
2121
const RESOURCE = 'Account';
2222

2323
const api = {
24+
async importAccounts(req, res, next) {
25+
try {
26+
if (req.authGroup.active === false) throw Boom.forbidden('You can not add members to an inactive group');
27+
if (!Array.isArray(req.body)) throw Boom.badRequest('You are expected to send an array of accounts');
28+
if(req.body.length > 1000) throw Boom.badRequest('At this time, we can only import 1000 accounts a time');
29+
const result = await acct.importAccounts(req.authGroup, req.globalSettings, req.body, req.user.sub || 'SYSTEM_ADMIN', req.customDomain);
30+
return res.respond(say.created(result, RESOURCE));
31+
} catch(error) {
32+
ueEvents.emit(req.authGroup.id, 'ue.account.import.error', error);
33+
next(error);
34+
}
35+
},
2436
async writeAccount(req, res, next) {
2537
try {
2638
if (req.groupActivationEvent === true) return api.activateGroupWithAccount(req, res, next);
2739
if (req.authGroup.active === false) throw Boom.forbidden('You can not add members to an inactive group');
2840
if (!req.body.email) throw Boom.preconditionRequired('username is required');
2941
if (req.body.generatePassword === true) {
30-
req.body.password = cryptoRandomString({length: 32, type: 'url-safe'});
42+
req.body.password = cryptoRandomString({length: 16, type: 'url-safe'});
3143
}
3244
if (!req.body.password) throw Boom.preconditionRequired('password is required');
3345
const password = req.body.password;

src/api/accounts/dal.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ export default {
66
const account = new Account(data);
77
return account.save();
88
},
9+
async writeMany(accounts) {
10+
return Account.insertMany(accounts, { ordered: false });
11+
},
912
async getAccounts(g, query) {
1013
query.query.authGroup = g;
1114
return Account.find(query.query).select(query.projection).sort(query.sort).skip(query.skip).limit(query.limit);

src/api/oidc/initialAccess/dal.js

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
import IAT from '../models/initialAccessToken';
22

33
export default {
4-
async updateAuthGroup(id, meta) {
5-
const update = {
6-
'payload.auth_group': meta.auth_group
7-
};
8-
if(meta.sub) update['payload.sub'] = meta.sub;
9-
if(meta.email) update['payload.email'] = meta.email;
10-
if(meta.uid) update['payload.uid'] = meta.uid;
4+
async updateAuthGroup(id, meta) {
5+
const update = {
6+
'payload.auth_group': meta.auth_group
7+
};
8+
if(meta.sub) update['payload.sub'] = meta.sub;
9+
if(meta.email) update['payload.email'] = meta.email;
10+
if(meta.uid) update['payload.uid'] = meta.uid;
1111

12-
return IAT.findOneAndUpdate( { _id: id }, update, {new: true});
13-
},
12+
return IAT.findOneAndUpdate( { _id: id }, update, {new: true});
13+
},
1414

15-
async getOne(id, authGroupId) {
16-
return IAT.findOne({ _id: id, 'payload.auth_group': authGroupId });
17-
},
15+
async getOne(id, authGroupId) {
16+
return IAT.findOne({ _id: id, 'payload.auth_group': authGroupId });
17+
},
1818

19-
async deleteOne(id, authGroupId) {
20-
return IAT.findOneAndRemove( { _id: id , 'payload.auth_group': authGroupId });
21-
}
22-
}
19+
async deleteOne(id, authGroupId) {
20+
return IAT.findOneAndRemove( { _id: id , 'payload.auth_group': authGroupId });
21+
},
22+
23+
async insertMany(docs) {
24+
return IAT.insertMany(docs, { ordered: false });
25+
}
26+
};

src/api/oidc/initialAccess/iat.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@ import oidc from '../oidc';
22
import dal from './dal';
33

44
export default {
5+
// todo - incomplete implementation for bulk user import
6+
async generateManyIAT(expiresIn, policies, authGroup, users) {
7+
if(!authGroup) throw new Error('authGroup not defined');
8+
const setOfIATs = [];
9+
await Promise.all((users.map(async(x) => {
10+
const iat = new (oidc(authGroup).InitialAccessToken)({ expiresIn, policies });
11+
iat.payload.auth_group = authGroup.id;
12+
iat.payload.sub = x._id || x.id;
13+
iat.payload.email = x.email;
14+
setOfIATs.push(iat);
15+
return x;
16+
})));
17+
return dal.insertMany(setOfIATs);
18+
},
519
async generateIAT(expiresIn, policies, authGroup, meta = {}) {
620
if(!authGroup) throw new Error('authGroup not defined');
721
return new (oidc(authGroup).InitialAccessToken)({ expiresIn, policies }).save().then(async (x) => {

src/events/factory.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ const OP_EVENTS = {
8888
'ue.account.create',
8989
'ue.account.edit',
9090
'ue.account.destroy',
91-
'ue.account.error'
91+
'ue.account.error',
92+
'ue.account.import.error'
9293
],
9394
group: [
9495
'ue.group.create',

src/routes/identity.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,15 @@ router.post('/:group/account', [
184184
m.permissions,
185185
m.access('accounts')
186186
], account.writeAccount);
187+
router.post('/:group/accounts', [
188+
m.validateAuthGroup,
189+
m.schemaCheck,
190+
m.isAuthenticated,
191+
m.getGlobalPluginSettings,
192+
m.organizationContext,
193+
m.permissions,
194+
m.access('accounts')
195+
], account.importAccounts);
187196
router.get('/:group/accounts', [
188197
m.validateAuthGroup,
189198
m.isAuthenticated,

src/routes/root.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import m from '../middleware';
44

55
const router = express.Router();
66
const pJson = require('../../package.json');
7+
const config = require('../config');
78

89
router.get('/', m.validateHostDomain, (req, res) => {
910
const date = new Date();
@@ -12,19 +13,23 @@ router.get('/', m.validateHostDomain, (req, res) => {
1213
version: pJson.version,
1314
by: `${req.authGroup.name} Platform`,
1415
url: req.authGroup.primaryDomain,
16+
company: req.authGroup.name,
1517
year: date.getFullYear(),
1618
home: pJson.homepage,
17-
custom: true
19+
custom: true,
20+
production: (config.ENV === 'production')
1821
});
1922
}
2023
return res.render('index', {
2124
title: pJson.name, version: pJson.version,
2225
description: pJson.description,
2326
by: pJson.author,
24-
url: pJson.person.url,
27+
url: config.INIT_ROOT_PRIMARY_DOMAIN,
28+
company: config.ROOT_COMPANY_NAME,
2529
year: date.getFullYear(),
2630
home: pJson.homepage,
27-
custom: false
31+
custom: false,
32+
production: (config.ENV === 'production')
2833
});
2934
});
3035

swagger.yaml

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,7 @@ paths:
763763
tags:
764764
- Users
765765
summary: Register a new user
766-
description: Register a new user to an Auth Group independant of any organization or other access considerations. Self-registration is possible if the Auth Group has defined locked=false. Otherwise, an appropriately permissioned member of the Auth Group must create the user. Please note that the only data provided by this endpoint (or any Account API) is the id, username, and email (and creat/modify/active meta data) of the user within this AuthGroup. The Account record holds no personal information aside from this email address and an optional phone number which is only visible to that Account owner. It is possible to add personal data into the metadata field, but this is a discouraged action.
766+
description: Register a new user to an Auth Group independant of any organization or other access considerations. Self-registration is possible if the Auth Group has defined locked=false. Otherwise, an appropriately permissioned member of the Auth Group must create the user. Please note that the only data provided by this endpoint (or any Account API) is the id, username, and email (and creat/modify/active meta data) of the user within this AuthGroup. The Account record holds no personal information aside from this email address and an optional phone number which is only visible to that Account owner; however, if you supply profile information, a secured profile record will be generated which the account holder can administrate later.
767767
operationId: writeAccount
768768
parameters:
769769
- name: group
@@ -854,6 +854,69 @@ paths:
854854
- bearer: []
855855
- openId: []
856856
/api/{group}/accounts:
857+
post:
858+
tags:
859+
- Users
860+
summary: Import users to the Auth Group
861+
description: This API allows you to import an array of users to the Auth Group independant of any organization or other access considerations. This API cannot be used for self-registration. Passwords are automatically generated and all users must claim their accounts. No profile data can be added with this API.
862+
operationId: importAccounts
863+
parameters:
864+
- name: group
865+
in: path
866+
description: the auth group ID associated to your business account
867+
schema:
868+
type: string
869+
required: true
870+
responses:
871+
'201':
872+
description: successful operation
873+
content:
874+
application/json:
875+
schema:
876+
properties:
877+
type:
878+
type: string
879+
example: 'Accounts'
880+
data:
881+
type: object
882+
properties:
883+
attempted:
884+
type: number
885+
description: how many attempted
886+
ok:
887+
type: number
888+
description: how many successfully written?
889+
failed:
890+
type: array
891+
items:
892+
type: object
893+
description: which accounts failed and why
894+
success:
895+
type: array
896+
items:
897+
$ref: '#/components/schemas/getAccount'
898+
'400':
899+
$ref: '#/components/responses/BadRequest'
900+
'401':
901+
$ref: '#/components/responses/Unauthorized'
902+
'404':
903+
$ref: '#/components/responses/NotFound'
904+
'405':
905+
$ref: '#/components/responses/InvalidInput'
906+
'417':
907+
$ref: '#/components/responses/ExpectationFailed'
908+
security:
909+
- bearer: [ ]
910+
- openId: [ ]
911+
requestBody:
912+
content:
913+
application/json:
914+
schema:
915+
type: array
916+
items:
917+
$ref: '#/components/schemas/importAccount'
918+
description: Account data to be written
919+
required: true
857920
get:
858921
tags:
859922
- Users
@@ -7513,6 +7576,30 @@ components:
75137576
type: string
75147577
format: uri
75157578

7579+
importAccount:
7580+
type: object
7581+
additionalProperties: false
7582+
required:
7583+
- email
7584+
properties:
7585+
id:
7586+
type: string
7587+
format: uuid
7588+
description: if provided, the system will attempt to use this ID as long as it is unique.
7589+
username:
7590+
type: string
7591+
description: optional identifier, must be unique in the authGroup. If not provided, set to email.
7592+
email:
7593+
type: string
7594+
description: email address
7595+
phone:
7596+
type: object
7597+
properties:
7598+
txt:
7599+
type: string
7600+
pattern: '\+(9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)\d{1,14}$'
7601+
description: used for notifications. only visible to the user in question.
7602+
75167603
writeAccount:
75177604
type: object
75187605
additionalProperties: false

0 commit comments

Comments
 (0)