insite-users-server 1.0.334

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.
@@ -0,0 +1,251 @@
1
+ import { debounce } from "@nesvet/n";
2
+
3
+
4
+ const rolesSnapshots = new Map();
5
+ const permissionsSnapshots = new Map();
6
+
7
+ const staticHost = process.env.STATIC_HOST ?? "localhost";
8
+
9
+
10
+ export class User {
11
+ constructor(users, userDoc) {
12
+ this.#users = users;
13
+
14
+ this._id = userDoc._id;
15
+ users.set(this._id, this);
16
+
17
+ this.update(userDoc);
18
+
19
+ if (users.isInited)
20
+ users.emit("user-create", this);
21
+
22
+ }
23
+
24
+ #users;
25
+
26
+ isUser = true;
27
+
28
+ roles = new Set();
29
+ slaveRoles = new Set();
30
+ slaveRoleIds = [];
31
+ abilities = {};
32
+
33
+ slaveOrgs = new Set();
34
+ slaveUsers = new Set();
35
+ slaves = new Set();
36
+
37
+ slaveIds = [];
38
+ permissiveIds = [];
39
+
40
+ sessions = new Set();
41
+
42
+ isOnline = false;
43
+
44
+ update({ _id, name, email, roles: ownRoleIds, org: orgId, avatar, ...restProps }) {
45
+
46
+ const users = this.#users;
47
+
48
+ Object.assign(this, restProps);
49
+
50
+ let isEmailUpdated;
51
+ if (email && this.email !== email) {
52
+ users.byEmail.delete(this.email);
53
+ this.email = email;
54
+ users.byEmail.set(email, this);
55
+ isEmailUpdated = true;
56
+ }
57
+
58
+ let isNameUpdated;
59
+ let isLastNameUpdated;
60
+ if (name && (this.name?.first !== name.first || this.name.middle !== name.middle || this.name.last !== name.last)) {
61
+ isLastNameUpdated = this.name?.last !== name.last;
62
+ this.name = {
63
+ first: name.first,
64
+ middle: name.middle,
65
+ last: name.last
66
+ };
67
+ this.initials = (((name.first[0] || "") + (name.last[0] || "")) || this.email[0]).toUpperCase();
68
+ isNameUpdated = true;
69
+ }
70
+
71
+ if (isEmailUpdated || isNameUpdated)
72
+ this.displayLabel = `${name.first} ${name.last}`.trim() || name.middle || this.email;
73
+
74
+ let isRolesUpdated;
75
+ if (ownRoleIds) {
76
+ this.ownRoleIds = ownRoleIds;
77
+
78
+ const snapshot = this.ownRoleIds.join(",");
79
+
80
+ if (snapshot !== rolesSnapshots.get(this._id)) {
81
+ rolesSnapshots.set(this._id, snapshot);
82
+ this.updateRoles();
83
+ isRolesUpdated = true;
84
+ }
85
+ }
86
+
87
+ let isOrgUpdated;
88
+ if (orgId !== undefined && this.org?._id !== orgId) {
89
+ this.org?.users.delete(this);
90
+ this.org = users.orgs.get(orgId) || users.orgs.null;
91
+ this.org.users.add(this);
92
+ isOrgUpdated = true;
93
+ }
94
+
95
+ if (avatar !== undefined && this.avatar !== avatar) {
96
+ this.avatar = avatar;
97
+ this.avatarUrl = avatar && `https://${staticHost}/avatars/${this._id}-${avatar}.webp`;
98
+ }
99
+
100
+ if (isRolesUpdated || isOrgUpdated) {
101
+ this.updatePermissions();
102
+
103
+ if (isLastNameUpdated)
104
+ users.isSortRequired = true;
105
+
106
+ if (users.isInited)
107
+ users.updateDebounced();
108
+
109
+ } else if (isLastNameUpdated)
110
+ users.sortDebounced();
111
+
112
+ }
113
+
114
+ updateRoles() {
115
+
116
+ const users = this.#users;
117
+
118
+ if (users.isInited) {
119
+ this.ownRoles = [];
120
+ this.roles.clear();
121
+ this.slaveRoles.clear();
122
+ Object.clear(this.abilities);
123
+
124
+ for (const roleId of this.ownRoleIds) {
125
+ const role = users.roles.get(roleId);
126
+
127
+ this.ownRoles.push(role);
128
+ this.roles.add(role);
129
+ for (const involvedRole of role.involves) {
130
+ this.roles.add(involvedRole);
131
+ this.slaveRoles.add(involvedRole);
132
+ }
133
+
134
+ this.slaveRoleIds = this.slaveRoles.ids();
135
+
136
+ users.abilities.merge(this.abilities, role.abilities);
137
+ }
138
+
139
+ if (this.roles.has(users.roles.root))
140
+ this.isRoot = true;
141
+
142
+ this.trimSessions();
143
+ }
144
+
145
+ }
146
+
147
+ updatePermissions() {
148
+
149
+ const users = this.#users;
150
+
151
+ if (users.isInited) {
152
+ this.slaveOrgs.clear();
153
+ this.slaveUsers.clear();
154
+ this.slaves.clear();
155
+ const slaveIds = new Set();
156
+
157
+ for (const org of users.orgs.sorted)
158
+ if ((org.ownerUsers.has(this) || this.isRoot) && !this.slaveOrgs.has(org)) {
159
+ this.slaveOrgs.add(org);
160
+ this.slaves.add(org);
161
+ slaveIds.add(org._id);
162
+ for (const slaveOrg of org.slaveOrgs) {
163
+ this.slaveOrgs.add(slaveOrg);
164
+ this.slaves.add(slaveOrg);
165
+ slaveIds.add(slaveOrg._id);
166
+ }
167
+ }
168
+
169
+ for (const org of new Set([ this.org, ...this.slaveOrgs, users.orgs.null ]))
170
+ for (const user of org.users)
171
+ if (user !== this && (this.slaveRoles.hasAny(user.ownRoles) || !user.ownRoles.length)) {
172
+ this.slaveUsers.add(user);
173
+ this.slaves.add(user);
174
+ slaveIds.add(user._id);
175
+ }
176
+
177
+ this.slaveIds = [ ...slaveIds ];
178
+
179
+ const permissiveOrgIds = [];
180
+ if (this.org._id) {
181
+ permissiveOrgIds.push(this.org._id);
182
+ for (const slaveOrg of this.org.slaveOrgs)
183
+ if (!this.slaveOrgs.has(slaveOrg))
184
+ permissiveOrgIds.push(slaveOrg._id);
185
+ }
186
+
187
+ this.permissiveIds = [ ...permissiveOrgIds, this._id, ...slaveIds ];
188
+
189
+
190
+ const snapshot = [
191
+ this.permissiveIds.join(","),
192
+ this.ownRoleIds.join(","),
193
+ JSON.stringify(this.abilities),
194
+ this.slaveRoleIds.join(",")
195
+ ].join("\n");
196
+
197
+ if (snapshot !== permissionsSnapshots.get(this._id)) {
198
+ permissionsSnapshots.set(this._id, snapshot);
199
+
200
+ users.emit("user-permissions-change", this);
201
+ }
202
+ }
203
+
204
+ }
205
+
206
+ trimSessions() {
207
+
208
+ const sessionsLimit = this.abilities.login?.sessionsLimit ?? 0;
209
+
210
+ if (this.sessions.size > sessionsLimit)
211
+ return this.#users.sessions.collection.deleteMany({ _id: { $in: [ ...this.sessions ].sort((a, b) => a.expiresAt - b.expiresAt).slice(0, this.sessions.size - sessionsLimit).ids() } });
212
+
213
+ }
214
+
215
+ updateIsOnline = debounce(() => {
216
+
217
+ let isOnline = false;
218
+
219
+ for (const session of this.sessions)
220
+ if (session.isOnline) {
221
+ isOnline = true;
222
+ break;
223
+ }
224
+
225
+ if (this.isOnline !== isOnline) {
226
+ this.isOnline = isOnline;
227
+ this.#users.emit("user-is-online", this);
228
+ }
229
+
230
+ }, 500);
231
+
232
+ delete() {
233
+
234
+ const users = this.#users;
235
+
236
+ this.org.users.delete(this);
237
+
238
+ users.delete(this._id);
239
+ users.byEmail.delete(this.email);
240
+ users.sorted.remove(this);
241
+
242
+ rolesSnapshots.delete(this._id);
243
+ permissionsSnapshots.delete(this._id);
244
+
245
+ users.replace(this._id);
246
+
247
+ users.updateDebounced();
248
+
249
+ }
250
+
251
+ }
@@ -0,0 +1,265 @@
1
+ import { ObjectId } from "insite-db";
2
+ import { debounce } from "@nesvet/n";
3
+ import argon2 from "argon2";
4
+ import EventEmitter from "eventemitter3";
5
+ import { Abilities } from "../abilities";
6
+ import { Orgs } from "../orgs";
7
+ import { Roles } from "../roles";
8
+ import { Sessions } from "../sessions";
9
+ import { Avatars } from "./avatars";
10
+ import { basisSchema } from "./schema";
11
+ import { User } from "./User";
12
+
13
+
14
+ const indexes = [
15
+ [ { email: 1 }, { unique: true } ],
16
+ [ { org: 1 } ]
17
+ ];
18
+
19
+ function sortUsersAndOrgs(a, b) {
20
+ return (
21
+ (a.isUser && b.isUser) ?
22
+ (a.name.last && b.name.last) ?
23
+ a.name.last > b.name.last ?
24
+ 1 :
25
+ a.name.last < b.name.last ? -1 : 0 :
26
+ (!a.name.last && !b.name.last) ?
27
+ a.displayLabel > b.displayLabel ?
28
+ 1 :
29
+ a.displayLabel < b.displayLabel ? -1 : 0 :
30
+ a.name.last ? -1 : 1 :
31
+ (a.isOrg && b.isOrg) ?
32
+ a.title > b.title ?
33
+ 1 :
34
+ a.title < b.title ? -1 : 0 :
35
+ a.isOrg ? -1 : 1
36
+ );
37
+ }
38
+
39
+
40
+ export class Users extends Map {
41
+ constructor({
42
+ collections,
43
+ indexes: customIndexes,
44
+ schema: customSchema,
45
+ initialRoot: initialRootProps,
46
+ abilities: abilitiesOptions,
47
+ roles: rolesOptions,
48
+ orgs: orgsOptions,
49
+ sessions: sessionsOptions,
50
+ avatars: avatarsOptions
51
+ }) {
52
+ super();
53
+
54
+ const eventEmitter = new EventEmitter();
55
+ for (const key in eventEmitter)
56
+ this[key] = eventEmitter[key];
57
+
58
+ this.collections = collections;
59
+
60
+ this.abilities = new Abilities(abilitiesOptions);
61
+
62
+ if (customSchema) {
63
+ customSchema.required?.removeAll(basisSchema.required);
64
+ if (customSchema.properties)
65
+ Object.delete(customSchema, ...Object.keys(basisSchema.properties));
66
+ }
67
+
68
+ const jsonSchema = {
69
+ ...basisSchema,
70
+ ...customSchema,
71
+ required: [ ...basisSchema.required, ...customSchema?.required ?? [] ],
72
+ properties: { ...basisSchema.properties, ...customSchema?.properties }
73
+ };
74
+
75
+ return (async () => {
76
+
77
+ this.collection = await this.collections.ensure("users", { jsonSchema });
78
+
79
+ this.collection.ensureIndexes([ ...indexes, ...customIndexes ?? [] ]);
80
+
81
+ if (!await this.collection.countDocuments({ roles: "root" }))
82
+ await this.new({
83
+ email: process.env.INSITE_ROOT_EMAIL ?? `root@${process.env.REMOTE_HOST || "insite.insite"}`,
84
+ password: process.env.INSITE_ROOT_PASSWORD ?? (process.env.MONGO_DBNAME || "insite"),
85
+ roles: [ "root" ],
86
+ name: { first: "Root" },
87
+ org: null,
88
+ job: "Root",
89
+ ...Object.delete(initialRootProps, "roles", "org")
90
+ });
91
+
92
+ this.roles = await new Roles(this, rolesOptions);
93
+ this.orgs = await new Orgs(this, orgsOptions);
94
+ this.sessions = await new Sessions(this, sessionsOptions);
95
+ this.avatars = await new Avatars(this, avatarsOptions);
96
+
97
+ await this.roles.init();
98
+
99
+ await this.orgs.initialLoad();
100
+ await this.initialLoad();
101
+
102
+ await this.orgs.init();
103
+ await this.init();
104
+
105
+ await this.sessions.init();
106
+
107
+ return this;
108
+ })();
109
+ }
110
+
111
+ isInited = false;
112
+
113
+ byEmail = new Map();
114
+ bySessionId = new Map();
115
+ sorted = [];
116
+
117
+ isSortRequired = false;
118
+
119
+ load(userDoc) {
120
+ new User(this, userDoc);
121
+
122
+ }
123
+
124
+ sort() {
125
+
126
+ this.isSortRequired = false;
127
+
128
+ this.sorted = [ ...this.values() ].sort((a, b) => a.name.last > b.name.last ? 1 : a.name.last < b.name.last ? -1 : 0);
129
+
130
+ }
131
+
132
+ sortDebounced = debounce(this.sort, 250);
133
+
134
+ update(shouldUpdateRoles) {
135
+ if (shouldUpdateRoles)
136
+ for (const user of this.values())
137
+ user.updateRoles();
138
+
139
+ for (const user of this.values())
140
+ user.updatePermissions();
141
+
142
+ if (this.isSortRequired)
143
+ this.sort();
144
+
145
+ }
146
+
147
+ updateDebounced = debounce(this.update, 250);
148
+
149
+ initialLoad = async () => {
150
+
151
+ for (const userDoc of await this.collection.find().toArray())
152
+ this.load(userDoc);
153
+
154
+ delete this.initialLoad;
155
+
156
+ };
157
+
158
+ init = async () => {
159
+
160
+ this.isInited = true;
161
+
162
+ this.update(true);
163
+
164
+ this.collection.changeListeners.add(next => {
165
+ switch (next.operationType) {
166
+ case "insert":
167
+ new User(this, next.fullDocument);
168
+ break;
169
+
170
+ case "replace":
171
+ this.get(next.documentKey._id).update(next.fullDocument);
172
+ break;
173
+
174
+ case "update":
175
+ this.get(next.documentKey._id).update(next.updateDescription.updatedFields);
176
+ break;
177
+
178
+ case "delete":
179
+ this.get(next.documentKey._id).delete();
180
+ }
181
+
182
+ });
183
+
184
+ delete this.init;
185
+
186
+ };
187
+
188
+ async new({ email, password, roles, name, org, job, ...restProps }) {
189
+ if (this.byEmail.has(email))
190
+ throw new Error("User exists");
191
+ else
192
+ await this.collection.insertOne({
193
+ _id: (new ObjectId()).toString(),
194
+ email,
195
+ password: await argon2.hash(password),
196
+ roles: this.isInited ? this.roles.cleanUpIds(roles) : roles,
197
+ name: {
198
+ first: name.first ?? "",
199
+ middle: name.middle ?? "",
200
+ last: name.last ?? ""
201
+ },
202
+ org: org ?? null,
203
+ job: job ?? "",
204
+ ...Object.delete(restProps, "_id"),
205
+ createdAt: Date.now()
206
+ });
207
+
208
+ return true;
209
+ }
210
+
211
+ async changePassword(_id, newPassword) {
212
+ if (!this.has(_id))
213
+ throw new Error("usernotfound");
214
+ else if (!newPassword)
215
+ throw new Error("emptypasswordisnotallowed");
216
+ else
217
+ await Promise.all([
218
+ this.collection.updateOne({ _id }, { $set: { password: await argon2.hash(newPassword) } }),
219
+ this.sessions.collection.deleteMany({ user: _id })
220
+ ]);
221
+
222
+ }
223
+
224
+ async login(email, password, sessionProps) {
225
+ const user = this.byEmail.get(email);
226
+
227
+ if (!user)
228
+ throw new Error("usernotfound");
229
+ else if (!await argon2.verify(user.password, password))
230
+ throw new Error("incorrectpassword");
231
+ else if (!user.abilities.login)
232
+ throw new Error("userisnotabletologin");
233
+
234
+ return this.sessions.new(user, sessionProps);
235
+ }
236
+
237
+ logout(session) {
238
+ session.delete();
239
+
240
+ return this.sessions.collection.deleteOne({ _id: session._id });
241
+ }
242
+
243
+ sortIds(ids) {
244
+ return ids
245
+ .map(_id => this.get(_id) || this.orgs.get(_id))
246
+ .filter(Boolean)
247
+ .sort(sortUsersAndOrgs)
248
+ .ids();
249
+ }
250
+
251
+ replaceMap = new Map();
252
+
253
+ replaceHandlers = new Set();
254
+
255
+ async replace(fromId) {
256
+ const toId = this.replaceMap.get(fromId) ?? null;
257
+ if (toId)
258
+ this.replaceMap.delete(fromId);
259
+
260
+ for (const handler of this.replaceHandlers)
261
+ await handler(fromId, toId);
262
+
263
+ }
264
+
265
+ }
@@ -0,0 +1,16 @@
1
+ import { schema as jsonSchema } from "./schema";
2
+
3
+
4
+ export class Avatars {
5
+ constructor(users, options = {}) {
6
+ this.users = users;
7
+ this.collections = users.collections;
8
+
9
+ return (async () => {
10
+
11
+ this.collection = await this.collections.ensure("users.avatars", { jsonSchema });
12
+
13
+ return this;
14
+ })();
15
+ }
16
+ }
@@ -0,0 +1 @@
1
+ export { Avatars } from "./Avatars";
@@ -0,0 +1,33 @@
1
+ import { regexps } from "../../lib";
2
+
3
+
4
+ export const schema = {
5
+ required: [
6
+ "_id",
7
+ "type",
8
+ "size",
9
+ "ts",
10
+ "data"
11
+ ],
12
+ properties: {
13
+ _id: {
14
+ bsonType: "string",
15
+ pattern: regexps._id.pattern
16
+ },
17
+ type: {
18
+ bsonType: "string",
19
+ maxLength: 256
20
+ },
21
+ size: {
22
+ bsonType: "number"
23
+ },
24
+ ts: {
25
+ bsonType: "string",
26
+ maxLength: 10
27
+ },
28
+ data: {
29
+ bsonType: "binData"
30
+ }
31
+ },
32
+ additionalProperties: false
33
+ };
@@ -0,0 +1 @@
1
+ export * from "./Users";
@@ -0,0 +1,91 @@
1
+ import { regexps } from "../lib";
2
+
3
+
4
+ export const basisSchema = {
5
+ required: [
6
+ "_id",
7
+ "email",
8
+ "password",
9
+ "roles",
10
+ "name",
11
+ "org",
12
+ "job",
13
+ "createdAt"
14
+ ],
15
+ properties: {
16
+ _id: {
17
+ bsonType: "string",
18
+ pattern: regexps._id.pattern
19
+ },
20
+ email: {
21
+ bsonType: "string",
22
+ pattern: regexps.email.pattern
23
+ },
24
+ password: {
25
+ bsonType: "string",
26
+ pattern: regexps.argon2.pattern
27
+ },
28
+ roles: {
29
+ bsonType: "array",
30
+ uniqueItems: true,
31
+ items: {
32
+ bsonType: "string",
33
+ pattern: regexps.role.pattern
34
+ },
35
+ additionalItems: false
36
+ },
37
+ name: {
38
+ bsonType: "object",
39
+ required: [
40
+ "first",
41
+ "middle",
42
+ "last"
43
+ ],
44
+ properties: {
45
+ first: {
46
+ bsonType: "string",
47
+ maxLength: 512
48
+ },
49
+ middle: {
50
+ bsonType: "string",
51
+ maxLength: 512
52
+ },
53
+ last: {
54
+ bsonType: "string",
55
+ maxLength: 512
56
+ }
57
+ },
58
+ additionalProperties: false
59
+ },
60
+ org: {
61
+ oneOf: [
62
+ {
63
+ bsonType: "string",
64
+ pattern: regexps._id.pattern
65
+ },
66
+ {
67
+ bsonType: "null"
68
+ }
69
+ ]
70
+ },
71
+ job: {
72
+ bsonType: "string",
73
+ maxLength: 512
74
+ },
75
+ avatar: {
76
+ oneOf: [
77
+ {
78
+ bsonType: "string",
79
+ maxLength: 10
80
+ },
81
+ {
82
+ bsonType: "null"
83
+ }
84
+ ]
85
+ },
86
+ createdAt: {
87
+ bsonType: "number"
88
+ }
89
+ },
90
+ additionalProperties: false
91
+ };