@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,23 @@
|
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
|
+
import type { IdentitySchema, Principal } from "./identity.types.js";
|
|
3
|
+
export declare class IdentityPresentationService {
|
|
4
|
+
private readonly db;
|
|
5
|
+
private readonly appName;
|
|
6
|
+
constructor(db: Kysely<IdentitySchema>, appName: string);
|
|
7
|
+
configuration(appId: string, requiresOrganizationId: boolean): Promise<{
|
|
8
|
+
displayName: string;
|
|
9
|
+
requiresOrganizationId: boolean;
|
|
10
|
+
}>;
|
|
11
|
+
presentation(actor: Principal): Promise<{
|
|
12
|
+
displayName: string;
|
|
13
|
+
organizationDisplayName: string;
|
|
14
|
+
locale: string;
|
|
15
|
+
timeZone: string;
|
|
16
|
+
}>;
|
|
17
|
+
organizationSettings(actor: Principal): Promise<{
|
|
18
|
+
displayName: string;
|
|
19
|
+
locale: string;
|
|
20
|
+
timeZone: string;
|
|
21
|
+
version: number;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export class IdentityPresentationService {
|
|
2
|
+
db;
|
|
3
|
+
appName;
|
|
4
|
+
constructor(db, appName) {
|
|
5
|
+
this.db = db;
|
|
6
|
+
this.appName = appName;
|
|
7
|
+
}
|
|
8
|
+
async configuration(appId, requiresOrganizationId) {
|
|
9
|
+
const app = await this.db
|
|
10
|
+
.selectFrom("identity_app_settings")
|
|
11
|
+
.select("display_name")
|
|
12
|
+
.where("app_id", "=", appId)
|
|
13
|
+
.executeTakeFirst();
|
|
14
|
+
return {
|
|
15
|
+
displayName: app?.display_name ?? this.appName,
|
|
16
|
+
requiresOrganizationId,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async presentation(actor) {
|
|
20
|
+
const app = await this.db
|
|
21
|
+
.selectFrom("identity_app_settings")
|
|
22
|
+
.select(["display_name", "locale", "time_zone"])
|
|
23
|
+
.where("app_id", "=", actor.appId)
|
|
24
|
+
.executeTakeFirst();
|
|
25
|
+
const organization = await this.db
|
|
26
|
+
.selectFrom("identity_settings")
|
|
27
|
+
.selectAll()
|
|
28
|
+
.where("tenant_id", "=", actor.tenant.id)
|
|
29
|
+
.executeTakeFirst();
|
|
30
|
+
return {
|
|
31
|
+
displayName: app?.display_name ?? this.appName,
|
|
32
|
+
organizationDisplayName: organization?.display_name ?? actor.tenant.name,
|
|
33
|
+
locale: organization?.locale ?? app?.locale ?? "en",
|
|
34
|
+
timeZone: organization?.time_zone ?? app?.time_zone ?? "UTC",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async organizationSettings(actor) {
|
|
38
|
+
const organization = await this.db
|
|
39
|
+
.selectFrom("identity_settings")
|
|
40
|
+
.select("version")
|
|
41
|
+
.where("tenant_id", "=", actor.tenant.id)
|
|
42
|
+
.executeTakeFirst();
|
|
43
|
+
const presentation = await this.presentation(actor);
|
|
44
|
+
return {
|
|
45
|
+
displayName: presentation.organizationDisplayName,
|
|
46
|
+
locale: presentation.locale,
|
|
47
|
+
timeZone: presentation.timeZone,
|
|
48
|
+
version: Number(organization?.version ?? 0),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
|
+
import type { IdentitySchema, IdentityProviderOptions } from "./identity.types.js";
|
|
3
|
+
import { type IdentityPermissionDeclaration } from "./identity.permission-declarations.js";
|
|
4
|
+
export declare function createIdentityProvider(database: Kysely<IdentitySchema>, environment: NodeJS.ProcessEnv, options?: IdentityProviderOptions): {
|
|
5
|
+
registerPermissions: (declaration: IdentityPermissionDeclaration) => Promise<{
|
|
6
|
+
owner: string;
|
|
7
|
+
permissionIds: string[];
|
|
8
|
+
}>;
|
|
9
|
+
handle: (request: import("node:http").IncomingMessage, response: import("node:http").ServerResponse, signal?: AbortSignal) => Promise<boolean>;
|
|
10
|
+
authenticate: (portal: import("./identity.types.js").Portal, token?: string) => Promise<import("./identity.types.js").Principal>;
|
|
11
|
+
authenticateRequest: (request: import("node:http").IncomingMessage, requestedPortal: import("./identity.types.js").Portal, signal?: AbortSignal) => Promise<import("./identity.types.js").Principal>;
|
|
12
|
+
requirePermission: (principal: import("./identity.types.js").Principal, permission: string) => void;
|
|
13
|
+
administration: {
|
|
14
|
+
list: (actor: import("./identity.types.js").Principal, resource: import("./identity.administration-schema.js").IdentityResource, query: import("./identity.administration-schema.js").IdentityListQuery) => Promise<{
|
|
15
|
+
data: Record<string, unknown>[];
|
|
16
|
+
meta: {
|
|
17
|
+
page: number;
|
|
18
|
+
perPage: number;
|
|
19
|
+
total: number;
|
|
20
|
+
lastPage: number;
|
|
21
|
+
};
|
|
22
|
+
}>;
|
|
23
|
+
show: (actor: import("./identity.types.js").Principal, resource: import("./identity.administration-schema.js").IdentityResource, id: string) => Promise<Record<string, unknown>>;
|
|
24
|
+
create: (actor: import("./identity.types.js").Principal, resource: import("./identity.administration-schema.js").IdentityResource, raw: unknown) => Promise<{
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
active: boolean;
|
|
28
|
+
version: number;
|
|
29
|
+
} | {
|
|
30
|
+
id: string;
|
|
31
|
+
userId: string;
|
|
32
|
+
tenantId: string;
|
|
33
|
+
roleId: string;
|
|
34
|
+
portal: import("./identity.types.js").Portal;
|
|
35
|
+
active: boolean;
|
|
36
|
+
version: number;
|
|
37
|
+
}>;
|
|
38
|
+
update: (actor: import("./identity.types.js").Principal, resource: import("./identity.administration-schema.js").IdentityResource, id: string, raw: unknown) => Promise<{
|
|
39
|
+
id: string;
|
|
40
|
+
userId: string;
|
|
41
|
+
tenantId: string;
|
|
42
|
+
roleId: string;
|
|
43
|
+
portal: import("./identity.types.js").Portal;
|
|
44
|
+
active: boolean;
|
|
45
|
+
version: number;
|
|
46
|
+
} | {
|
|
47
|
+
active: boolean;
|
|
48
|
+
version: number;
|
|
49
|
+
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
} | {
|
|
52
|
+
id: string;
|
|
53
|
+
permissionIds: string[];
|
|
54
|
+
version: number;
|
|
55
|
+
}>;
|
|
56
|
+
remove: (actor: import("./identity.types.js").Principal, resource: import("./identity.administration-schema.js").IdentityResource, id: string, expectedVersion?: number) => Promise<void>;
|
|
57
|
+
profile: (actor: import("./identity.types.js").Principal, raw?: unknown) => Promise<{
|
|
58
|
+
active: boolean;
|
|
59
|
+
version: number;
|
|
60
|
+
email: string;
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
}>;
|
|
64
|
+
settings: (actor: import("./identity.types.js").Principal, raw?: unknown) => Promise<{
|
|
65
|
+
displayName: string;
|
|
66
|
+
locale: string;
|
|
67
|
+
timeZone: string;
|
|
68
|
+
version: number;
|
|
69
|
+
}>;
|
|
70
|
+
presentation: (actor: import("./identity.types.js").Principal) => Promise<{
|
|
71
|
+
displayName: string;
|
|
72
|
+
organizationDisplayName: string;
|
|
73
|
+
locale: string;
|
|
74
|
+
timeZone: string;
|
|
75
|
+
}>;
|
|
76
|
+
};
|
|
77
|
+
verify(): Promise<void>;
|
|
78
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { IdentityRepository } from "./identity.repository.js";
|
|
3
|
+
import { IdentityService } from "./identity.service.js";
|
|
4
|
+
import { IdentityController } from "./identity.controller.js";
|
|
5
|
+
import { IdentityAdministrationService } from "./identity.administration-service.js";
|
|
6
|
+
import { IdentityLifecycleService } from "./identity.lifecycle-service.js";
|
|
7
|
+
import { identityKeySchema } from "./identity.schema.js";
|
|
8
|
+
import { registerIdentityPermissions } from "./identity.permission-declarations.js";
|
|
9
|
+
export function createIdentityProvider(database, environment, options = {}) {
|
|
10
|
+
const config = z
|
|
11
|
+
.object({
|
|
12
|
+
appId: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/),
|
|
13
|
+
origin: z.url(),
|
|
14
|
+
mode: z.enum(["single-client", "multi-tenant"]),
|
|
15
|
+
tenantId: identityKeySchema,
|
|
16
|
+
sessionSeconds: z.coerce.number().int().min(300).max(86400),
|
|
17
|
+
})
|
|
18
|
+
.parse({
|
|
19
|
+
appId: environment.APP_ID,
|
|
20
|
+
origin: environment.APP_URL,
|
|
21
|
+
mode: environment.IDENTITY_MODE ?? "single-client",
|
|
22
|
+
tenantId: environment.IDENTITY_TENANT_ID ?? "default",
|
|
23
|
+
sessionSeconds: environment.IDENTITY_SESSION_SECONDS ?? 28800,
|
|
24
|
+
});
|
|
25
|
+
const origin = new URL(config.origin);
|
|
26
|
+
if (origin.origin !== config.origin ||
|
|
27
|
+
!["http:", "https:"].includes(origin.protocol))
|
|
28
|
+
throw new Error("Identity requires a valid application origin.");
|
|
29
|
+
if (origin.protocol !== "https:" &&
|
|
30
|
+
!["127.0.0.1", "localhost", "[::1]"].includes(origin.hostname))
|
|
31
|
+
throw new Error("Identity requires HTTPS outside loopback development.");
|
|
32
|
+
const service = new IdentityService(new IdentityRepository(database), config);
|
|
33
|
+
const appName = z
|
|
34
|
+
.string()
|
|
35
|
+
.trim()
|
|
36
|
+
.min(1)
|
|
37
|
+
.max(100)
|
|
38
|
+
.parse(environment.APP_NAME || config.appId);
|
|
39
|
+
const administration = new IdentityAdministrationService(database, appName);
|
|
40
|
+
const lifecycle = new IdentityLifecycleService(database, config, options.delivery);
|
|
41
|
+
const controller = new IdentityController(service, config, administration, lifecycle);
|
|
42
|
+
return {
|
|
43
|
+
registerPermissions: (declaration) => registerIdentityPermissions(database, config.appId, declaration),
|
|
44
|
+
handle: controller.handle.bind(controller),
|
|
45
|
+
authenticate: service.authenticate.bind(service),
|
|
46
|
+
authenticateRequest: controller.authenticateRequest.bind(controller),
|
|
47
|
+
requirePermission: service.requirePermission.bind(service),
|
|
48
|
+
administration: {
|
|
49
|
+
list: administration.list.bind(administration),
|
|
50
|
+
show: administration.show.bind(administration),
|
|
51
|
+
create: administration.create.bind(administration),
|
|
52
|
+
update: administration.update.bind(administration),
|
|
53
|
+
remove: administration.remove.bind(administration),
|
|
54
|
+
profile: administration.profile.bind(administration),
|
|
55
|
+
settings: administration.settings.bind(administration),
|
|
56
|
+
presentation: administration.presentation.bind(administration),
|
|
57
|
+
},
|
|
58
|
+
async verify() {
|
|
59
|
+
await database.selectFrom("identity_permission_declarations").select(["permission_id", "label"]).limit(1).execute();
|
|
60
|
+
for (const declaration of options.permissions ?? [])
|
|
61
|
+
await registerIdentityPermissions(database, config.appId, declaration);
|
|
62
|
+
await database
|
|
63
|
+
.selectFrom("identity_roles")
|
|
64
|
+
.select("id")
|
|
65
|
+
.limit(1)
|
|
66
|
+
.execute();
|
|
67
|
+
await database
|
|
68
|
+
.selectFrom("identity_users")
|
|
69
|
+
.select("version")
|
|
70
|
+
.limit(1)
|
|
71
|
+
.execute();
|
|
72
|
+
await database
|
|
73
|
+
.selectFrom("identity_tenants")
|
|
74
|
+
.select("version")
|
|
75
|
+
.limit(1)
|
|
76
|
+
.execute();
|
|
77
|
+
await database
|
|
78
|
+
.selectFrom("identity_roles")
|
|
79
|
+
.select("version")
|
|
80
|
+
.limit(1)
|
|
81
|
+
.execute();
|
|
82
|
+
await database
|
|
83
|
+
.selectFrom("identity_settings")
|
|
84
|
+
.select("version")
|
|
85
|
+
.limit(1)
|
|
86
|
+
.execute();
|
|
87
|
+
await database
|
|
88
|
+
.selectFrom("identity_app_settings")
|
|
89
|
+
.select(["version", "session_seconds"])
|
|
90
|
+
.limit(1)
|
|
91
|
+
.execute();
|
|
92
|
+
await database
|
|
93
|
+
.selectFrom("identity_tokens")
|
|
94
|
+
.select(["delivered", "consumed_at"])
|
|
95
|
+
.limit(1)
|
|
96
|
+
.execute();
|
|
97
|
+
await database
|
|
98
|
+
.selectFrom("identity_audit_events")
|
|
99
|
+
.select("created_at")
|
|
100
|
+
.limit(1)
|
|
101
|
+
.execute();
|
|
102
|
+
await database
|
|
103
|
+
.selectFrom("identity_custom_roles")
|
|
104
|
+
.select(["app_id", "tenant_id", "version"])
|
|
105
|
+
.limit(1)
|
|
106
|
+
.execute();
|
|
107
|
+
await database
|
|
108
|
+
.selectFrom("identity_custom_role_permissions")
|
|
109
|
+
.select("permission_id")
|
|
110
|
+
.limit(1)
|
|
111
|
+
.execute();
|
|
112
|
+
await database
|
|
113
|
+
.selectFrom("identity_memberships")
|
|
114
|
+
.select(["custom_role_id", "active", "version"])
|
|
115
|
+
.limit(1)
|
|
116
|
+
.execute();
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Kysely } from "kysely";
|
|
2
|
+
import type { IdentitySchema, Portal } from "./identity.types.js";
|
|
3
|
+
export declare class IdentityRepository {
|
|
4
|
+
private readonly db;
|
|
5
|
+
constructor(db: Kysely<IdentitySchema>);
|
|
6
|
+
user(email: string): Promise<{
|
|
7
|
+
active: number;
|
|
8
|
+
email: string;
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
password_hash: string;
|
|
12
|
+
version: number;
|
|
13
|
+
} | undefined>;
|
|
14
|
+
sessionSeconds(appId: string, fallback: number): Promise<number>;
|
|
15
|
+
principal(userId: string, tenantId: string, portal: Portal, appId: string): Promise<{
|
|
16
|
+
user: {
|
|
17
|
+
id: string;
|
|
18
|
+
email: string;
|
|
19
|
+
name: string;
|
|
20
|
+
};
|
|
21
|
+
tenant: {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
};
|
|
25
|
+
permissions: string[];
|
|
26
|
+
} | null>;
|
|
27
|
+
session(tokenHash: string, appId: string, portal: Portal, now: string): Promise<{
|
|
28
|
+
app_id: string;
|
|
29
|
+
expires_at: string;
|
|
30
|
+
portal: Portal;
|
|
31
|
+
tenant_id: string;
|
|
32
|
+
token_hash: string;
|
|
33
|
+
user_id: string;
|
|
34
|
+
} | undefined>;
|
|
35
|
+
createSession(session: IdentitySchema["identity_sessions"], expectedHash: string, previous?: string): Promise<boolean>;
|
|
36
|
+
revoke(tokenHash: string, appId: string, portal: Portal): Promise<void>;
|
|
37
|
+
throttle(key: string): Promise<number>;
|
|
38
|
+
clearThrottle(key: string): Promise<void>;
|
|
39
|
+
changePassword(userId: string, expectedHash: string, passwordHash: string): Promise<boolean>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { filterDeclaredPermissions } from "./identity.permission-declarations.js";
|
|
2
|
+
import { sql } from "kysely";
|
|
3
|
+
import { checkIdentityRequest } from "./identity.request-context.js";
|
|
4
|
+
export class IdentityRepository {
|
|
5
|
+
db;
|
|
6
|
+
constructor(db) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
}
|
|
9
|
+
user(email) {
|
|
10
|
+
return this.db
|
|
11
|
+
.selectFrom("identity_users")
|
|
12
|
+
.selectAll()
|
|
13
|
+
.where("email", "=", email)
|
|
14
|
+
.executeTakeFirst();
|
|
15
|
+
}
|
|
16
|
+
async sessionSeconds(appId, fallback) {
|
|
17
|
+
const row = await this.db
|
|
18
|
+
.selectFrom("identity_app_settings")
|
|
19
|
+
.select("session_seconds")
|
|
20
|
+
.where("app_id", "=", appId)
|
|
21
|
+
.executeTakeFirst();
|
|
22
|
+
return Number(row?.session_seconds ?? fallback);
|
|
23
|
+
}
|
|
24
|
+
async principal(userId, tenantId, portal, appId) {
|
|
25
|
+
const member = await this.db
|
|
26
|
+
.selectFrom("identity_memberships as m")
|
|
27
|
+
.innerJoin("identity_users as u", "u.id", "m.user_id")
|
|
28
|
+
.innerJoin("identity_tenants as t", "t.id", "m.tenant_id")
|
|
29
|
+
.select([
|
|
30
|
+
"u.id",
|
|
31
|
+
"u.email",
|
|
32
|
+
"u.name",
|
|
33
|
+
"t.id as tenantId",
|
|
34
|
+
"t.name as tenantName",
|
|
35
|
+
"m.custom_role_id as customRoleId",
|
|
36
|
+
])
|
|
37
|
+
.where("u.id", "=", userId)
|
|
38
|
+
.where("m.tenant_id", "=", tenantId)
|
|
39
|
+
.where("m.role_id", "=", portal)
|
|
40
|
+
.where("m.active", "=", 1)
|
|
41
|
+
.where("u.active", "=", 1)
|
|
42
|
+
.where("t.active", "=", 1)
|
|
43
|
+
.executeTakeFirst();
|
|
44
|
+
if (!member)
|
|
45
|
+
return null;
|
|
46
|
+
if (member.customRoleId) {
|
|
47
|
+
const role = await this.db
|
|
48
|
+
.selectFrom("identity_custom_roles")
|
|
49
|
+
.select("id")
|
|
50
|
+
.where("id", "=", member.customRoleId)
|
|
51
|
+
.where("app_id", "=", appId)
|
|
52
|
+
.where("tenant_id", "=", tenantId)
|
|
53
|
+
.where("active", "=", 1)
|
|
54
|
+
.executeTakeFirst();
|
|
55
|
+
if (!role || portal !== "user")
|
|
56
|
+
return null;
|
|
57
|
+
const permissions = await this.db
|
|
58
|
+
.selectFrom("identity_custom_role_permissions")
|
|
59
|
+
.select("permission_id")
|
|
60
|
+
.where("role_id", "=", role.id)
|
|
61
|
+
.execute();
|
|
62
|
+
return {
|
|
63
|
+
user: { id: member.id, email: member.email, name: member.name },
|
|
64
|
+
tenant: { id: member.tenantId, name: member.tenantName },
|
|
65
|
+
permissions: await filterDeclaredPermissions(this.db, appId, portal, permissions.map((row) => row.permission_id)),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const permissions = await this.db
|
|
69
|
+
.selectFrom("identity_role_permissions")
|
|
70
|
+
.select("permission_id")
|
|
71
|
+
.where("role_id", "=", portal)
|
|
72
|
+
.execute();
|
|
73
|
+
return {
|
|
74
|
+
user: { id: member.id, email: member.email, name: member.name },
|
|
75
|
+
tenant: { id: member.tenantId, name: member.tenantName },
|
|
76
|
+
permissions: await filterDeclaredPermissions(this.db, appId, portal, permissions.map((p) => p.permission_id)),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async session(tokenHash, appId, portal, now) {
|
|
80
|
+
return this.db
|
|
81
|
+
.selectFrom("identity_sessions")
|
|
82
|
+
.selectAll()
|
|
83
|
+
.where("token_hash", "=", tokenHash)
|
|
84
|
+
.where("app_id", "=", appId)
|
|
85
|
+
.where("portal", "=", portal)
|
|
86
|
+
.where("expires_at", ">", now)
|
|
87
|
+
.executeTakeFirst();
|
|
88
|
+
}
|
|
89
|
+
async createSession(session, expectedHash, previous) {
|
|
90
|
+
return this.db.transaction().execute(async (trx) => {
|
|
91
|
+
checkIdentityRequest();
|
|
92
|
+
const current = await trx
|
|
93
|
+
.selectFrom("identity_users")
|
|
94
|
+
.select("id")
|
|
95
|
+
.where("id", "=", session.user_id)
|
|
96
|
+
.where("password_hash", "=", expectedHash)
|
|
97
|
+
.where("active", "=", 1)
|
|
98
|
+
.executeTakeFirst();
|
|
99
|
+
if (!current)
|
|
100
|
+
return false;
|
|
101
|
+
const membership = await trx
|
|
102
|
+
.selectFrom("identity_memberships")
|
|
103
|
+
.select("custom_role_id")
|
|
104
|
+
.where("user_id", "=", session.user_id)
|
|
105
|
+
.where("tenant_id", "=", session.tenant_id)
|
|
106
|
+
.where("role_id", "=", session.portal)
|
|
107
|
+
.where("active", "=", 1)
|
|
108
|
+
.executeTakeFirst();
|
|
109
|
+
if (!membership)
|
|
110
|
+
return false;
|
|
111
|
+
if (membership.custom_role_id) {
|
|
112
|
+
const role = await trx
|
|
113
|
+
.selectFrom("identity_custom_roles")
|
|
114
|
+
.select("id")
|
|
115
|
+
.where("id", "=", membership.custom_role_id)
|
|
116
|
+
.where("app_id", "=", session.app_id)
|
|
117
|
+
.where("tenant_id", "=", session.tenant_id)
|
|
118
|
+
.where("active", "=", 1)
|
|
119
|
+
.executeTakeFirst();
|
|
120
|
+
if (!role)
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
await trx
|
|
124
|
+
.deleteFrom("identity_sessions")
|
|
125
|
+
.where("expires_at", "<=", new Date().toISOString())
|
|
126
|
+
.execute();
|
|
127
|
+
if (previous)
|
|
128
|
+
await trx
|
|
129
|
+
.deleteFrom("identity_sessions")
|
|
130
|
+
.where("token_hash", "=", previous)
|
|
131
|
+
.where("app_id", "=", session.app_id)
|
|
132
|
+
.where("portal", "=", session.portal)
|
|
133
|
+
.execute();
|
|
134
|
+
await trx.insertInto("identity_sessions").values(session).execute();
|
|
135
|
+
checkIdentityRequest();
|
|
136
|
+
return true;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async revoke(tokenHash, appId, portal) {
|
|
140
|
+
checkIdentityRequest();
|
|
141
|
+
await this.db
|
|
142
|
+
.deleteFrom("identity_sessions")
|
|
143
|
+
.where("token_hash", "=", tokenHash)
|
|
144
|
+
.where("app_id", "=", appId)
|
|
145
|
+
.where("portal", "=", portal)
|
|
146
|
+
.execute();
|
|
147
|
+
}
|
|
148
|
+
async throttle(key) {
|
|
149
|
+
const now = new Date();
|
|
150
|
+
return this.db.transaction().execute(async (trx) => {
|
|
151
|
+
await trx
|
|
152
|
+
.deleteFrom("identity_throttles")
|
|
153
|
+
.where("expires_at", "<=", now.toISOString())
|
|
154
|
+
.execute();
|
|
155
|
+
await trx
|
|
156
|
+
.insertInto("identity_throttles")
|
|
157
|
+
.values({
|
|
158
|
+
key,
|
|
159
|
+
attempts: 1,
|
|
160
|
+
expires_at: new Date(now.getTime() + 15 * 60 * 1000).toISOString(),
|
|
161
|
+
})
|
|
162
|
+
.onConflict((c) => c.column("key").doUpdateSet({ attempts: sql `attempts + 1` }))
|
|
163
|
+
.execute();
|
|
164
|
+
const row = await trx
|
|
165
|
+
.selectFrom("identity_throttles")
|
|
166
|
+
.select("attempts")
|
|
167
|
+
.where("key", "=", key)
|
|
168
|
+
.executeTakeFirstOrThrow();
|
|
169
|
+
return Number(row.attempts);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
async clearThrottle(key) {
|
|
173
|
+
await this.db
|
|
174
|
+
.deleteFrom("identity_throttles")
|
|
175
|
+
.where("key", "=", key)
|
|
176
|
+
.execute();
|
|
177
|
+
}
|
|
178
|
+
async changePassword(userId, expectedHash, passwordHash) {
|
|
179
|
+
return this.db.transaction().execute(async (trx) => {
|
|
180
|
+
checkIdentityRequest();
|
|
181
|
+
const result = await trx
|
|
182
|
+
.updateTable("identity_users")
|
|
183
|
+
.set({ password_hash: passwordHash })
|
|
184
|
+
.where("id", "=", userId)
|
|
185
|
+
.where("password_hash", "=", expectedHash)
|
|
186
|
+
.executeTakeFirst();
|
|
187
|
+
if (result.numUpdatedRows !== 1n)
|
|
188
|
+
return false;
|
|
189
|
+
await trx
|
|
190
|
+
.deleteFrom("identity_sessions")
|
|
191
|
+
.where("user_id", "=", userId)
|
|
192
|
+
.execute();
|
|
193
|
+
return true;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { IdentityError } from "./identity.error.js";
|
|
3
|
+
const requestSignals = new AsyncLocalStorage();
|
|
4
|
+
export function runIdentityRequest(signal, work) {
|
|
5
|
+
return signal ? requestSignals.run(signal, work) : work();
|
|
6
|
+
}
|
|
7
|
+
export function checkIdentityRequest() {
|
|
8
|
+
if (requestSignals.getStore()?.aborted)
|
|
9
|
+
throw new IdentityError(408, "The request expired. Try again.");
|
|
10
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const identityRolesMigration = {
|
|
2
|
+
async up(db) {
|
|
3
|
+
await db.schema
|
|
4
|
+
.createTable("identity_custom_roles")
|
|
5
|
+
.addColumn("id", "text", (c) => c.primaryKey())
|
|
6
|
+
.addColumn("app_id", "text", (c) => c.notNull())
|
|
7
|
+
.addColumn("tenant_id", "text", (c) => c.notNull().references("identity_tenants.id"))
|
|
8
|
+
.addColumn("name", "text", (c) => c.notNull())
|
|
9
|
+
.addColumn("active", "integer", (c) => c.notNull().defaultTo(1))
|
|
10
|
+
.addColumn("version", "integer", (c) => c.notNull().defaultTo(0))
|
|
11
|
+
.addUniqueConstraint("identity_custom_role_name", [
|
|
12
|
+
"app_id",
|
|
13
|
+
"tenant_id",
|
|
14
|
+
"name",
|
|
15
|
+
])
|
|
16
|
+
.execute();
|
|
17
|
+
await db.schema
|
|
18
|
+
.createTable("identity_custom_role_permissions")
|
|
19
|
+
.addColumn("role_id", "text", (c) => c.notNull().references("identity_custom_roles.id").onDelete("cascade"))
|
|
20
|
+
.addColumn("permission_id", "text", (c) => c.notNull().references("identity_permissions.id"))
|
|
21
|
+
.addPrimaryKeyConstraint("identity_custom_role_permission_pk", [
|
|
22
|
+
"role_id",
|
|
23
|
+
"permission_id",
|
|
24
|
+
])
|
|
25
|
+
.execute();
|
|
26
|
+
await db.schema
|
|
27
|
+
.alterTable("identity_memberships")
|
|
28
|
+
.addColumn("custom_role_id", "text", (c) => c.references("identity_custom_roles.id"))
|
|
29
|
+
.execute();
|
|
30
|
+
await db.schema
|
|
31
|
+
.alterTable("identity_memberships")
|
|
32
|
+
.addColumn("active", "integer", (c) => c.notNull().defaultTo(1))
|
|
33
|
+
.execute();
|
|
34
|
+
await db.schema
|
|
35
|
+
.alterTable("identity_memberships")
|
|
36
|
+
.addColumn("version", "integer", (c) => c.notNull().defaultTo(0))
|
|
37
|
+
.execute();
|
|
38
|
+
},
|
|
39
|
+
async down(db) {
|
|
40
|
+
const customRole = await db
|
|
41
|
+
.selectFrom("identity_custom_roles")
|
|
42
|
+
.select("id")
|
|
43
|
+
.limit(1)
|
|
44
|
+
.executeTakeFirst();
|
|
45
|
+
const restrictedMembership = await db
|
|
46
|
+
.selectFrom("identity_memberships")
|
|
47
|
+
.select("user_id")
|
|
48
|
+
.where((eb) => eb.or([
|
|
49
|
+
eb("custom_role_id", "is not", null),
|
|
50
|
+
eb("active", "!=", 1),
|
|
51
|
+
]))
|
|
52
|
+
.limit(1)
|
|
53
|
+
.executeTakeFirst();
|
|
54
|
+
if (customRole || restrictedMembership)
|
|
55
|
+
throw new Error("Role rollback would remove access restrictions. Restore a compatible database snapshot instead.");
|
|
56
|
+
for (const column of ["custom_role_id", "active", "version"])
|
|
57
|
+
await db.schema
|
|
58
|
+
.alterTable("identity_memberships")
|
|
59
|
+
.dropColumn(column)
|
|
60
|
+
.execute();
|
|
61
|
+
await db.schema.dropTable("identity_custom_role_permissions").execute();
|
|
62
|
+
await db.schema.dropTable("identity_custom_roles").execute();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
|
+
import type { IdentitySchema, Portal, Principal } from "./identity.types.js";
|
|
3
|
+
export declare class IdentityRolesService {
|
|
4
|
+
private readonly db;
|
|
5
|
+
constructor(db: Kysely<IdentitySchema>);
|
|
6
|
+
resolve(database: Kysely<IdentitySchema>, actor: Principal, roleId: string, tenantId: string): Promise<{
|
|
7
|
+
portal: Portal;
|
|
8
|
+
customRoleId: null;
|
|
9
|
+
} | {
|
|
10
|
+
portal: "user";
|
|
11
|
+
customRoleId: string;
|
|
12
|
+
}>;
|
|
13
|
+
create(actor: Principal, raw: unknown): Promise<{
|
|
14
|
+
id: `${string}-${string}-${string}-${string}-${string}`;
|
|
15
|
+
name: string;
|
|
16
|
+
tenantId: string;
|
|
17
|
+
portal: string;
|
|
18
|
+
system: boolean;
|
|
19
|
+
active: boolean;
|
|
20
|
+
version: number;
|
|
21
|
+
permissionIds: string[];
|
|
22
|
+
}>;
|
|
23
|
+
update(actor: Principal, id: string, raw: unknown): Promise<{
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
tenantId: string;
|
|
27
|
+
portal: string;
|
|
28
|
+
system: boolean;
|
|
29
|
+
active: boolean;
|
|
30
|
+
version: number;
|
|
31
|
+
permissionIds: string[];
|
|
32
|
+
}>;
|
|
33
|
+
updateMembership(actor: Principal, id: string, raw: unknown): Promise<{
|
|
34
|
+
id: string;
|
|
35
|
+
userId: string;
|
|
36
|
+
tenantId: string;
|
|
37
|
+
roleId: string;
|
|
38
|
+
portal: Portal;
|
|
39
|
+
active: boolean;
|
|
40
|
+
version: number;
|
|
41
|
+
}>;
|
|
42
|
+
removeMembership(actor: Principal, id: string, expectedVersion?: number): Promise<void>;
|
|
43
|
+
private manage;
|
|
44
|
+
private scope;
|
|
45
|
+
private permissions;
|
|
46
|
+
private uniqueName;
|
|
47
|
+
private replacePermissions;
|
|
48
|
+
private audit;
|
|
49
|
+
}
|