@devxcrew/platform 0.1.2
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/README.md +82 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -0
- package/dist/modules/identity/identity.administration-migration.d.ts +2 -0
- package/dist/modules/identity/identity.administration-migration.js +75 -0
- package/dist/modules/identity/identity.administration-repository.d.ts +19 -0
- package/dist/modules/identity/identity.administration-repository.js +102 -0
- package/dist/modules/identity/identity.administration-schema.d.ts +113 -0
- package/dist/modules/identity/identity.administration-schema.js +121 -0
- package/dist/modules/identity/identity.administration-service.d.ts +97 -0
- package/dist/modules/identity/identity.administration-service.js +517 -0
- package/dist/modules/identity/identity.controller.d.ts +22 -0
- package/dist/modules/identity/identity.controller.js +278 -0
- package/dist/modules/identity/identity.error.d.ts +6 -0
- package/dist/modules/identity/identity.error.js +15 -0
- package/dist/modules/identity/identity.lifecycle-schema.d.ts +19 -0
- package/dist/modules/identity/identity.lifecycle-schema.js +29 -0
- package/dist/modules/identity/identity.lifecycle-service.d.ts +64 -0
- package/dist/modules/identity/identity.lifecycle-service.js +395 -0
- package/dist/modules/identity/identity.membership-policy.d.ts +3 -0
- package/dist/modules/identity/identity.membership-policy.js +27 -0
- package/dist/modules/identity/identity.migration.d.ts +2 -0
- package/dist/modules/identity/identity.migration.js +79 -0
- package/dist/modules/identity/identity.password.d.ts +3 -0
- package/dist/modules/identity/identity.password.js +21 -0
- package/dist/modules/identity/identity.permission-declarations-migration.d.ts +5 -0
- package/dist/modules/identity/identity.permission-declarations-migration.js +15 -0
- package/dist/modules/identity/identity.permission-declarations.d.ts +23 -0
- package/dist/modules/identity/identity.permission-declarations.js +57 -0
- package/dist/modules/identity/identity.permission-labels-migration.d.ts +5 -0
- package/dist/modules/identity/identity.permission-labels-migration.js +9 -0
- package/dist/modules/identity/identity.presentation-service.d.ts +23 -0
- package/dist/modules/identity/identity.presentation-service.js +51 -0
- package/dist/modules/identity/identity.provider.d.ts +78 -0
- package/dist/modules/identity/identity.provider.js +119 -0
- package/dist/modules/identity/identity.public-schemas.d.ts +4 -0
- package/dist/modules/identity/identity.public-schemas.js +3 -0
- package/dist/modules/identity/identity.repository.d.ts +40 -0
- package/dist/modules/identity/identity.repository.js +196 -0
- package/dist/modules/identity/identity.request-context.d.ts +2 -0
- package/dist/modules/identity/identity.request-context.js +10 -0
- package/dist/modules/identity/identity.roles-migration.d.ts +2 -0
- package/dist/modules/identity/identity.roles-migration.js +64 -0
- package/dist/modules/identity/identity.roles-service.d.ts +49 -0
- package/dist/modules/identity/identity.roles-service.js +327 -0
- package/dist/modules/identity/identity.routes.d.ts +22 -0
- package/dist/modules/identity/identity.routes.js +11 -0
- package/dist/modules/identity/identity.schema.d.ts +27 -0
- package/dist/modules/identity/identity.schema.js +34 -0
- package/dist/modules/identity/identity.seed.d.ts +3 -0
- package/dist/modules/identity/identity.seed.js +81 -0
- package/dist/modules/identity/identity.service.d.ts +32 -0
- package/dist/modules/identity/identity.service.js +83 -0
- package/dist/modules/identity/identity.types.d.ts +150 -0
- package/dist/modules/identity/identity.types.js +1 -0
- package/dist/modules/identity/index.d.ts +12 -0
- package/dist/modules/identity/index.js +9 -0
- package/package.json +62 -0
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import { validateDeclaredPermissions } from "./identity.permission-declarations.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { IdentityAdministrationRepository } from "./identity.administration-repository.js";
|
|
4
|
+
import { IdentityRolesService } from "./identity.roles-service.js";
|
|
5
|
+
import { protectAdministrators } from "./identity.membership-policy.js";
|
|
6
|
+
import { IdentityPresentationService } from "./identity.presentation-service.js";
|
|
7
|
+
import { checkIdentityRequest } from "./identity.request-context.js";
|
|
8
|
+
import { sql } from "kysely";
|
|
9
|
+
import { hashPassword } from "./identity.password.js";
|
|
10
|
+
import { IdentityError } from "./identity.error.js";
|
|
11
|
+
import { userCreateSchema, userUpdateSchema, organizationCreateSchema, organizationUpdateSchema, membershipSchema, roleUpdateSchema, profileSchema, settingsSchema, securitySettingsSchema, } from "./identity.administration-schema.js";
|
|
12
|
+
export class IdentityAdministrationService {
|
|
13
|
+
db;
|
|
14
|
+
appName;
|
|
15
|
+
repository;
|
|
16
|
+
roles;
|
|
17
|
+
presentationService;
|
|
18
|
+
constructor(db, appName) {
|
|
19
|
+
this.db = db;
|
|
20
|
+
this.appName = appName;
|
|
21
|
+
this.repository = new IdentityAdministrationRepository(db);
|
|
22
|
+
this.roles = new IdentityRolesService(db);
|
|
23
|
+
this.presentationService = new IdentityPresentationService(db, appName);
|
|
24
|
+
}
|
|
25
|
+
async list(actor, resource, query) {
|
|
26
|
+
this.allow(actor, resource, false);
|
|
27
|
+
return this.repository.list(actor, resource, query);
|
|
28
|
+
}
|
|
29
|
+
async show(actor, resource, id) {
|
|
30
|
+
this.allow(actor, resource, false);
|
|
31
|
+
const row = await this.repository.show(actor, resource, id);
|
|
32
|
+
if (!row)
|
|
33
|
+
throw new IdentityError(404, "Resource not found.");
|
|
34
|
+
return row;
|
|
35
|
+
}
|
|
36
|
+
async create(actor, resource, raw) {
|
|
37
|
+
if (resource === "roles")
|
|
38
|
+
return this.roles.create(actor, raw);
|
|
39
|
+
this.allow(actor, resource, true);
|
|
40
|
+
if (resource === "users") {
|
|
41
|
+
const input = userCreateSchema.parse(raw);
|
|
42
|
+
const tenantId = this.tenant(actor, input.tenantId);
|
|
43
|
+
await this.roles.resolve(this.db, actor, input.roleId, tenantId);
|
|
44
|
+
const passwordHash = await hashPassword(input.password);
|
|
45
|
+
return this.mutate(actor, resource, randomUUID(), async (trx, id) => {
|
|
46
|
+
await this.activeTenant(trx, tenantId);
|
|
47
|
+
const selected = await this.roles.resolve(trx, actor, input.roleId, tenantId);
|
|
48
|
+
if (await trx
|
|
49
|
+
.selectFrom("identity_users")
|
|
50
|
+
.select("id")
|
|
51
|
+
.where("email", "=", input.email)
|
|
52
|
+
.executeTakeFirst())
|
|
53
|
+
throw new IdentityError(409, "Email already exists.");
|
|
54
|
+
await trx
|
|
55
|
+
.insertInto("identity_users")
|
|
56
|
+
.values({
|
|
57
|
+
id,
|
|
58
|
+
email: input.email,
|
|
59
|
+
name: input.name,
|
|
60
|
+
active: 1,
|
|
61
|
+
password_hash: passwordHash,
|
|
62
|
+
})
|
|
63
|
+
.execute();
|
|
64
|
+
await trx
|
|
65
|
+
.insertInto("identity_memberships")
|
|
66
|
+
.values({
|
|
67
|
+
user_id: id,
|
|
68
|
+
tenant_id: tenantId,
|
|
69
|
+
role_id: selected.portal,
|
|
70
|
+
custom_role_id: selected.customRoleId,
|
|
71
|
+
})
|
|
72
|
+
.execute();
|
|
73
|
+
return {
|
|
74
|
+
id,
|
|
75
|
+
email: input.email,
|
|
76
|
+
name: input.name,
|
|
77
|
+
active: true,
|
|
78
|
+
version: 0,
|
|
79
|
+
};
|
|
80
|
+
}, tenantId);
|
|
81
|
+
}
|
|
82
|
+
if (resource === "organizations") {
|
|
83
|
+
const input = organizationCreateSchema.parse(raw);
|
|
84
|
+
return this.mutate(actor, resource, input.id, async (trx) => {
|
|
85
|
+
if (await trx
|
|
86
|
+
.selectFrom("identity_tenants")
|
|
87
|
+
.select("id")
|
|
88
|
+
.where("id", "=", input.id)
|
|
89
|
+
.executeTakeFirst())
|
|
90
|
+
throw new IdentityError(409, "Organization already exists.");
|
|
91
|
+
await trx
|
|
92
|
+
.insertInto("identity_tenants")
|
|
93
|
+
.values({
|
|
94
|
+
id: input.id,
|
|
95
|
+
name: input.name,
|
|
96
|
+
active: Number(input.active),
|
|
97
|
+
})
|
|
98
|
+
.execute();
|
|
99
|
+
return { ...input, version: 0 };
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (resource === "memberships") {
|
|
103
|
+
const input = membershipSchema.parse(raw);
|
|
104
|
+
const tenantId = this.tenant(actor, input.tenantId);
|
|
105
|
+
const selected = await this.roles.resolve(this.db, actor, input.roleId, tenantId);
|
|
106
|
+
await this.show(actor, "users", input.userId);
|
|
107
|
+
const id = `${input.userId}~${tenantId}~${selected.portal}`;
|
|
108
|
+
return this.mutate(actor, resource, id, async (trx) => {
|
|
109
|
+
await this.activeTenant(trx, tenantId);
|
|
110
|
+
const currentRole = await this.roles.resolve(trx, actor, input.roleId, tenantId);
|
|
111
|
+
if (await trx
|
|
112
|
+
.selectFrom("identity_memberships")
|
|
113
|
+
.select("user_id")
|
|
114
|
+
.where("user_id", "=", input.userId)
|
|
115
|
+
.where("tenant_id", "=", tenantId)
|
|
116
|
+
.where("role_id", "=", currentRole.portal)
|
|
117
|
+
.executeTakeFirst())
|
|
118
|
+
throw new IdentityError(409, "Membership already exists.");
|
|
119
|
+
await trx
|
|
120
|
+
.insertInto("identity_memberships")
|
|
121
|
+
.values({
|
|
122
|
+
user_id: input.userId,
|
|
123
|
+
tenant_id: tenantId,
|
|
124
|
+
role_id: currentRole.portal,
|
|
125
|
+
custom_role_id: currentRole.customRoleId,
|
|
126
|
+
})
|
|
127
|
+
.execute();
|
|
128
|
+
return {
|
|
129
|
+
id,
|
|
130
|
+
userId: input.userId,
|
|
131
|
+
tenantId,
|
|
132
|
+
roleId: input.roleId,
|
|
133
|
+
portal: currentRole.portal,
|
|
134
|
+
active: true,
|
|
135
|
+
version: 0,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
throw new IdentityError(405, "Creation is not supported for this resource.");
|
|
140
|
+
}
|
|
141
|
+
async update(actor, resource, id, raw) {
|
|
142
|
+
if (resource === "memberships")
|
|
143
|
+
return this.roles.updateMembership(actor, id, raw);
|
|
144
|
+
if (resource === "roles" && !["user", "admin", "super-admin"].includes(id))
|
|
145
|
+
return this.roles.update(actor, id, raw);
|
|
146
|
+
this.allow(actor, resource, true);
|
|
147
|
+
await this.show(actor, resource, id);
|
|
148
|
+
if (resource === "users") {
|
|
149
|
+
const input = userUpdateSchema.parse(raw);
|
|
150
|
+
return this.mutate(actor, resource, id, async (trx) => {
|
|
151
|
+
const current = await trx
|
|
152
|
+
.selectFrom("identity_users")
|
|
153
|
+
.selectAll()
|
|
154
|
+
.where("id", "=", id)
|
|
155
|
+
.executeTakeFirstOrThrow();
|
|
156
|
+
this.revision(current.version, input.expectedVersion);
|
|
157
|
+
if (actor.portal !== "super-admin")
|
|
158
|
+
await this.unprivilegedUser(trx, id);
|
|
159
|
+
if (input.active === false) {
|
|
160
|
+
await protectAdministrators(trx, id);
|
|
161
|
+
await trx
|
|
162
|
+
.deleteFrom("identity_sessions")
|
|
163
|
+
.where("user_id", "=", id)
|
|
164
|
+
.execute();
|
|
165
|
+
}
|
|
166
|
+
if (input.email &&
|
|
167
|
+
(await trx
|
|
168
|
+
.selectFrom("identity_users")
|
|
169
|
+
.select("id")
|
|
170
|
+
.where("email", "=", input.email)
|
|
171
|
+
.where("id", "!=", id)
|
|
172
|
+
.executeTakeFirst()))
|
|
173
|
+
throw new IdentityError(409, "Email already exists.");
|
|
174
|
+
const { expectedVersion, ...fields } = input;
|
|
175
|
+
await trx
|
|
176
|
+
.updateTable("identity_users")
|
|
177
|
+
.set({
|
|
178
|
+
...fields,
|
|
179
|
+
version: sql `version + 1`,
|
|
180
|
+
active: input.active === undefined
|
|
181
|
+
? current.active
|
|
182
|
+
: Number(input.active),
|
|
183
|
+
})
|
|
184
|
+
.where("id", "=", id)
|
|
185
|
+
.execute();
|
|
186
|
+
return {
|
|
187
|
+
id,
|
|
188
|
+
name: input.name ?? current.name,
|
|
189
|
+
email: input.email ?? current.email,
|
|
190
|
+
active: input.active ?? Boolean(current.active),
|
|
191
|
+
version: expectedVersion + 1,
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (resource === "organizations") {
|
|
196
|
+
const input = organizationUpdateSchema.parse(raw);
|
|
197
|
+
return this.mutate(actor, resource, id, async (trx) => {
|
|
198
|
+
const current = await trx
|
|
199
|
+
.selectFrom("identity_tenants")
|
|
200
|
+
.select("version")
|
|
201
|
+
.where("id", "=", id)
|
|
202
|
+
.executeTakeFirstOrThrow();
|
|
203
|
+
this.revision(current.version, input.expectedVersion);
|
|
204
|
+
if (input.active === false && id === actor.tenant.id)
|
|
205
|
+
throw new IdentityError(409, "Cannot deactivate your current organization.");
|
|
206
|
+
const { expectedVersion, ...fields } = input;
|
|
207
|
+
await trx
|
|
208
|
+
.updateTable("identity_tenants")
|
|
209
|
+
.set({
|
|
210
|
+
...fields,
|
|
211
|
+
version: sql `version + 1`,
|
|
212
|
+
active: input.active === undefined ? undefined : Number(input.active),
|
|
213
|
+
})
|
|
214
|
+
.where("id", "=", id)
|
|
215
|
+
.execute();
|
|
216
|
+
if (input.active === false)
|
|
217
|
+
await trx
|
|
218
|
+
.deleteFrom("identity_sessions")
|
|
219
|
+
.where("tenant_id", "=", id)
|
|
220
|
+
.execute();
|
|
221
|
+
const row = await trx
|
|
222
|
+
.selectFrom("identity_tenants")
|
|
223
|
+
.selectAll()
|
|
224
|
+
.where("id", "=", id)
|
|
225
|
+
.executeTakeFirstOrThrow();
|
|
226
|
+
return {
|
|
227
|
+
...row,
|
|
228
|
+
active: Boolean(row.active),
|
|
229
|
+
version: Number(row.version),
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (resource === "roles") {
|
|
234
|
+
const input = roleUpdateSchema.parse(raw);
|
|
235
|
+
if (!input.permissionIds.includes(`desk.${id}`) ||
|
|
236
|
+
!input.permissionIds.includes("identity.password"))
|
|
237
|
+
throw new IdentityError(409, "Keep required portal and password permissions.");
|
|
238
|
+
if (id !== "user" && !input.permissionIds.includes("identity.manage"))
|
|
239
|
+
throw new IdentityError(409, "Keep administration permission on administrator roles.");
|
|
240
|
+
return this.mutate(actor, resource, id, async (trx) => {
|
|
241
|
+
const current = await trx
|
|
242
|
+
.selectFrom("identity_roles")
|
|
243
|
+
.select("version")
|
|
244
|
+
.where("id", "=", id)
|
|
245
|
+
.executeTakeFirstOrThrow();
|
|
246
|
+
this.revision(current.version, input.expectedVersion);
|
|
247
|
+
await validateDeclaredPermissions(trx, actor.appId, id, input.permissionIds);
|
|
248
|
+
const known = await trx
|
|
249
|
+
.selectFrom("identity_permissions")
|
|
250
|
+
.select("id")
|
|
251
|
+
.execute();
|
|
252
|
+
if (input.permissionIds.some((permission) => !known.some((row) => row.id === permission)))
|
|
253
|
+
throw new IdentityError(422, "Unknown permission.");
|
|
254
|
+
if (id === "user" &&
|
|
255
|
+
input.permissionIds.some((permission) => permission.startsWith("identity.manage")))
|
|
256
|
+
throw new IdentityError(403, "User roles cannot receive administration permissions.");
|
|
257
|
+
await trx
|
|
258
|
+
.deleteFrom("identity_role_permissions")
|
|
259
|
+
.where("role_id", "=", id)
|
|
260
|
+
.execute();
|
|
261
|
+
for (const permission of new Set(input.permissionIds))
|
|
262
|
+
await trx
|
|
263
|
+
.insertInto("identity_role_permissions")
|
|
264
|
+
.values({ role_id: id, permission_id: permission })
|
|
265
|
+
.execute();
|
|
266
|
+
await trx
|
|
267
|
+
.updateTable("identity_roles")
|
|
268
|
+
.set({ version: sql `version + 1` })
|
|
269
|
+
.where("id", "=", id)
|
|
270
|
+
.execute();
|
|
271
|
+
return {
|
|
272
|
+
id,
|
|
273
|
+
permissionIds: [...new Set(input.permissionIds)],
|
|
274
|
+
version: input.expectedVersion + 1,
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
throw new IdentityError(405, "Update is not supported for this resource.");
|
|
279
|
+
}
|
|
280
|
+
async remove(actor, resource, id, expectedVersion) {
|
|
281
|
+
if (resource === "memberships")
|
|
282
|
+
return this.roles.removeMembership(actor, id, expectedVersion);
|
|
283
|
+
this.allow(actor, resource, true);
|
|
284
|
+
const row = await this.show(actor, resource, id);
|
|
285
|
+
if (resource === "sessions") {
|
|
286
|
+
await this.mutate(actor, resource, id, async (trx) => {
|
|
287
|
+
await trx
|
|
288
|
+
.deleteFrom("identity_sessions")
|
|
289
|
+
.where("token_hash", "=", id)
|
|
290
|
+
.where("app_id", "=", actor.appId)
|
|
291
|
+
.execute();
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
throw new IdentityError(405, "Deletion is not supported for this resource.");
|
|
296
|
+
}
|
|
297
|
+
async profile(actor, raw) {
|
|
298
|
+
if (!actor.permissions.includes("identity.self"))
|
|
299
|
+
throw new IdentityError(403, "Access denied.");
|
|
300
|
+
if (raw !== undefined) {
|
|
301
|
+
const input = profileSchema.parse(raw);
|
|
302
|
+
await this.mutate(actor, "profile", actor.user.id, async (trx) => {
|
|
303
|
+
const changed = await trx
|
|
304
|
+
.updateTable("identity_users")
|
|
305
|
+
.set({ name: input.name, version: sql `version + 1` })
|
|
306
|
+
.where("id", "=", actor.user.id)
|
|
307
|
+
.where("version", "=", input.expectedVersion)
|
|
308
|
+
.executeTakeFirst();
|
|
309
|
+
if (changed.numUpdatedRows !== 1n)
|
|
310
|
+
throw new IdentityError(409, "This record changed. Reload before saving.");
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const user = await this.db
|
|
314
|
+
.selectFrom("identity_users")
|
|
315
|
+
.select(["id", "name", "email", "active", "version"])
|
|
316
|
+
.where("id", "=", actor.user.id)
|
|
317
|
+
.executeTakeFirstOrThrow();
|
|
318
|
+
return {
|
|
319
|
+
...user,
|
|
320
|
+
active: Boolean(user.active),
|
|
321
|
+
version: Number(user.version),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
async settings(actor, raw) {
|
|
325
|
+
if (raw !== undefined) {
|
|
326
|
+
this.allow(actor, "settings", true);
|
|
327
|
+
const input = settingsSchema.parse(raw);
|
|
328
|
+
await this.mutate(actor, "settings", actor.tenant.id, async (trx) => {
|
|
329
|
+
const current = await trx
|
|
330
|
+
.selectFrom("identity_settings")
|
|
331
|
+
.select("version")
|
|
332
|
+
.where("tenant_id", "=", actor.tenant.id)
|
|
333
|
+
.executeTakeFirst();
|
|
334
|
+
this.revision(current?.version ?? 0, input.expectedVersion);
|
|
335
|
+
await trx
|
|
336
|
+
.insertInto("identity_settings")
|
|
337
|
+
.values({
|
|
338
|
+
tenant_id: actor.tenant.id,
|
|
339
|
+
display_name: input.displayName,
|
|
340
|
+
locale: input.locale,
|
|
341
|
+
time_zone: input.timeZone,
|
|
342
|
+
version: input.expectedVersion + 1,
|
|
343
|
+
})
|
|
344
|
+
.onConflict((c) => c.column("tenant_id").doUpdateSet({
|
|
345
|
+
display_name: input.displayName,
|
|
346
|
+
locale: input.locale,
|
|
347
|
+
time_zone: input.timeZone,
|
|
348
|
+
version: input.expectedVersion + 1,
|
|
349
|
+
}))
|
|
350
|
+
.execute();
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return this.presentationService.organizationSettings(actor);
|
|
354
|
+
}
|
|
355
|
+
async applicationSettings(actor, security, raw, fallbackSeconds) {
|
|
356
|
+
if (actor.portal === "user")
|
|
357
|
+
throw new IdentityError(403, "Access denied.");
|
|
358
|
+
if (raw !== undefined) {
|
|
359
|
+
if (actor.portal !== "super-admin" ||
|
|
360
|
+
!actor.permissions.includes("identity.manage"))
|
|
361
|
+
throw new IdentityError(403, "Access denied.");
|
|
362
|
+
const input = security
|
|
363
|
+
? securitySettingsSchema.parse(raw)
|
|
364
|
+
: settingsSchema.parse(raw);
|
|
365
|
+
await this.mutate(actor, security ? "security-settings" : "application-settings", actor.appId, async (trx) => {
|
|
366
|
+
const current = await trx
|
|
367
|
+
.selectFrom("identity_app_settings")
|
|
368
|
+
.selectAll()
|
|
369
|
+
.where("app_id", "=", actor.appId)
|
|
370
|
+
.executeTakeFirst();
|
|
371
|
+
this.revision(current?.version ?? 0, input.expectedVersion);
|
|
372
|
+
const row = {
|
|
373
|
+
app_id: actor.appId,
|
|
374
|
+
display_name: current?.display_name ?? this.appName,
|
|
375
|
+
locale: current?.locale ?? "en",
|
|
376
|
+
time_zone: current?.time_zone ?? "UTC",
|
|
377
|
+
session_seconds: current?.session_seconds ?? fallbackSeconds,
|
|
378
|
+
version: input.expectedVersion + 1,
|
|
379
|
+
};
|
|
380
|
+
if ("sessionSeconds" in input)
|
|
381
|
+
row.session_seconds = input.sessionSeconds;
|
|
382
|
+
else {
|
|
383
|
+
row.display_name = input.displayName;
|
|
384
|
+
row.locale = input.locale;
|
|
385
|
+
row.time_zone = input.timeZone;
|
|
386
|
+
}
|
|
387
|
+
await trx
|
|
388
|
+
.insertInto("identity_app_settings")
|
|
389
|
+
.values(row)
|
|
390
|
+
.onConflict((c) => c.column("app_id").doUpdateSet(row))
|
|
391
|
+
.execute();
|
|
392
|
+
if (security)
|
|
393
|
+
await trx
|
|
394
|
+
.deleteFrom("identity_sessions")
|
|
395
|
+
.where("app_id", "=", actor.appId)
|
|
396
|
+
.execute();
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const row = await this.db
|
|
400
|
+
.selectFrom("identity_app_settings")
|
|
401
|
+
.selectAll()
|
|
402
|
+
.where("app_id", "=", actor.appId)
|
|
403
|
+
.executeTakeFirst();
|
|
404
|
+
return security
|
|
405
|
+
? {
|
|
406
|
+
sessionSeconds: Number(row?.session_seconds ?? fallbackSeconds),
|
|
407
|
+
version: Number(row?.version ?? 0),
|
|
408
|
+
}
|
|
409
|
+
: {
|
|
410
|
+
displayName: row?.display_name ?? this.appName,
|
|
411
|
+
locale: row?.locale ?? "en",
|
|
412
|
+
timeZone: row?.time_zone ?? "UTC",
|
|
413
|
+
version: Number(row?.version ?? 0),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
revision(current, expected) {
|
|
417
|
+
if (Number(current) !== expected)
|
|
418
|
+
throw new IdentityError(409, "This record changed. Reload before saving.");
|
|
419
|
+
}
|
|
420
|
+
async presentation(actor) {
|
|
421
|
+
return this.presentationService.presentation(actor);
|
|
422
|
+
}
|
|
423
|
+
async configuration(appId, requiresOrganizationId) {
|
|
424
|
+
return this.presentationService.configuration(appId, requiresOrganizationId);
|
|
425
|
+
}
|
|
426
|
+
allow(actor, resource, write) {
|
|
427
|
+
if (!write &&
|
|
428
|
+
["roles", "permissions", "organizations"].includes(resource) &&
|
|
429
|
+
actor.portal !== "user")
|
|
430
|
+
return;
|
|
431
|
+
if (resource === "sessions" && actor.portal === "user")
|
|
432
|
+
return;
|
|
433
|
+
if (actor.portal === "user" ||
|
|
434
|
+
!actor.permissions.includes("identity.manage"))
|
|
435
|
+
throw new IdentityError(403, "Access denied.");
|
|
436
|
+
if (write &&
|
|
437
|
+
["roles", "permissions", "organizations"].includes(resource) &&
|
|
438
|
+
actor.portal !== "super-admin")
|
|
439
|
+
throw new IdentityError(403, "Only super administrators can change this resource.");
|
|
440
|
+
}
|
|
441
|
+
tenant(actor, requested) {
|
|
442
|
+
if (requested &&
|
|
443
|
+
requested !== actor.tenant.id &&
|
|
444
|
+
actor.portal !== "super-admin")
|
|
445
|
+
throw new IdentityError(403, "Access denied.");
|
|
446
|
+
return requested ?? actor.tenant.id;
|
|
447
|
+
}
|
|
448
|
+
assignableRole(actor, role) {
|
|
449
|
+
if (actor.portal !== "super-admin" && role !== "user")
|
|
450
|
+
throw new IdentityError(403, "Only super administrators can assign privileged roles.");
|
|
451
|
+
}
|
|
452
|
+
async activeTenant(trx, id) {
|
|
453
|
+
if (!(await trx
|
|
454
|
+
.selectFrom("identity_tenants")
|
|
455
|
+
.select("id")
|
|
456
|
+
.where("id", "=", id)
|
|
457
|
+
.where("active", "=", 1)
|
|
458
|
+
.executeTakeFirst()))
|
|
459
|
+
throw new IdentityError(422, "Organization is inactive or unavailable.");
|
|
460
|
+
}
|
|
461
|
+
async unprivilegedUser(trx, id) {
|
|
462
|
+
const privileged = await trx
|
|
463
|
+
.selectFrom("identity_memberships")
|
|
464
|
+
.select("role_id")
|
|
465
|
+
.where("user_id", "=", id)
|
|
466
|
+
.where("role_id", "in", ["admin", "super-admin"])
|
|
467
|
+
.executeTakeFirst();
|
|
468
|
+
if (privileged)
|
|
469
|
+
throw new IdentityError(403, "Only super administrators can change privileged accounts.");
|
|
470
|
+
const tenants = await trx
|
|
471
|
+
.selectFrom("identity_memberships")
|
|
472
|
+
.select("tenant_id")
|
|
473
|
+
.where("user_id", "=", id)
|
|
474
|
+
.distinct()
|
|
475
|
+
.execute();
|
|
476
|
+
if (tenants.length > 1)
|
|
477
|
+
throw new IdentityError(403, "Only super administrators can change shared accounts.");
|
|
478
|
+
}
|
|
479
|
+
async mutate(actor, resource, id, action, affectedTenant) {
|
|
480
|
+
return this.db.transaction().execute(async (trx) => {
|
|
481
|
+
checkIdentityRequest();
|
|
482
|
+
const result = await action(trx, id);
|
|
483
|
+
checkIdentityRequest();
|
|
484
|
+
const targetTenant = affectedTenant ??
|
|
485
|
+
(resource === "organizations"
|
|
486
|
+
? id
|
|
487
|
+
: resource === "memberships"
|
|
488
|
+
? id.split("~")[1]
|
|
489
|
+
: actor.tenant.id);
|
|
490
|
+
let targets = [targetTenant];
|
|
491
|
+
if (!affectedTenant && ["users", "profile"].includes(resource))
|
|
492
|
+
targets = (await trx
|
|
493
|
+
.selectFrom("identity_memberships")
|
|
494
|
+
.select("tenant_id")
|
|
495
|
+
.where("user_id", "=", id)
|
|
496
|
+
.distinct()
|
|
497
|
+
.execute()).map((row) => row.tenant_id);
|
|
498
|
+
if (["roles", "application-settings", "security-settings"].includes(resource))
|
|
499
|
+
targets = (await trx.selectFrom("identity_tenants").select("id").execute()).map((row) => row.id);
|
|
500
|
+
for (const tenant of new Set(targets))
|
|
501
|
+
await trx
|
|
502
|
+
.insertInto("identity_audit_events")
|
|
503
|
+
.values({
|
|
504
|
+
id: randomUUID(),
|
|
505
|
+
app_id: actor.appId,
|
|
506
|
+
actor_id: actor.user.id,
|
|
507
|
+
tenant_id: tenant,
|
|
508
|
+
action: `identity.${resource}.changed`,
|
|
509
|
+
resource_id: id,
|
|
510
|
+
created_at: new Date().toISOString(),
|
|
511
|
+
})
|
|
512
|
+
.execute();
|
|
513
|
+
checkIdentityRequest();
|
|
514
|
+
return result;
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { IdentityService } from "./identity.service.js";
|
|
3
|
+
import type { IdentityConfig, Portal } from "./identity.types.js";
|
|
4
|
+
import { IdentityAdministrationService } from "./identity.administration-service.js";
|
|
5
|
+
import { IdentityLifecycleService } from "./identity.lifecycle-service.js";
|
|
6
|
+
export declare class IdentityController {
|
|
7
|
+
private readonly service;
|
|
8
|
+
private readonly config;
|
|
9
|
+
private readonly administration;
|
|
10
|
+
private readonly lifecycle;
|
|
11
|
+
constructor(service: IdentityService, config: IdentityConfig, administration: IdentityAdministrationService, lifecycle: IdentityLifecycleService);
|
|
12
|
+
handle(request: IncomingMessage, response: ServerResponse, signal?: AbortSignal): Promise<boolean>;
|
|
13
|
+
authenticateRequest(request: IncomingMessage, requestedPortal: Portal, signal?: AbortSignal): Promise<import("./identity.types.js").Principal>;
|
|
14
|
+
private handleRequest;
|
|
15
|
+
private adminRequest;
|
|
16
|
+
private wireList;
|
|
17
|
+
private cookie;
|
|
18
|
+
private cookieName;
|
|
19
|
+
private token;
|
|
20
|
+
private json;
|
|
21
|
+
private body;
|
|
22
|
+
}
|