@lobb-js/lobb-ext-auth 0.14.0 → 0.14.1
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/index.js +10 -10
- package/extensions/auth/studio/index.ts +10 -10
- package/extensions/auth/workflows/actionController.ts +12 -11
- package/extensions/auth/workflows/baseWorkflow.ts +49 -52
- package/extensions/auth/workflows/currentUserPermissionsWorkflow.ts +12 -11
- package/extensions/auth/workflows/meAliasWorkflows.ts +16 -12
- package/extensions/auth/workflows/policiesWorkflows.ts +0 -8
- package/extensions/auth/workflows/sharesWorkflows.ts +50 -51
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -19,36 +19,36 @@ export default function extension(utils) {
|
|
|
19
19
|
{
|
|
20
20
|
// Generic auth primitive: callers ask "can the current user access X?"
|
|
21
21
|
// and pass any combination of { collection, action, role }. The handler
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
22
|
+
// takes over with a boolean (`ctx.takeOver(bool)`); callers read that
|
|
23
|
+
// and decide what to render. A take-over is required because the
|
|
24
|
+
// dispatch ignores plain returns — a bare `return false` would be
|
|
25
|
+
// dropped and the caller would default to allow.
|
|
26
26
|
eventName: "auth.canAccess",
|
|
27
|
-
handler: async (input) => {
|
|
27
|
+
handler: async (input, ctx) => {
|
|
28
28
|
// Admin shortcut — admins bypass everything, regardless of how
|
|
29
29
|
// their permissions snapshot was populated.
|
|
30
30
|
const user = utils.ctx.extensions.auth?.user;
|
|
31
31
|
if (user?.role === "admin")
|
|
32
|
-
return true;
|
|
32
|
+
return ctx.takeOver(true);
|
|
33
33
|
// Walk the permissions snapshot. Works for both logged-in non-admin
|
|
34
34
|
// users (snapshot from their role) and share-token recipients
|
|
35
35
|
// (snapshot from the share's embedded permissions). If nothing
|
|
36
36
|
// is populated — anonymous viewer — default to deny.
|
|
37
37
|
const permissions = utils.ctx.extensions.auth?.permissions;
|
|
38
38
|
if (permissions === undefined || permissions === null)
|
|
39
|
-
return false;
|
|
39
|
+
return ctx.takeOver(false);
|
|
40
40
|
if (permissions === true)
|
|
41
|
-
return true;
|
|
41
|
+
return ctx.takeOver(true);
|
|
42
42
|
const action = input.action ?? "read";
|
|
43
43
|
const requiredCollections = Array.isArray(input.collection)
|
|
44
44
|
? input.collection
|
|
45
45
|
: input.collection ? [input.collection] : [];
|
|
46
46
|
for (const collectionName of requiredCollections) {
|
|
47
47
|
if (!isActionAllowed(collectionName, action, permissions)) {
|
|
48
|
-
return false;
|
|
48
|
+
return ctx.takeOver(false);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
return true;
|
|
51
|
+
return ctx.takeOver(true);
|
|
52
52
|
},
|
|
53
53
|
},
|
|
54
54
|
],
|
|
@@ -22,24 +22,24 @@ export default function extension(utils: ExtensionUtils): Extension {
|
|
|
22
22
|
{
|
|
23
23
|
// Generic auth primitive: callers ask "can the current user access X?"
|
|
24
24
|
// and pass any combination of { collection, action, role }. The handler
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
25
|
+
// takes over with a boolean (`ctx.takeOver(bool)`); callers read that
|
|
26
|
+
// and decide what to render. A take-over is required because the
|
|
27
|
+
// dispatch ignores plain returns — a bare `return false` would be
|
|
28
|
+
// dropped and the caller would default to allow.
|
|
29
29
|
eventName: "auth.canAccess",
|
|
30
|
-
handler: async (input) => {
|
|
30
|
+
handler: async (input, ctx) => {
|
|
31
31
|
// Admin shortcut — admins bypass everything, regardless of how
|
|
32
32
|
// their permissions snapshot was populated.
|
|
33
33
|
const user = utils.ctx.extensions.auth?.user;
|
|
34
|
-
if (user?.role === "admin") return true;
|
|
34
|
+
if (user?.role === "admin") return ctx.takeOver(true);
|
|
35
35
|
|
|
36
36
|
// Walk the permissions snapshot. Works for both logged-in non-admin
|
|
37
37
|
// users (snapshot from their role) and share-token recipients
|
|
38
38
|
// (snapshot from the share's embedded permissions). If nothing
|
|
39
39
|
// is populated — anonymous viewer — default to deny.
|
|
40
40
|
const permissions = utils.ctx.extensions.auth?.permissions;
|
|
41
|
-
if (permissions === undefined || permissions === null) return false;
|
|
42
|
-
if (permissions === true) return true;
|
|
41
|
+
if (permissions === undefined || permissions === null) return ctx.takeOver(false);
|
|
42
|
+
if (permissions === true) return ctx.takeOver(true);
|
|
43
43
|
|
|
44
44
|
const action: CollectionPermissionActionsKeys = input.action ?? "read";
|
|
45
45
|
|
|
@@ -49,11 +49,11 @@ export default function extension(utils: ExtensionUtils): Extension {
|
|
|
49
49
|
|
|
50
50
|
for (const collectionName of requiredCollections) {
|
|
51
51
|
if (!isActionAllowed(collectionName, action, permissions)) {
|
|
52
|
-
return false;
|
|
52
|
+
return ctx.takeOver(false);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
return true;
|
|
56
|
+
return ctx.takeOver(true);
|
|
57
57
|
},
|
|
58
58
|
},
|
|
59
59
|
],
|
|
@@ -15,19 +15,20 @@ export function getAuthActionControllerWorkflows(): Workflow[] {
|
|
|
15
15
|
name: "authLogoutActionController",
|
|
16
16
|
eventName: "core.actions.controller.override",
|
|
17
17
|
handler: async (input, ctx) => {
|
|
18
|
-
if (input.name
|
|
18
|
+
if (input.name === "auth_logout") {
|
|
19
|
+
const context = input.context as Context;
|
|
20
|
+
const token = getBearerToken(context);
|
|
21
|
+
if (!token) {
|
|
22
|
+
throw new ctx.LobbError({
|
|
23
|
+
code: "BAD_REQUEST",
|
|
24
|
+
message:
|
|
25
|
+
"You should pass session's token through the bearer header",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (!token) {
|
|
23
|
-
throw new ctx.LobbError({
|
|
24
|
-
code: "BAD_REQUEST",
|
|
25
|
-
message: "You should pass session's token through the bearer header",
|
|
26
|
-
});
|
|
29
|
+
await ctx.lobb.runAction("auth_logout", { token });
|
|
30
|
+
return ctx.takeOver(context.body(null, 204));
|
|
27
31
|
}
|
|
28
|
-
|
|
29
|
-
await ctx.lobb.runAction("auth_logout", { token });
|
|
30
|
-
return context.body(null, 204);
|
|
31
32
|
},
|
|
32
33
|
},
|
|
33
34
|
];
|
|
@@ -9,32 +9,32 @@ export function getBaseWorkflows(_extensionConfig: ExtensionConfig): Workflow[]
|
|
|
9
9
|
name: "auth_preventAdminUserDeletion",
|
|
10
10
|
eventName: "core.store.preDeleteOne",
|
|
11
11
|
handler: async (input, ctx) => {
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
if (
|
|
13
|
+
input.collectionName === "auth_users" &&
|
|
14
|
+
input.triggeredBy === "API" &&
|
|
15
|
+
Number(input.id) === 1
|
|
16
|
+
) {
|
|
16
17
|
throw new ctx.LobbError({
|
|
17
18
|
code: "FORBIDDEN",
|
|
18
19
|
message: "The admin user cannot be deleted.",
|
|
19
20
|
});
|
|
20
21
|
}
|
|
21
|
-
return input;
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
name: "auth_preventAdminUserUpdate",
|
|
26
26
|
eventName: "core.store.preUpdateOne",
|
|
27
27
|
handler: async (input, ctx) => {
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
if (
|
|
29
|
+
input.collectionName === "auth_users" &&
|
|
30
|
+
input.triggeredBy === "API" &&
|
|
31
|
+
Number(input.id) === 1
|
|
32
|
+
) {
|
|
32
33
|
throw new ctx.LobbError({
|
|
33
34
|
code: "FORBIDDEN",
|
|
34
35
|
message: "The admin user cannot be updated.",
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
|
-
return input;
|
|
38
38
|
},
|
|
39
39
|
},
|
|
40
40
|
...baseWorkflows,
|
|
@@ -51,58 +51,55 @@ export const baseWorkflows: Workflow[] = [
|
|
|
51
51
|
eventName: "core.webserver.middlwares.pre",
|
|
52
52
|
handler: async (input, ctx) => {
|
|
53
53
|
const token = getBearerToken(input.context);
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
const context = input.context as Context;
|
|
54
|
+
if (token) {
|
|
55
|
+
const context = input.context as Context;
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
method: "findAll",
|
|
61
|
-
props: {
|
|
62
|
-
collectionName: "auth_sessions",
|
|
63
|
-
params: { filter: { token } },
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
const session = sessionsResult.data[0];
|
|
67
|
-
|
|
68
|
-
if (session) {
|
|
69
|
-
const usersResult = await ctx.workflows.collectionStore({
|
|
57
|
+
// 1) Try the token as a normal user session.
|
|
58
|
+
const sessionsResult = await ctx.workflows.collectionStore({
|
|
70
59
|
method: "findAll",
|
|
71
60
|
props: {
|
|
72
|
-
collectionName: "
|
|
73
|
-
params: { filter: {
|
|
61
|
+
collectionName: "auth_sessions",
|
|
62
|
+
params: { filter: { token } },
|
|
74
63
|
},
|
|
75
64
|
});
|
|
76
|
-
const
|
|
65
|
+
const session = sessionsResult.data[0];
|
|
77
66
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
67
|
+
if (session) {
|
|
68
|
+
const usersResult = await ctx.workflows.collectionStore({
|
|
69
|
+
method: "findAll",
|
|
70
|
+
props: {
|
|
71
|
+
collectionName: "auth_users",
|
|
72
|
+
params: { filter: { id: session.user_id } },
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
const user = usersResult.data[0];
|
|
82
76
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
77
|
+
context.set("auth_session", session);
|
|
78
|
+
context.set("auth_user", user);
|
|
79
|
+
} else {
|
|
80
|
+
// 2) Otherwise try the token as a share. Shares carry their own
|
|
81
|
+
// permissions snapshot and have a fixed expiry — no user identity.
|
|
82
|
+
const sharesResult = await ctx.workflows.collectionStore({
|
|
83
|
+
method: "findAll",
|
|
84
|
+
props: {
|
|
85
|
+
collectionName: "auth_shares",
|
|
86
|
+
params: { filter: { token } },
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
const share = sharesResult.data[0];
|
|
93
90
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
if (share && new Date(share.expires_at) > new Date()) {
|
|
92
|
+
let permissions: any = {};
|
|
93
|
+
try {
|
|
94
|
+
permissions = JSON.parse(share.permissions ?? "{}");
|
|
95
|
+
} catch {
|
|
96
|
+
// malformed permissions — treat as no access
|
|
97
|
+
permissions = {};
|
|
98
|
+
}
|
|
99
|
+
context.set("auth_share", { ...share, permissions });
|
|
100
|
+
}
|
|
101
101
|
}
|
|
102
|
-
context.set("auth_share", { ...share, permissions });
|
|
103
102
|
}
|
|
104
|
-
|
|
105
|
-
return input;
|
|
106
103
|
},
|
|
107
104
|
},
|
|
108
105
|
];
|
|
@@ -15,18 +15,19 @@ export function getCurrentUserPermissionsWorkflow(
|
|
|
15
15
|
name: "auth_attachCurrentUserPermissions",
|
|
16
16
|
eventName: "core.controllers.findOne",
|
|
17
17
|
handler: async (input) => {
|
|
18
|
-
if (input.collectionName !== "auth_users") return input;
|
|
19
18
|
const context = input.context as Context | undefined;
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
if (
|
|
20
|
+
input.collectionName === "auth_users" &&
|
|
21
|
+
context?.req.param("id") === "me"
|
|
22
|
+
) {
|
|
23
|
+
const role = input.response?.data?.role;
|
|
24
|
+
// Strip functions (e.g. dynamic filter predicates) — they can't be
|
|
25
|
+
// serialised to the client.
|
|
26
|
+
const permissions = JSON.parse(JSON.stringify(
|
|
27
|
+
extensionConfig.roles?.[role]?.permissions ?? {},
|
|
28
|
+
));
|
|
29
|
+
input.response = { ...input.response, permissions };
|
|
30
|
+
}
|
|
30
31
|
},
|
|
31
32
|
};
|
|
32
33
|
}
|
|
@@ -22,14 +22,15 @@ function resolveMeAlias(input: any, ctx: EventContext): number {
|
|
|
22
22
|
|
|
23
23
|
export const meAliasWorkflows: Workflow[] = [
|
|
24
24
|
{
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// Share bearers don't have an auth_users row to fetch — take over the /me
|
|
26
|
+
// findOne at the controller boundary with the share's permissions snapshot.
|
|
27
|
+
// Studio uses the same /me endpoint to learn what the current bearer can do
|
|
28
|
+
// regardless of bearer type. Done at the controller level (not the store)
|
|
29
|
+
// so the HTTP response never originates from the data layer; the store-level
|
|
30
|
+
// resolveMeAlias below only runs for real users (where it resolves the id).
|
|
31
|
+
name: "auth_userFindOneMeShare",
|
|
32
|
+
eventName: "core.controllers.preFindOne",
|
|
27
33
|
handler: async (input, ctx) => {
|
|
28
|
-
// Share bearers don't have an auth_users row to fetch — short-circuit
|
|
29
|
-
// the findOne with the share's permissions snapshot. Studio uses the
|
|
30
|
-
// same /me endpoint to learn what the current bearer can do regardless
|
|
31
|
-
// of bearer type. Falls through to resolveMeAlias otherwise (which
|
|
32
|
-
// handles the session case and throws for unauthenticated requests).
|
|
33
34
|
if (input.collectionName === "auth_users" && input.id === "me") {
|
|
34
35
|
const context = input.context as Context;
|
|
35
36
|
const user = context.get("auth_user") as User | undefined;
|
|
@@ -38,7 +39,7 @@ export const meAliasWorkflows: Workflow[] = [
|
|
|
38
39
|
| { permissions: any }
|
|
39
40
|
| undefined;
|
|
40
41
|
if (share) {
|
|
41
|
-
|
|
42
|
+
return ctx.takeOver(new Response(
|
|
42
43
|
JSON.stringify({
|
|
43
44
|
data: null,
|
|
44
45
|
permissions: share.permissions,
|
|
@@ -47,12 +48,17 @@ export const meAliasWorkflows: Workflow[] = [
|
|
|
47
48
|
status: 200,
|
|
48
49
|
headers: { "Content-Type": "application/json" },
|
|
49
50
|
},
|
|
50
|
-
);
|
|
51
|
+
));
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
}
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "auth_userFindOneMeAlias",
|
|
59
|
+
eventName: "core.store.preFindOne",
|
|
60
|
+
handler: async (input, ctx) => {
|
|
54
61
|
input.id = resolveMeAlias(input, ctx);
|
|
55
|
-
return input;
|
|
56
62
|
},
|
|
57
63
|
},
|
|
58
64
|
{
|
|
@@ -60,7 +66,6 @@ export const meAliasWorkflows: Workflow[] = [
|
|
|
60
66
|
eventName: "core.store.preUpdateOne",
|
|
61
67
|
handler: async (input, ctx) => {
|
|
62
68
|
input.id = resolveMeAlias(input, ctx);
|
|
63
|
-
return input;
|
|
64
69
|
},
|
|
65
70
|
},
|
|
66
71
|
{
|
|
@@ -68,7 +73,6 @@ export const meAliasWorkflows: Workflow[] = [
|
|
|
68
73
|
eventName: "core.store.preDeleteOne",
|
|
69
74
|
handler: async (input, ctx) => {
|
|
70
75
|
input.id = resolveMeAlias(input, ctx);
|
|
71
|
-
return input;
|
|
72
76
|
},
|
|
73
77
|
},
|
|
74
78
|
];
|
|
@@ -33,7 +33,6 @@ export function getPoliciesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
33
33
|
});
|
|
34
34
|
if (output?.payload) input.data = output.payload;
|
|
35
35
|
}
|
|
36
|
-
return input;
|
|
37
36
|
},
|
|
38
37
|
},
|
|
39
38
|
{
|
|
@@ -60,7 +59,6 @@ export function getPoliciesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
60
59
|
});
|
|
61
60
|
if (output?.filter) input.filter = output.filter;
|
|
62
61
|
}
|
|
63
|
-
return input;
|
|
64
62
|
},
|
|
65
63
|
},
|
|
66
64
|
{
|
|
@@ -88,7 +86,6 @@ export function getPoliciesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
88
86
|
});
|
|
89
87
|
if (output?.payload) input.data = output.payload;
|
|
90
88
|
}
|
|
91
|
-
return input;
|
|
92
89
|
},
|
|
93
90
|
},
|
|
94
91
|
{
|
|
@@ -114,7 +111,6 @@ export function getPoliciesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
114
111
|
recordId: input.id,
|
|
115
112
|
});
|
|
116
113
|
}
|
|
117
|
-
return input;
|
|
118
114
|
},
|
|
119
115
|
},
|
|
120
116
|
// Post-fetch field-level filtering using the role/share's `read.fields`
|
|
@@ -132,7 +128,6 @@ export function getPoliciesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
132
128
|
);
|
|
133
129
|
input.data = handleReadFields(permissions, input.collectionName, input.data);
|
|
134
130
|
}
|
|
135
|
-
return input;
|
|
136
131
|
},
|
|
137
132
|
},
|
|
138
133
|
{
|
|
@@ -148,7 +143,6 @@ export function getPoliciesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
148
143
|
);
|
|
149
144
|
input.data = handleReadFields(permissions, input.collectionName, input.data);
|
|
150
145
|
}
|
|
151
|
-
return input;
|
|
152
146
|
},
|
|
153
147
|
},
|
|
154
148
|
{
|
|
@@ -164,7 +158,6 @@ export function getPoliciesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
164
158
|
);
|
|
165
159
|
input.data = handleReadFields(permissions, input.collectionName, input.data);
|
|
166
160
|
}
|
|
167
|
-
return input;
|
|
168
161
|
},
|
|
169
162
|
},
|
|
170
163
|
{
|
|
@@ -180,7 +173,6 @@ export function getPoliciesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
180
173
|
);
|
|
181
174
|
input.data = handleReadFields(permissions, input.collectionName, input.data);
|
|
182
175
|
}
|
|
183
|
-
return input;
|
|
184
176
|
},
|
|
185
177
|
},
|
|
186
178
|
];
|
|
@@ -22,7 +22,6 @@ export function getSharesWorkflows(extensionConfig: ExtensionConfig): Workflow[]
|
|
|
22
22
|
if (input.collectionName === "auth_shares") {
|
|
23
23
|
input.data.token = generateRandomId();
|
|
24
24
|
}
|
|
25
|
-
return input;
|
|
26
25
|
},
|
|
27
26
|
},
|
|
28
27
|
// Normalize the two ways a caller can specify share lifetime into the
|
|
@@ -37,30 +36,28 @@ export function getSharesWorkflows(extensionConfig: ExtensionConfig): Workflow[]
|
|
|
37
36
|
eventName: "core.store.preCreateOne",
|
|
38
37
|
order: "pre",
|
|
39
38
|
handler: async (input, ctx) => {
|
|
40
|
-
if (input.collectionName
|
|
39
|
+
if (input.collectionName === "auth_shares") {
|
|
40
|
+
const seconds = input.data.expires_in_seconds;
|
|
41
|
+
if (typeof seconds === "number") {
|
|
42
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
43
|
+
throw new ctx.LobbError({
|
|
44
|
+
code: "BAD_REQUEST",
|
|
45
|
+
message: "expires_in_seconds must be a positive number.",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
input.data.expires_at = new Date(Date.now() + seconds * 1000)
|
|
49
|
+
.toISOString();
|
|
50
|
+
delete input.data.expires_in_seconds;
|
|
51
|
+
}
|
|
41
52
|
|
|
42
|
-
|
|
43
|
-
if (typeof seconds === "number") {
|
|
44
|
-
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
53
|
+
if (!input.data.expires_at) {
|
|
45
54
|
throw new ctx.LobbError({
|
|
46
55
|
code: "BAD_REQUEST",
|
|
47
|
-
message:
|
|
56
|
+
message:
|
|
57
|
+
"Must provide either expires_at or expires_in_seconds when creating a share.",
|
|
48
58
|
});
|
|
49
59
|
}
|
|
50
|
-
input.data.expires_at = new Date(Date.now() + seconds * 1000)
|
|
51
|
-
.toISOString();
|
|
52
|
-
delete input.data.expires_in_seconds;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (!input.data.expires_at) {
|
|
56
|
-
throw new ctx.LobbError({
|
|
57
|
-
code: "BAD_REQUEST",
|
|
58
|
-
message:
|
|
59
|
-
"Must provide either expires_at or expires_in_seconds when creating a share.",
|
|
60
|
-
});
|
|
61
60
|
}
|
|
62
|
-
|
|
63
|
-
return input;
|
|
64
61
|
},
|
|
65
62
|
},
|
|
66
63
|
// Auto-set created_by from the authenticated request user so callers
|
|
@@ -71,12 +68,14 @@ export function getSharesWorkflows(extensionConfig: ExtensionConfig): Workflow[]
|
|
|
71
68
|
name: "auth_setShareCreatedBy",
|
|
72
69
|
eventName: "core.store.preCreateOne",
|
|
73
70
|
handler: async (input) => {
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
if (
|
|
72
|
+
input.collectionName === "auth_shares" &&
|
|
73
|
+
input.triggeredBy === "API"
|
|
74
|
+
) {
|
|
75
|
+
const context = input.context as Context;
|
|
76
|
+
const user = context.get("auth_user") as User | undefined;
|
|
77
|
+
if (user) input.data.created_by = user.id;
|
|
78
|
+
}
|
|
80
79
|
},
|
|
81
80
|
},
|
|
82
81
|
// Intersection guard: a share's embedded permissions snapshot must be a
|
|
@@ -88,35 +87,35 @@ export function getSharesWorkflows(extensionConfig: ExtensionConfig): Workflow[]
|
|
|
88
87
|
name: "auth_validateShareSubset",
|
|
89
88
|
eventName: "core.store.preCreateOne",
|
|
90
89
|
handler: async (input, ctx) => {
|
|
91
|
-
if (
|
|
92
|
-
|
|
90
|
+
if (
|
|
91
|
+
input.collectionName === "auth_shares" &&
|
|
92
|
+
input.triggeredBy === "API"
|
|
93
|
+
) {
|
|
94
|
+
const context = input.context as Context;
|
|
95
|
+
const { permissions: creatorPermissions } = resolveRequestPermissions(
|
|
96
|
+
context,
|
|
97
|
+
extensionConfig,
|
|
98
|
+
ctx,
|
|
99
|
+
);
|
|
93
100
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
sharePermissions = JSON.parse(input.data.permissions);
|
|
104
|
-
} catch {
|
|
105
|
-
throw new ctx.LobbError({
|
|
106
|
-
code: "BAD_REQUEST",
|
|
107
|
-
message: "Share permissions must be valid JSON.",
|
|
108
|
-
});
|
|
109
|
-
}
|
|
101
|
+
let sharePermissions: PermissionsConfig;
|
|
102
|
+
try {
|
|
103
|
+
sharePermissions = JSON.parse(input.data.permissions);
|
|
104
|
+
} catch {
|
|
105
|
+
throw new ctx.LobbError({
|
|
106
|
+
code: "BAD_REQUEST",
|
|
107
|
+
message: "Share permissions must be valid JSON.",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
if (!isShareSubsetOfCreator(sharePermissions, creatorPermissions)) {
|
|
112
|
+
throw new ctx.LobbError({
|
|
113
|
+
code: "FORBIDDEN",
|
|
114
|
+
message:
|
|
115
|
+
"Cannot create a share with broader permissions than your own.",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
117
118
|
}
|
|
118
|
-
|
|
119
|
-
return input;
|
|
120
119
|
},
|
|
121
120
|
},
|
|
122
121
|
// Hourly housekeeping: delete shares whose expires_at is in the past.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobb-js/lobb-ext-auth",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"package": "svelte-package --input extensions/auth/studio"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@lobb-js/core": "^0.
|
|
35
|
+
"@lobb-js/core": "^0.42.0",
|
|
36
36
|
"argon2": "^0.40.3",
|
|
37
37
|
"hono": "^4.7.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@lobb-js/studio": "^0.
|
|
40
|
+
"@lobb-js/studio": "^0.50.0",
|
|
41
41
|
"@lucide/svelte": "^0.563.1",
|
|
42
42
|
"@sveltejs/adapter-node": "^5.5.4",
|
|
43
43
|
"@sveltejs/kit": "^2.60.1",
|