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,224 @@
1
+ import { debounce } from "@nesvet/n";
2
+ import { Role } from "./Role";
3
+ import { basisSchema } from "./schema";
4
+
5
+
6
+ const indexes = [
7
+ [ { involves: 1 } ]
8
+ ];
9
+
10
+ function preventDirectDelete() {
11
+ throw new Error("Direct usage of roles collection.deleteOne or .deleteMany is forbidden. Use roles.collectionDelete instead.");
12
+ }
13
+
14
+ function sortRoles(a, b) {
15
+ return (
16
+ (b.involves.size - a.involves.size) || (
17
+ (a.title && b.title) ?
18
+ a.title > b.title ? 1 : a.title < b.title ? -1 : 0 :
19
+ a.title ?
20
+ -1 :
21
+ b.title ?
22
+ 1 :
23
+ a._id > b._id ?
24
+ 1 :
25
+ a._id < b._id ?
26
+ -1 :
27
+ 0
28
+
29
+ )
30
+ );
31
+ }
32
+
33
+
34
+ export class Roles extends Map {
35
+ constructor(users, options = {}) {
36
+ super();
37
+
38
+ const {
39
+ schema: customSchema,
40
+ indexes: customIndexes,
41
+ root: rootProps
42
+ } = options;
43
+
44
+ this.users = users;
45
+ this.collections = users.collections;
46
+ this.abilities = users.abilities;
47
+
48
+ this.root = new Role(this, {
49
+ ...rootProps,
50
+ _id: "root",
51
+ involves: [],
52
+ abilities: this.abilities.getMaximum(),
53
+ title: "Root",
54
+ description: "Root",
55
+ createdAt: Date.now()
56
+ });
57
+
58
+ if (customSchema) {
59
+ customSchema.required?.removeAll(basisSchema.required);
60
+ if (customSchema.properties)
61
+ Object.delete(customSchema, ...Object.keys(basisSchema.properties));
62
+ }
63
+
64
+ const jsonSchema = {
65
+ ...basisSchema,
66
+ ...customSchema,
67
+ required: [ ...basisSchema.required, ...customSchema?.required ?? [] ],
68
+ properties: { ...basisSchema.properties, ...customSchema?.properties }
69
+ };
70
+
71
+ return (async () => {
72
+
73
+ this.collection = await this.collections.ensure("roles", { jsonSchema });
74
+
75
+ this.collection.ensureIndexes([ ...indexes, ...customIndexes ?? [] ]);
76
+
77
+ this.collection.deleteOne =
78
+ this.collection.deleteMany =
79
+ preventDirectDelete;
80
+
81
+ return this;
82
+ })();
83
+ }
84
+
85
+ load(roleDoc) {
86
+ new Role(this, roleDoc);
87
+
88
+ }
89
+
90
+ resolve(role, involvedBy) {
91
+ involvedBy = [ role, ...involvedBy ];
92
+
93
+ for (const involvedRoleId of role.ownInvolveIds) {
94
+ const involvedRole = this.get(involvedRoleId);
95
+ if (involvedRole) {
96
+ for (const involvedByRole of involvedBy) {
97
+ involvedByRole.involves.reAdd(involvedRole);
98
+ this.abilities.merge(involvedByRole.inheritedAbilities, involvedRole.abilities);
99
+ }
100
+ if (involvedRole !== role)
101
+ this.resolve(involvedRole, involvedBy);
102
+ }
103
+ }
104
+
105
+ }
106
+
107
+ sorted = [];
108
+
109
+ async update() {
110
+
111
+ const array = [ ...this.values() ];
112
+
113
+ this.root.ownInvolveIds = [];
114
+ for (const role of array) {
115
+ if (role !== this.root && !array.some(anotherRole => anotherRole.ownInvolveIds.includes(role._id)))
116
+ this.root.ownInvolveIds.push(role._id);
117
+ role.involves.clear();
118
+ role.abilities = Object.deepClone(role.ownAbilities);
119
+ role.inheritedAbilities = {};
120
+ }
121
+
122
+ this.root.involves.add(this.root);
123
+ this.resolve(this.root, []);
124
+
125
+ for (const role of array)
126
+ role.ownInvolves = this.getAll(role.ownInvolveIds).sort(sortRoles).reverse();
127
+
128
+ const sorted = [ this.root ];
129
+
130
+ for (let i = 0; i < sorted.length; i++) {
131
+ const role = sorted[i];
132
+ for (const involvedRole of role.ownInvolves) {
133
+ const involvedRoleIndex = sorted.indexOf(involvedRole);
134
+ if (~involvedRoleIndex) {
135
+ sorted.splice(involvedRoleIndex, 1);
136
+ i--;
137
+ }
138
+ sorted.splice(i + 1, 0, involvedRole);
139
+ }
140
+ }
141
+
142
+ let _o = 0;
143
+ for (const role of sorted) {
144
+ this.abilities.merge(role.abilities, role.inheritedAbilities);
145
+ role._o = _o++;
146
+ }
147
+
148
+ this.sorted = sorted;
149
+
150
+ this.users.emit("roles-update");
151
+
152
+ if (this.users.isInited) {
153
+ this.users.updateDebounced.clear();
154
+ this.users.update(true);
155
+ }
156
+
157
+ }
158
+
159
+ updateDebounced = debounce(this.update, 250);
160
+
161
+ init = async () => {
162
+
163
+ for (const roleDoc of await this.collection.find().toArray())
164
+ this.load(roleDoc);
165
+
166
+ await this.update();
167
+
168
+ this.collection.changeListeners.add(next => {
169
+ switch (next.operationType) {
170
+ case "insert":
171
+ const role = new Role(this, next.fullDocument);
172
+ this.users.emit("roles-role-update", role, next);
173
+ break;
174
+
175
+ case "update":
176
+ case "replace":
177
+ this.get(next.documentKey._id).update(next.updateDescription?.updatedFields || next.fullDocument, next);
178
+ break;
179
+
180
+ case "delete":
181
+ this.get(next.documentKey._id).delete();
182
+ this.users.emit("roles-role-update", null, next);
183
+ }
184
+
185
+ });
186
+
187
+ delete this.init;
188
+
189
+ };
190
+
191
+ new({ _id, involves, title, description, ...restProps }) {
192
+ return this.collection.insertOne({
193
+ _id,
194
+ involves: involves || [],
195
+ abilities: {},
196
+ title: title ?? "",
197
+ description: description ?? "",
198
+ ...Object.delete(restProps, "abilities"),
199
+ createdAt: Date.now()
200
+ });
201
+ }
202
+
203
+ collectionDelete(role) {
204
+ const _id = typeof role == "string" ? role : role._id;
205
+
206
+ return this.collection.bulkWrite([
207
+ { updateMany: { filter: { involves: _id }, update: { $pullAll: { involves: [ _id ] } } } },
208
+ { deleteOne: { filter: { _id } } }
209
+ ]);
210
+ }
211
+
212
+ cleanUpIds(ids) {
213
+ const roles = this.sorted.intersection(this.getAll(ids));
214
+
215
+ const cleanedUpIds = [];
216
+
217
+ for (const role of roles)
218
+ if (roles.every(anotherRole => anotherRole === role || !anotherRole.involves.has(role)))
219
+ cleanedUpIds.push(role._id);
220
+
221
+ return cleanedUpIds;
222
+ }
223
+
224
+ }
@@ -0,0 +1 @@
1
+ export * from "./Roles";
@@ -0,0 +1,45 @@
1
+ import { regexps } from "../lib";
2
+
3
+
4
+ export const basisSchema = {
5
+ required: [
6
+ "_id",
7
+ "involves",
8
+ "abilities",
9
+ "title",
10
+ "description",
11
+ "createdAt"
12
+ ],
13
+ properties: {
14
+ _id: {
15
+ bsonType: "string",
16
+ pattern: regexps.role.pattern,
17
+ not: { enum: [ "root" ] }
18
+ },
19
+ involves: {
20
+ bsonType: "array",
21
+ uniqueItems: true,
22
+ items: {
23
+ bsonType: "string",
24
+ pattern: regexps.role.pattern,
25
+ not: { enum: [ "root" ] }
26
+ },
27
+ additionalItems: false
28
+ },
29
+ abilities: {
30
+ bsonType: "object"
31
+ },
32
+ title: {
33
+ bsonType: "string",
34
+ maxLength: 256
35
+ },
36
+ description: {
37
+ bsonType: "string",
38
+ maxLength: 10240
39
+ },
40
+ createdAt: {
41
+ bsonType: "number"
42
+ }
43
+ },
44
+ additionalProperties: false
45
+ };
@@ -0,0 +1,72 @@
1
+ export class Session {
2
+ constructor(sessions, sessionDoc) {
3
+ this.#sessions = sessions;
4
+
5
+ this._id = sessionDoc._id;
6
+ sessions.set(this._id, this);
7
+
8
+ this.user = sessions.users.get(sessionDoc.user);
9
+ this.user.sessions.add(this);
10
+
11
+ sessions.users.bySessionId.set(this._id, this.user);
12
+
13
+ this.update(sessionDoc);
14
+
15
+ }
16
+
17
+ #sessions;
18
+
19
+ update({ _id, user, isOnline, ...restProps }) {
20
+
21
+ const sessions = this.#sessions;
22
+
23
+ if (isOnline !== undefined && this.isOnline != isOnline) {
24
+ this.isOnline = isOnline;
25
+
26
+ if (sessions.isInited && this.user.isOnline != this.isOnline)
27
+ this.user.updateIsOnline();
28
+ }
29
+
30
+ Object.assign(this, restProps);
31
+
32
+ }
33
+
34
+ prolong({ _id, user, ...updates }) {
35
+
36
+ const sessions = this.#sessions;
37
+
38
+ const ts = Date.now();
39
+
40
+ updates.prolongedAt = ts;
41
+ updates.expiresAt = new Date(ts + (sessions.collection.expireAfterSeconds * 1000));
42
+
43
+ this.update(updates);
44
+
45
+ return sessions.collection.updateOne({ _id: this._id }, { $set: updates });
46
+ }
47
+
48
+ offline() {
49
+
50
+ const updates = { isOnline: false };
51
+
52
+ this.update(updates);
53
+
54
+ return this.#sessions.collection.updateOne({ _id: this._id }, { $set: updates });
55
+ }
56
+
57
+ delete() {
58
+
59
+ const sessions = this.#sessions;
60
+
61
+ sessions.delete(this._id);
62
+ this.user.sessions.delete(this);
63
+ sessions.users.bySessionId.delete(this._id);
64
+
65
+ sessions.users.emit("session-delete", this);
66
+
67
+ if (this.isOnline)
68
+ this.user.updateIsOnline();
69
+
70
+ }
71
+
72
+ }
@@ -0,0 +1,118 @@
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
+ }
@@ -0,0 +1 @@
1
+ export * from "./Sessions";
@@ -0,0 +1,45 @@
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
+ };