insite-users-server 1.0.334 → 1.0.336

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.
@@ -1,118 +0,0 @@
1
- import crypto from "node:crypto";
2
- import { random } from "@nesvet/n";
3
- import { basisSchema } from "./schema";
4
- import { Session } from "./Session";
5
-
6
-
7
- const expireAfterSeconds = 3600 * 24 * 7;
8
-
9
- const indexes = [
10
- [ { user: 1 } ],
11
- [ { expiresAt: 1 }, { expireAfterSeconds } ]
12
- ];
13
-
14
-
15
- export class Sessions extends Map {
16
- constructor(users, options = {}) {
17
- super();
18
-
19
- const {
20
- schema: customSchema,
21
- indexes: customIndexes
22
- } = options;
23
-
24
- this.users = users;
25
- this.collections = users.collections;
26
-
27
- if (customSchema) {
28
- customSchema.required?.removeAll(basisSchema.required);
29
- if (customSchema.properties)
30
- Object.delete(customSchema, ...Object.keys(basisSchema.properties));
31
- }
32
-
33
- const jsonSchema = {
34
- ...basisSchema,
35
- ...customSchema,
36
- required: [ ...basisSchema.required, ...customSchema?.required ?? [] ],
37
- properties: { ...basisSchema.properties, ...customSchema?.properties }
38
- };
39
-
40
- return (async () => {
41
-
42
- this.collection = await this.collections.ensure("sessions", { jsonSchema });
43
-
44
- this.collection.expireAfterSeconds = expireAfterSeconds;
45
-
46
- this.collection.ensureIndexes([ ...indexes, ...customIndexes ?? [] ]);
47
-
48
- await this.collection.updateMany({ isOnline: true }, { $set: { isOnline: false } });
49
-
50
- return this;
51
- })();
52
- }
53
-
54
- isInited = false;
55
-
56
- makeId() {
57
- return `${crypto.randomBytes(random(8, 16)).toString("hex")}$${crypto.randomBytes(random(8, 16)).toString("hex")}`;
58
- }
59
-
60
- load(sessionDoc) {
61
- new Session(this, sessionDoc);
62
-
63
- }
64
-
65
- init = async () => {
66
-
67
- for (const sessionDoc of await this.collection.find().toArray())
68
- this.load(sessionDoc);
69
-
70
- this.collection.changeListeners.add(next => {
71
- switch (next.operationType) {
72
- case "insert":
73
- if (!this.has(next.documentKey._id))
74
- new Session(this, next.fullDocument);
75
-
76
- this.users.get(next.fullDocument.user)?.trimSessions();
77
-
78
- break;
79
-
80
- case "update":
81
- case "replace":
82
- this.get(next.documentKey._id)?.update(next.updateDescription?.updatedFields || next.fullDocument);
83
- break;
84
-
85
- case "delete":
86
- this.get(next.documentKey._id)?.delete();
87
- }
88
-
89
- });
90
-
91
- this.isInited = true;
92
-
93
- delete this.init;
94
-
95
- };
96
-
97
- async new(user, props) {
98
-
99
- const ts = Date.now();
100
-
101
- const sessionDoc = {
102
- ...props,
103
- _id: this.uid(),
104
- user: user._id,
105
- createdAt: ts,
106
- prolongedAt: ts,
107
- expiresAt: new Date(ts + (expireAfterSeconds * 1000))
108
- };
109
-
110
- /* new Session(…) before await insertOne(…) for avoid changeStream insert event to come earlier and create the session duplicate */
111
- const session = new Session(this, sessionDoc);
112
-
113
- await this.collection.insertOne(sessionDoc);
114
-
115
- return session;
116
- }
117
-
118
- }
@@ -1 +0,0 @@
1
- export * from "./Sessions";
@@ -1,45 +0,0 @@
1
- import { regexps } from "../lib";
2
-
3
-
4
- export const basisSchema = {
5
- required: [
6
- "_id",
7
- "user",
8
- "remoteAddress",
9
- "isOnline",
10
- "createdAt",
11
- "prolongedAt",
12
- "expiresAt"
13
- ],
14
- properties: {
15
- _id: {
16
- bsonType: "string",
17
- pattern: regexps.sessionId.pattern
18
- },
19
- user: {
20
- bsonType: "string",
21
- pattern: regexps._id.pattern
22
- },
23
- userAgent: {
24
- bsonType: "string",
25
- maxLength: 4096
26
- },
27
- remoteAddress: {
28
- bsonType: "string",
29
- pattern: regexps.ip.pattern
30
- },
31
- isOnline: {
32
- bsonType: "bool"
33
- },
34
- createdAt: {
35
- bsonType: "number"
36
- },
37
- prolongedAt: {
38
- bsonType: "number"
39
- },
40
- expiresAt: {
41
- bsonType: "date"
42
- }
43
- },
44
- additionalProperties: false
45
- };
package/lib/users/User.js DELETED
@@ -1,251 +0,0 @@
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
- }
@@ -1,265 +0,0 @@
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
- }
@@ -1,16 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- export { Avatars } from "./Avatars";