@m5kdev/backend 0.6.0 → 0.7.0
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/dist/src/modules/ai/ai.service.d.ts +11 -13
- package/dist/src/modules/ai/ai.service.js +6 -6
- package/dist/src/modules/ai/ai.trpc.d.ts +1 -1
- package/dist/src/modules/auth/auth.lib.d.ts +4 -8
- package/dist/src/modules/auth/auth.lib.js +2 -2
- package/dist/src/modules/auth/auth.service.d.ts +17 -47
- package/dist/src/modules/auth/auth.service.js +79 -66
- package/dist/src/modules/auth/auth.trpc.d.ts +1 -1
- package/dist/src/modules/base/base.actor.d.ts +68 -0
- package/dist/src/modules/base/base.actor.js +99 -0
- package/dist/src/modules/base/base.actor.test.d.ts +1 -0
- package/dist/src/modules/base/base.actor.test.js +58 -0
- package/dist/src/modules/base/base.grants.d.ts +3 -7
- package/dist/src/modules/base/base.grants.js +22 -10
- package/dist/src/modules/base/base.grants.test.js +16 -45
- package/dist/src/modules/base/base.procedure.d.ts +17 -20
- package/dist/src/modules/base/base.procedure.js +36 -24
- package/dist/src/modules/base/base.service.d.ts +7 -19
- package/dist/src/modules/base/base.service.js +19 -12
- package/dist/src/modules/base/base.service.test.js +89 -61
- package/dist/src/modules/billing/billing.service.d.ts +4 -25
- package/dist/src/modules/billing/billing.service.js +6 -6
- package/dist/src/modules/billing/billing.trpc.d.ts +2 -2
- package/dist/src/modules/billing/billing.trpc.js +4 -6
- package/dist/src/modules/connect/connect.service.d.ts +19 -11
- package/dist/src/modules/connect/connect.service.js +10 -8
- package/dist/src/modules/connect/connect.trpc.d.ts +2 -2
- package/dist/src/modules/recurrence/recurrence.service.d.ts +36 -6
- package/dist/src/modules/recurrence/recurrence.service.js +13 -10
- package/dist/src/modules/recurrence/recurrence.trpc.d.ts +1 -1
- package/dist/src/modules/social/social.service.d.ts +3 -4
- package/dist/src/modules/social/social.service.js +3 -3
- package/dist/src/modules/tag/tag.service.d.ts +16 -12
- package/dist/src/modules/tag/tag.service.js +4 -4
- package/dist/src/modules/tag/tag.trpc.d.ts +1 -1
- package/dist/src/modules/workflow/workflow.service.d.ts +48 -8
- package/dist/src/modules/workflow/workflow.service.js +6 -6
- package/dist/src/modules/workflow/workflow.trpc.d.ts +2 -2
- package/dist/src/types.d.ts +4 -4
- package/dist/src/utils/trpc.d.ts +31 -41
- package/dist/src/utils/trpc.js +95 -0
- package/dist/src/utils/trpc.test.d.ts +1 -0
- package/dist/src/utils/trpc.test.js +154 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createActorFromContext = createActorFromContext;
|
|
4
|
+
exports.validateActor = validateActor;
|
|
5
|
+
exports.createServiceActor = createServiceActor;
|
|
6
|
+
exports.getServiceActorScope = getServiceActorScope;
|
|
7
|
+
exports.hasServiceActorScope = hasServiceActorScope;
|
|
8
|
+
const errors_1 = require("../../utils/errors");
|
|
9
|
+
function createActorFromContext(context, scope) {
|
|
10
|
+
if (!context.user.role) {
|
|
11
|
+
throw new errors_1.ServerError({
|
|
12
|
+
code: "BAD_REQUEST",
|
|
13
|
+
message: "User role not found in context",
|
|
14
|
+
layer: "controller",
|
|
15
|
+
layerName: "ActorValidation",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (scope === "organization") {
|
|
19
|
+
if (!context.session.activeOrganizationId || !context.session.activeOrganizationRole) {
|
|
20
|
+
throw new errors_1.ServerError({
|
|
21
|
+
code: "FORBIDDEN",
|
|
22
|
+
message: "Active organization context required",
|
|
23
|
+
layer: "controller",
|
|
24
|
+
layerName: "ActorValidation",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (scope === "team") {
|
|
29
|
+
if (!context.session.activeOrganizationId || !context.session.activeOrganizationRole) {
|
|
30
|
+
throw new errors_1.ServerError({
|
|
31
|
+
code: "FORBIDDEN",
|
|
32
|
+
message: "Active organization context required for team scope",
|
|
33
|
+
layer: "controller",
|
|
34
|
+
layerName: "ActorValidation",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (!context.session.activeTeamId || !context.session.activeTeamRole) {
|
|
38
|
+
throw new errors_1.ServerError({
|
|
39
|
+
code: "FORBIDDEN",
|
|
40
|
+
message: "Active team context required",
|
|
41
|
+
layer: "controller",
|
|
42
|
+
layerName: "ActorValidation",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
userId: context.user.id,
|
|
48
|
+
userRole: context.user.role,
|
|
49
|
+
organizationId: context.session.activeOrganizationId,
|
|
50
|
+
organizationRole: context.session.activeOrganizationRole,
|
|
51
|
+
teamId: context.session.activeTeamId,
|
|
52
|
+
teamRole: context.session.activeTeamRole,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function validateActor(actor, scope) {
|
|
56
|
+
if (!actor.userId || !actor.userRole)
|
|
57
|
+
return false;
|
|
58
|
+
if (scope === "user")
|
|
59
|
+
return true;
|
|
60
|
+
if (scope === "organization") {
|
|
61
|
+
return Boolean(actor.organizationId && actor.organizationRole);
|
|
62
|
+
}
|
|
63
|
+
return Boolean(actor.organizationId && actor.organizationRole && actor.teamId && actor.teamRole);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Builds a flat actor for tests / grants without session. Validates that team scope implies organization.
|
|
67
|
+
*/
|
|
68
|
+
function createServiceActor(claims) {
|
|
69
|
+
const organizationId = claims.organizationId ?? null;
|
|
70
|
+
const organizationRole = claims.organizationRole ?? null;
|
|
71
|
+
const teamId = claims.teamId ?? null;
|
|
72
|
+
const teamRole = claims.teamRole ?? null;
|
|
73
|
+
if ((teamId || teamRole) && (!organizationId || !organizationRole)) {
|
|
74
|
+
throw new Error("organization access before team access");
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
userId: claims.userId,
|
|
78
|
+
userRole: claims.userRole,
|
|
79
|
+
organizationId,
|
|
80
|
+
organizationRole,
|
|
81
|
+
teamId,
|
|
82
|
+
teamRole,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function getServiceActorScope(actor) {
|
|
86
|
+
if (validateActor(actor, "team"))
|
|
87
|
+
return "team";
|
|
88
|
+
if (validateActor(actor, "organization"))
|
|
89
|
+
return "organization";
|
|
90
|
+
return "user";
|
|
91
|
+
}
|
|
92
|
+
function hasServiceActorScope(actor, scope) {
|
|
93
|
+
if (scope === "team")
|
|
94
|
+
return validateActor(actor, "team");
|
|
95
|
+
if (scope === "organization") {
|
|
96
|
+
return validateActor(actor, "organization") || validateActor(actor, "team");
|
|
97
|
+
}
|
|
98
|
+
return validateActor(actor, "user");
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const base_actor_1 = require("./base.actor");
|
|
4
|
+
describe("base.actor", () => {
|
|
5
|
+
it("creates a user actor when only user claims are present", () => {
|
|
6
|
+
const actor = (0, base_actor_1.createServiceActor)({
|
|
7
|
+
userId: "user-1",
|
|
8
|
+
userRole: "member",
|
|
9
|
+
});
|
|
10
|
+
expect(actor).toEqual({
|
|
11
|
+
userId: "user-1",
|
|
12
|
+
userRole: "member",
|
|
13
|
+
organizationId: null,
|
|
14
|
+
organizationRole: null,
|
|
15
|
+
teamId: null,
|
|
16
|
+
teamRole: null,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
it("derives the highest available scope", () => {
|
|
20
|
+
expect((0, base_actor_1.getServiceActorScope)({
|
|
21
|
+
userId: "user-1",
|
|
22
|
+
userRole: "member",
|
|
23
|
+
organizationId: "org-1",
|
|
24
|
+
organizationRole: "owner",
|
|
25
|
+
teamId: null,
|
|
26
|
+
teamRole: null,
|
|
27
|
+
})).toBe("organization");
|
|
28
|
+
expect((0, base_actor_1.getServiceActorScope)({
|
|
29
|
+
userId: "user-1",
|
|
30
|
+
userRole: "member",
|
|
31
|
+
organizationId: "org-1",
|
|
32
|
+
organizationRole: "owner",
|
|
33
|
+
teamId: "team-1",
|
|
34
|
+
teamRole: "manager",
|
|
35
|
+
})).toBe("team");
|
|
36
|
+
});
|
|
37
|
+
it("validates hierarchy before building a team actor", () => {
|
|
38
|
+
expect(() => (0, base_actor_1.createServiceActor)({
|
|
39
|
+
userId: "user-1",
|
|
40
|
+
userRole: "member",
|
|
41
|
+
teamId: "team-1",
|
|
42
|
+
teamRole: "manager",
|
|
43
|
+
})).toThrow("organization access before team access");
|
|
44
|
+
});
|
|
45
|
+
it("checks required scope against broader actors", () => {
|
|
46
|
+
const actor = (0, base_actor_1.createServiceActor)({
|
|
47
|
+
userId: "user-1",
|
|
48
|
+
userRole: "member",
|
|
49
|
+
organizationId: "org-1",
|
|
50
|
+
organizationRole: "owner",
|
|
51
|
+
teamId: "team-1",
|
|
52
|
+
teamRole: "manager",
|
|
53
|
+
});
|
|
54
|
+
expect((0, base_actor_1.hasServiceActorScope)(actor, "user")).toBe(true);
|
|
55
|
+
expect((0, base_actor_1.hasServiceActorScope)(actor, "organization")).toBe(true);
|
|
56
|
+
expect((0, base_actor_1.hasServiceActorScope)(actor, "team")).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ServiceActor } from "./base.actor";
|
|
2
2
|
import type { ServerResultAsync } from "./base.dto";
|
|
3
3
|
type Level = "user" | "team" | "organization";
|
|
4
4
|
type Access = "all" | "own";
|
|
@@ -19,10 +19,6 @@ export type NestedGrants = Record<string, Partial<Record<Level, Record<string, R
|
|
|
19
19
|
export type ResourceGrant = Omit<Grant, "resource">;
|
|
20
20
|
export type ResourceActionGrant = Omit<ResourceGrant, "action">;
|
|
21
21
|
export declare function flattenNestedGrants(nestedGrants: NestedGrants): Grant[];
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
user: User;
|
|
25
|
-
}
|
|
26
|
-
export declare function checkPermissionSync<T extends Entity>(ctx: PermissionContext, grants: ResourceActionGrant[], entities?: T | T[]): boolean;
|
|
27
|
-
export declare function checkPermissionAsync<T extends Entity>(ctx: PermissionContext, grants: ResourceActionGrant[], getEntities: () => ServerResultAsync<T | T[] | undefined>): ServerResultAsync<boolean>;
|
|
22
|
+
export declare function checkPermissionSync<T extends Entity>(actor: ServiceActor, grants: ResourceActionGrant[], entities?: T | T[]): boolean;
|
|
23
|
+
export declare function checkPermissionAsync<T extends Entity>(actor: ServiceActor, grants: ResourceActionGrant[], getEntities: () => ServerResultAsync<T | T[] | undefined>): ServerResultAsync<boolean>;
|
|
28
24
|
export {};
|
|
@@ -92,26 +92,38 @@ function checkOwnAccess(grants, roles, contextValues, entities) {
|
|
|
92
92
|
}
|
|
93
93
|
return false;
|
|
94
94
|
}
|
|
95
|
-
function checkPermissionSync(
|
|
95
|
+
function checkPermissionSync(actor, grants, entities) {
|
|
96
96
|
if (!grants || grants.length === 0)
|
|
97
97
|
return false;
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
const roles = {
|
|
99
|
+
userRole: actor.userRole,
|
|
100
|
+
teamRole: actor.teamRole,
|
|
101
|
+
organizationRole: actor.organizationRole,
|
|
102
|
+
};
|
|
103
|
+
const contextValues = {
|
|
104
|
+
userId: actor.userId,
|
|
105
|
+
teamId: actor.teamId,
|
|
106
|
+
organizationId: actor.organizationId,
|
|
107
|
+
};
|
|
102
108
|
// Pass 1: Check for "all" access first (no ownership check needed)
|
|
103
109
|
if (hasAllAccess(grants, roles))
|
|
104
110
|
return true;
|
|
105
111
|
// Pass 2: Check "own" access with ownership validation
|
|
106
112
|
return checkOwnAccess(grants, roles, contextValues, entities);
|
|
107
113
|
}
|
|
108
|
-
async function checkPermissionAsync(
|
|
114
|
+
async function checkPermissionAsync(actor, grants, getEntities) {
|
|
109
115
|
if (!grants || grants.length === 0)
|
|
110
116
|
return (0, neverthrow_1.ok)(false);
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
const roles = {
|
|
118
|
+
userRole: actor.userRole,
|
|
119
|
+
teamRole: actor.teamRole,
|
|
120
|
+
organizationRole: actor.organizationRole,
|
|
121
|
+
};
|
|
122
|
+
const contextValues = {
|
|
123
|
+
userId: actor.userId,
|
|
124
|
+
teamId: actor.teamId,
|
|
125
|
+
organizationId: actor.organizationId,
|
|
126
|
+
};
|
|
115
127
|
// Pass 1: Check for "all" access first (no entity fetch needed)
|
|
116
128
|
if (hasAllAccess(grants, roles))
|
|
117
129
|
return (0, neverthrow_1.ok)(true);
|
|
@@ -1,53 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const neverthrow_1 = require("neverthrow");
|
|
4
|
-
const base_grants_1 = require("./base.grants");
|
|
5
4
|
const errors_1 = require("../../utils/errors");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// ============================================
|
|
9
|
-
function createMockUser(overrides = {}) {
|
|
10
|
-
return {
|
|
11
|
-
id: "user-123",
|
|
12
|
-
role: "member",
|
|
13
|
-
email: "test@example.com",
|
|
14
|
-
emailVerified: true,
|
|
15
|
-
name: "Test User",
|
|
16
|
-
createdAt: new Date(),
|
|
17
|
-
updatedAt: new Date(),
|
|
18
|
-
image: null,
|
|
19
|
-
onboarding: null,
|
|
20
|
-
preferences: null,
|
|
21
|
-
flags: null,
|
|
22
|
-
stripeCustomerId: null,
|
|
23
|
-
paymentCustomerId: null,
|
|
24
|
-
paymentPlanTier: null,
|
|
25
|
-
paymentPlanExpiresAt: null,
|
|
26
|
-
...overrides,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
function createMockSession(overrides = {}) {
|
|
30
|
-
return {
|
|
31
|
-
id: "session-123",
|
|
32
|
-
userId: "user-123",
|
|
33
|
-
token: "token-123",
|
|
34
|
-
expiresAt: new Date(Date.now() + 86400000),
|
|
35
|
-
createdAt: new Date(),
|
|
36
|
-
updatedAt: new Date(),
|
|
37
|
-
ipAddress: null,
|
|
38
|
-
userAgent: null,
|
|
39
|
-
activeOrganizationId: null,
|
|
40
|
-
activeTeamId: null,
|
|
41
|
-
activeOrganizationRole: null,
|
|
42
|
-
activeTeamRole: null,
|
|
43
|
-
...overrides,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
5
|
+
const base_actor_1 = require("./base.actor");
|
|
6
|
+
const base_grants_1 = require("./base.grants");
|
|
46
7
|
function createMockContext(userOverrides = {}, sessionOverrides = {}) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
8
|
+
const organizationId = sessionOverrides.activeOrganizationId ?? (sessionOverrides.activeTeamId ? "org-123" : null);
|
|
9
|
+
const organizationRole = sessionOverrides.activeOrganizationRole ?? (sessionOverrides.activeTeamRole ? "member" : null);
|
|
10
|
+
const actor = (0, base_actor_1.createServiceActor)({
|
|
11
|
+
userId: userOverrides.id ?? "user-123",
|
|
12
|
+
userRole: userOverrides.role ?? "member",
|
|
13
|
+
organizationId,
|
|
14
|
+
organizationRole,
|
|
15
|
+
teamId: sessionOverrides.activeTeamId ?? null,
|
|
16
|
+
teamRole: sessionOverrides.activeTeamRole ?? null,
|
|
17
|
+
});
|
|
18
|
+
if (!actor) {
|
|
19
|
+
throw new Error("Expected actor");
|
|
20
|
+
}
|
|
21
|
+
return actor;
|
|
51
22
|
}
|
|
52
23
|
function createMockEntity(overrides = {}) {
|
|
53
24
|
return {
|
|
@@ -2,22 +2,25 @@ import type { QueryInput } from "@m5kdev/commons/modules/schemas/query.schema";
|
|
|
2
2
|
import type { TRPC_ERROR_CODE_KEY } from "@trpc/server";
|
|
3
3
|
import type { ServerError } from "../../utils/errors";
|
|
4
4
|
import type { logger } from "../../utils/logger";
|
|
5
|
-
import type { Context, Session, User } from "../auth/auth.lib";
|
|
6
5
|
import type { Base } from "./base.abstract";
|
|
6
|
+
import { type Actor, type ActorScope, type AuthenticatedActor } from "./base.actor";
|
|
7
7
|
import type { ServerResult, ServerResultAsync } from "./base.dto";
|
|
8
8
|
import type { Entity, ResourceActionGrant } from "./base.grants";
|
|
9
9
|
type ServiceLogger = ReturnType<typeof logger.child>;
|
|
10
10
|
type RepositoryMap = Record<string, Base>;
|
|
11
11
|
type ServiceMap = Record<string, Base>;
|
|
12
12
|
export type ServiceProcedureContext = {
|
|
13
|
-
|
|
14
|
-
session?: Session | null;
|
|
13
|
+
actor?: AuthenticatedActor | null;
|
|
15
14
|
} & Record<string, unknown>;
|
|
16
15
|
export type ServiceProcedureState = Record<string, unknown>;
|
|
17
16
|
export type ServiceProcedureStoredValue<T> = [T] extends [undefined] ? undefined : Awaited<T>;
|
|
18
17
|
export type ServiceProcedureResultLike<T> = T | ServerResult<T> | Promise<T | ServerResult<T>>;
|
|
19
|
-
export type ServiceProcedureContextFilterScope =
|
|
18
|
+
export type ServiceProcedureContextFilterScope = ActorScope;
|
|
20
19
|
export type ServiceProcedureContextFilteredInput<TInput> = Extract<NonNullable<TInput>, QueryInput>;
|
|
20
|
+
type ServiceProcedureAuthContext<Scope extends ActorScope> = {
|
|
21
|
+
actor: Actor[Scope];
|
|
22
|
+
};
|
|
23
|
+
type ServiceProcedureRequiredScopeFromFilter<TInclude extends readonly ServiceProcedureContextFilterScope[] | undefined> = TInclude extends readonly ServiceProcedureContextFilterScope[] ? "team" extends TInclude[number] ? "team" : "organization" extends TInclude[number] ? "organization" : "user" : "user";
|
|
21
24
|
export type ServiceProcedure<TInput, TCtx extends ServiceProcedureContext, TOutput> = (input: TInput, ctx: TCtx) => ServerResultAsync<TOutput>;
|
|
22
25
|
export type ServiceProcedureArgs<TInput, TCtx extends ServiceProcedureContext, Repositories extends RepositoryMap, Services extends ServiceMap, State extends ServiceProcedureState> = {
|
|
23
26
|
input: TInput;
|
|
@@ -50,24 +53,24 @@ export type ServiceProcedureAccessConfig<TInput, TCtx extends ServiceProcedureCo
|
|
|
50
53
|
export interface ServiceProcedureBuilder<TInput, TCtx extends ServiceProcedureContext, Repositories extends RepositoryMap, Services extends ServiceMap, State extends ServiceProcedureState = Record<string, never>> {
|
|
51
54
|
use<StepName extends string, TOutput = void>(stepName: StepName, step: ServiceProcedureStep<TInput, TCtx, Repositories, Services, State, TOutput>): ServiceProcedureBuilder<TInput, TCtx, Repositories, Services, State & Record<StepName, ServiceProcedureStoredValue<TOutput>>>;
|
|
52
55
|
mapInput<StepName extends string, TNextInput>(stepName: StepName, step: ServiceProcedureInputMapper<TInput, TCtx, Repositories, Services, State, TNextInput>): ServiceProcedureBuilder<ServiceProcedureStoredValue<TNextInput>, TCtx, Repositories, Services, State & Record<StepName, ServiceProcedureStoredValue<TNextInput>>>;
|
|
53
|
-
addContextFilter
|
|
56
|
+
addContextFilter<TInclude extends readonly ServiceProcedureContextFilterScope[] | undefined = undefined>(include?: TInclude): ServiceProcedureBuilder<ServiceProcedureContextFilteredInput<TInput>, TCtx & ServiceProcedureAuthContext<ServiceProcedureRequiredScopeFromFilter<TInclude>>, Repositories, Services, State & {
|
|
54
57
|
contextFilter: ServiceProcedureContextFilteredInput<TInput>;
|
|
55
58
|
}>;
|
|
56
|
-
requireAuth(): ServiceProcedureBuilder<TInput, TCtx &
|
|
59
|
+
requireAuth<Scope extends ActorScope = "user">(scope?: Scope): ServiceProcedureBuilder<TInput, TCtx & ServiceProcedureAuthContext<Scope>, Repositories, Services, State>;
|
|
57
60
|
handle<TOutput>(handler: ServiceProcedureHandler<TInput, TCtx, Repositories, Services, State, TOutput>): ServiceProcedure<TInput, TCtx, TOutput>;
|
|
58
61
|
}
|
|
59
62
|
export interface PermissionServiceProcedureBuilder<TInput, TCtx extends ServiceProcedureContext, Repositories extends RepositoryMap, Services extends ServiceMap, State extends ServiceProcedureState = Record<string, never>> extends ServiceProcedureBuilder<TInput, TCtx, Repositories, Services, State> {
|
|
60
63
|
use<StepName extends string, TOutput = void>(stepName: StepName, step: ServiceProcedureStep<TInput, TCtx, Repositories, Services, State, TOutput>): PermissionServiceProcedureBuilder<TInput, TCtx, Repositories, Services, State & Record<StepName, ServiceProcedureStoredValue<TOutput>>>;
|
|
61
64
|
mapInput<StepName extends string, TNextInput>(stepName: StepName, step: ServiceProcedureInputMapper<TInput, TCtx, Repositories, Services, State, TNextInput>): PermissionServiceProcedureBuilder<ServiceProcedureStoredValue<TNextInput>, TCtx, Repositories, Services, State & Record<StepName, ServiceProcedureStoredValue<TNextInput>>>;
|
|
62
|
-
addContextFilter
|
|
65
|
+
addContextFilter<TInclude extends readonly ServiceProcedureContextFilterScope[] | undefined = undefined>(include?: TInclude): PermissionServiceProcedureBuilder<ServiceProcedureContextFilteredInput<TInput>, TCtx & ServiceProcedureAuthContext<ServiceProcedureRequiredScopeFromFilter<TInclude>>, Repositories, Services, State & {
|
|
63
66
|
contextFilter: ServiceProcedureContextFilteredInput<TInput>;
|
|
64
67
|
}>;
|
|
65
|
-
requireAuth(): PermissionServiceProcedureBuilder<TInput, TCtx &
|
|
66
|
-
access(config: ServiceProcedureAccessEntitiesConfig<TInput, TCtx, Repositories, Services, State>): PermissionServiceProcedureBuilder<TInput, TCtx &
|
|
67
|
-
access<TEntities extends Entity | Entity[] | undefined>(config: ServiceProcedureAccessEntitiesConfig<TInput, TCtx, Repositories, Services, State, TEntities>): PermissionServiceProcedureBuilder<TInput, TCtx &
|
|
68
|
+
requireAuth<Scope extends ActorScope = "user">(scope?: Scope): PermissionServiceProcedureBuilder<TInput, TCtx & ServiceProcedureAuthContext<Scope>, Repositories, Services, State>;
|
|
69
|
+
access(config: ServiceProcedureAccessEntitiesConfig<TInput, TCtx, Repositories, Services, State>): PermissionServiceProcedureBuilder<TInput, TCtx & ServiceProcedureAuthContext<"user">, Repositories, Services, State>;
|
|
70
|
+
access<TEntities extends Entity | Entity[] | undefined>(config: ServiceProcedureAccessEntitiesConfig<TInput, TCtx, Repositories, Services, State, TEntities>): PermissionServiceProcedureBuilder<TInput, TCtx & ServiceProcedureAuthContext<"user">, Repositories, Services, State & {
|
|
68
71
|
access: TEntities;
|
|
69
72
|
}>;
|
|
70
|
-
access<StepName extends ServiceProcedureEntityStepName<State>>(config: ServiceProcedureAccessStateConfig<State, StepName>): PermissionServiceProcedureBuilder<TInput, TCtx &
|
|
73
|
+
access<StepName extends ServiceProcedureEntityStepName<State>>(config: ServiceProcedureAccessStateConfig<State, StepName>): PermissionServiceProcedureBuilder<TInput, TCtx & ServiceProcedureAuthContext<"user">, Repositories, Services, State & {
|
|
71
74
|
access: State[StepName];
|
|
72
75
|
}>;
|
|
73
76
|
}
|
|
@@ -75,7 +78,7 @@ type BaseServiceProcedureHost<Repositories extends RepositoryMap, Services exten
|
|
|
75
78
|
repository: Repositories;
|
|
76
79
|
service: Services;
|
|
77
80
|
logger: ServiceLogger;
|
|
78
|
-
addContextFilter(
|
|
81
|
+
addContextFilter(actor: AuthenticatedActor, include?: {
|
|
79
82
|
user?: boolean;
|
|
80
83
|
organization?: boolean;
|
|
81
84
|
team?: boolean;
|
|
@@ -89,14 +92,8 @@ type BaseServiceProcedureHost<Repositories extends RepositoryMap, Services exten
|
|
|
89
92
|
handleUnknownError(error: unknown): ServerError;
|
|
90
93
|
};
|
|
91
94
|
type PermissionServiceProcedureHost<Repositories extends RepositoryMap, Services extends ServiceMap> = BaseServiceProcedureHost<Repositories, Services> & {
|
|
92
|
-
checkPermission<T extends Entity>(
|
|
93
|
-
|
|
94
|
-
user: User;
|
|
95
|
-
}, action: string, entities?: T | T[], grants?: ResourceActionGrant[]): boolean;
|
|
96
|
-
checkPermissionAsync<T extends Entity>(ctx: {
|
|
97
|
-
session: Session;
|
|
98
|
-
user: User;
|
|
99
|
-
}, action: string, getEntities: () => ServerResultAsync<T | T[] | undefined>, grants?: ResourceActionGrant[]): ServerResultAsync<boolean>;
|
|
95
|
+
checkPermission<T extends Entity>(actor: AuthenticatedActor, action: string, entities?: T | T[], grants?: ResourceActionGrant[]): boolean;
|
|
96
|
+
checkPermissionAsync<T extends Entity>(actor: AuthenticatedActor, action: string, getEntities: () => ServerResultAsync<T | T[] | undefined>, grants?: ResourceActionGrant[]): ServerResultAsync<boolean>;
|
|
100
97
|
};
|
|
101
98
|
type ProcedureRuntimeStep<Repositories extends RepositoryMap, Services extends ServiceMap> = {
|
|
102
99
|
stage: "use" | "input" | "auth" | "access";
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createServiceProcedureBuilder = createServiceProcedureBuilder;
|
|
4
4
|
exports.createPermissionServiceProcedureBuilder = createPermissionServiceProcedureBuilder;
|
|
5
5
|
const neverthrow_1 = require("neverthrow");
|
|
6
|
+
const base_actor_1 = require("./base.actor");
|
|
6
7
|
const DEFAULT_CONTEXT_FILTER_INCLUDE = [
|
|
7
8
|
"user",
|
|
8
9
|
];
|
|
@@ -43,19 +44,24 @@ function logProcedureStage(host, procedureName, ctx, stage, { stepName, duration
|
|
|
43
44
|
stepName,
|
|
44
45
|
durationMs,
|
|
45
46
|
errorCode,
|
|
46
|
-
|
|
47
|
-
hasSession: Boolean(ctx.session),
|
|
47
|
+
hasActor: Boolean(ctx.actor),
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
|
-
function
|
|
50
|
+
function requireProcedureActor(host, ctx, scope) {
|
|
51
|
+
if (!ctx.actor) {
|
|
52
|
+
return host.error("UNAUTHORIZED", "Unauthorized");
|
|
53
|
+
}
|
|
54
|
+
if (!(0, base_actor_1.validateActor)(ctx.actor, scope)) {
|
|
55
|
+
return host.error("FORBIDDEN", "Forbidden");
|
|
56
|
+
}
|
|
57
|
+
return (0, neverthrow_1.ok)(ctx.actor);
|
|
58
|
+
}
|
|
59
|
+
function createRequireAuthStep(host, scope = "user") {
|
|
51
60
|
return {
|
|
52
61
|
stage: "auth",
|
|
53
62
|
stepName: "auth",
|
|
54
63
|
run: async ({ ctx }) => {
|
|
55
|
-
|
|
56
|
-
return host.error("UNAUTHORIZED", "Unauthorized");
|
|
57
|
-
}
|
|
58
|
-
return (0, neverthrow_1.ok)(true);
|
|
64
|
+
return requireProcedureActor(host, ctx, scope);
|
|
59
65
|
},
|
|
60
66
|
};
|
|
61
67
|
}
|
|
@@ -75,10 +81,20 @@ function createInputStep(stepName, step) {
|
|
|
75
81
|
}
|
|
76
82
|
function createContextFilterStep(host, include) {
|
|
77
83
|
const contextInclude = getContextFilterInclude(include);
|
|
84
|
+
const requiredScope = contextInclude.team
|
|
85
|
+
? "team"
|
|
86
|
+
: contextInclude.organization
|
|
87
|
+
? "organization"
|
|
88
|
+
: "user";
|
|
78
89
|
return {
|
|
79
90
|
stage: "input",
|
|
80
91
|
stepName: "contextFilter",
|
|
81
|
-
run: async ({ input, ctx }) =>
|
|
92
|
+
run: async ({ input, ctx }) => {
|
|
93
|
+
const actor = requireProcedureActor(host, ctx, requiredScope);
|
|
94
|
+
if (actor.isErr())
|
|
95
|
+
return actor;
|
|
96
|
+
return (0, neverthrow_1.ok)(host.addContextFilter(actor.value, contextInclude, input));
|
|
97
|
+
},
|
|
82
98
|
};
|
|
83
99
|
}
|
|
84
100
|
function createAccessStep(host, config) {
|
|
@@ -87,16 +103,12 @@ function createAccessStep(host, config) {
|
|
|
87
103
|
stepName: "access",
|
|
88
104
|
run: async (args) => {
|
|
89
105
|
const typedArgs = args;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const permissionContext = {
|
|
94
|
-
user: typedArgs.ctx.user,
|
|
95
|
-
session: typedArgs.ctx.session,
|
|
96
|
-
};
|
|
106
|
+
const actor = requireProcedureActor(host, typedArgs.ctx, "user");
|
|
107
|
+
if (actor.isErr())
|
|
108
|
+
return actor;
|
|
97
109
|
if ("entityStep" in config && typeof config.entityStep === "string") {
|
|
98
110
|
const entities = typedArgs.state[config.entityStep];
|
|
99
|
-
const hasPermission = host.checkPermission(
|
|
111
|
+
const hasPermission = host.checkPermission(actor.value, config.action, entities, config.grants);
|
|
100
112
|
if (!hasPermission) {
|
|
101
113
|
return host.error("FORBIDDEN");
|
|
102
114
|
}
|
|
@@ -105,7 +117,7 @@ function createAccessStep(host, config) {
|
|
|
105
117
|
if (typeof config.entities === "function") {
|
|
106
118
|
const resolveEntities = config.entities;
|
|
107
119
|
let loadedEntities;
|
|
108
|
-
const permission = await host.checkPermissionAsync(
|
|
120
|
+
const permission = await host.checkPermissionAsync(actor.value, config.action, async () => {
|
|
109
121
|
const entityResult = await normalizeProcedureResult(resolveEntities(typedArgs));
|
|
110
122
|
if (entityResult.isOk()) {
|
|
111
123
|
loadedEntities = entityResult.value;
|
|
@@ -121,7 +133,7 @@ function createAccessStep(host, config) {
|
|
|
121
133
|
return (0, neverthrow_1.ok)(loadedEntities);
|
|
122
134
|
}
|
|
123
135
|
const entities = config.entities;
|
|
124
|
-
const hasPermission = host.checkPermission(
|
|
136
|
+
const hasPermission = host.checkPermission(actor.value, config.action, entities, config.grants);
|
|
125
137
|
if (!hasPermission) {
|
|
126
138
|
return host.error("FORBIDDEN");
|
|
127
139
|
}
|
|
@@ -203,7 +215,7 @@ function createServiceProcedureBuilder(host, config) {
|
|
|
203
215
|
function addContextFilter(include) {
|
|
204
216
|
const steps = hasStepName(config.steps, "auth")
|
|
205
217
|
? config.steps
|
|
206
|
-
: [...config.steps, createRequireAuthStep(host)];
|
|
218
|
+
: [...config.steps, createRequireAuthStep(host, "user")];
|
|
207
219
|
assertUniqueStepName(steps, "contextFilter");
|
|
208
220
|
return createServiceProcedureBuilder(host, {
|
|
209
221
|
...config,
|
|
@@ -226,11 +238,11 @@ function createServiceProcedureBuilder(host, config) {
|
|
|
226
238
|
});
|
|
227
239
|
},
|
|
228
240
|
addContextFilter,
|
|
229
|
-
requireAuth() {
|
|
241
|
+
requireAuth(scope) {
|
|
230
242
|
assertUniqueStepName(config.steps, "auth");
|
|
231
243
|
return createServiceProcedureBuilder(host, {
|
|
232
244
|
...config,
|
|
233
|
-
steps: [...config.steps, createRequireAuthStep(host)],
|
|
245
|
+
steps: [...config.steps, createRequireAuthStep(host, scope ?? "user")],
|
|
234
246
|
});
|
|
235
247
|
},
|
|
236
248
|
handle(handler) {
|
|
@@ -243,7 +255,7 @@ function createPermissionServiceProcedureBuilder(host, config) {
|
|
|
243
255
|
function addContextFilter(include) {
|
|
244
256
|
const steps = hasStepName(config.steps, "auth")
|
|
245
257
|
? config.steps
|
|
246
|
-
: [...config.steps, createRequireAuthStep(host)];
|
|
258
|
+
: [...config.steps, createRequireAuthStep(host, "user")];
|
|
247
259
|
assertUniqueStepName(steps, "contextFilter");
|
|
248
260
|
return createPermissionServiceProcedureBuilder(host, {
|
|
249
261
|
...config,
|
|
@@ -273,11 +285,11 @@ function createPermissionServiceProcedureBuilder(host, config) {
|
|
|
273
285
|
});
|
|
274
286
|
},
|
|
275
287
|
addContextFilter,
|
|
276
|
-
requireAuth() {
|
|
288
|
+
requireAuth(scope) {
|
|
277
289
|
assertUniqueStepName(config.steps, "auth");
|
|
278
290
|
return createPermissionServiceProcedureBuilder(host, {
|
|
279
291
|
...config,
|
|
280
|
-
steps: [...config.steps, createRequireAuthStep(host)],
|
|
292
|
+
steps: [...config.steps, createRequireAuthStep(host, scope ?? "user")],
|
|
281
293
|
});
|
|
282
294
|
},
|
|
283
295
|
access,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QueryFilter, QueryInput } from "@m5kdev/commons/modules/schemas/query.schema";
|
|
2
|
-
import type { Context, Session, User } from "../auth/auth.lib";
|
|
3
2
|
import { Base } from "./base.abstract";
|
|
3
|
+
import { type AuthenticatedActor } from "./base.actor";
|
|
4
4
|
import type { ServerResult, ServerResultAsync } from "./base.dto";
|
|
5
5
|
import { type Entity, type ResourceActionGrant, type ResourceGrant } from "./base.grants";
|
|
6
6
|
import { type PermissionServiceProcedureBuilder, type ServiceProcedureBuilder, type ServiceProcedureContext } from "./base.procedure";
|
|
@@ -12,7 +12,7 @@ export declare class BaseService<Repositories extends Record<string, Base>, Serv
|
|
|
12
12
|
addUserFilter(value: string, query?: undefined, columnId?: string, method?: QueryFilter["method"]): QueryInput;
|
|
13
13
|
addUserFilter<TQuery extends QueryInput>(value: string, query: TQuery, columnId?: string, method?: QueryFilter["method"]): TQuery;
|
|
14
14
|
protected procedure<TInput, TCtx extends ServiceProcedureContext = DefaultContext>(name: string): ServiceProcedureBuilder<TInput, TCtx, Repositories, Services>;
|
|
15
|
-
addContextFilter(
|
|
15
|
+
addContextFilter(actor: AuthenticatedActor, include?: {
|
|
16
16
|
user?: boolean;
|
|
17
17
|
organization?: boolean;
|
|
18
18
|
team?: boolean;
|
|
@@ -20,7 +20,7 @@ export declare class BaseService<Repositories extends Record<string, Base>, Serv
|
|
|
20
20
|
columnId: string;
|
|
21
21
|
method: QueryFilter["method"];
|
|
22
22
|
}>): QueryInput;
|
|
23
|
-
addContextFilter<TQuery extends QueryInput>(
|
|
23
|
+
addContextFilter<TQuery extends QueryInput>(actor: AuthenticatedActor, include: {
|
|
24
24
|
user?: boolean;
|
|
25
25
|
organization?: boolean;
|
|
26
26
|
team?: boolean;
|
|
@@ -32,21 +32,9 @@ export declare class BaseService<Repositories extends Record<string, Base>, Serv
|
|
|
32
32
|
export declare class BasePermissionService<Repositories extends Record<string, Base>, Services extends Record<string, Base>, DefaultContext extends ServiceProcedureContext = ServiceProcedureContext> extends BaseService<Repositories, Services, DefaultContext> {
|
|
33
33
|
grants: ResourceGrant[];
|
|
34
34
|
constructor(repository: Repositories, service: Services, grants?: ResourceGrant[]);
|
|
35
|
-
accessGuard<T extends Entity>(
|
|
36
|
-
|
|
37
|
-
user: User;
|
|
38
|
-
}, action: string, entities?: T | T[], grants?: ResourceActionGrant[]): ServerResult<true>;
|
|
39
|
-
accessGuardAsync<T extends Entity>(ctx: {
|
|
40
|
-
session: Session;
|
|
41
|
-
user: User;
|
|
42
|
-
}, action: string, getEntities: () => ServerResultAsync<T | T[] | undefined>, grants?: ResourceActionGrant[]): ServerResultAsync<true>;
|
|
35
|
+
accessGuard<T extends Entity>(actor: AuthenticatedActor, action: string, entities?: T | T[], grants?: ResourceActionGrant[]): ServerResult<true>;
|
|
36
|
+
accessGuardAsync<T extends Entity>(actor: AuthenticatedActor, action: string, getEntities: () => ServerResultAsync<T | T[] | undefined>, grants?: ResourceActionGrant[]): ServerResultAsync<true>;
|
|
43
37
|
protected procedure<TInput, TCtx extends ServiceProcedureContext = DefaultContext>(name: string): PermissionServiceProcedureBuilder<TInput, TCtx, Repositories, Services>;
|
|
44
|
-
checkPermission<T extends Entity>(
|
|
45
|
-
|
|
46
|
-
user: User;
|
|
47
|
-
}, action: string, entities?: T | T[], grants?: ResourceActionGrant[]): boolean;
|
|
48
|
-
checkPermissionAsync<T extends Entity>(ctx: {
|
|
49
|
-
session: Session;
|
|
50
|
-
user: User;
|
|
51
|
-
}, action: string, getEntities: () => ServerResultAsync<T | T[] | undefined>, grants?: ResourceActionGrant[]): ServerResultAsync<boolean>;
|
|
38
|
+
checkPermission<T extends Entity>(actor: AuthenticatedActor, action: string, entities?: T | T[], grants?: ResourceActionGrant[]): boolean;
|
|
39
|
+
checkPermissionAsync<T extends Entity>(actor: AuthenticatedActor, action: string, getEntities: () => ServerResultAsync<T | T[] | undefined>, grants?: ResourceActionGrant[]): ServerResultAsync<boolean>;
|
|
52
40
|
}
|