@lobb-js/lobb-ext-auth 0.11.0 → 0.11.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/README.md +1 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +23 -4
- package/dist/index.js +41 -2
- package/dist/lib/components/pages/settings/index.svelte +1 -1
- package/dist/lib/components/pages/settings/pages/activityFeed.svelte +1 -1
- package/dist/lib/components/pages/settings/pages/rolesAndPermissions.svelte +1 -1
- package/dist/lib/components/pages/settings/pages/users.svelte +1 -1
- package/dist/lib/components/pages/userSettings/index.svelte +45 -32
- package/dist/onStartup.js +17 -2
- package/dist/shared/permissions.d.ts +2 -0
- package/dist/shared/permissions.js +35 -0
- package/extensions/auth/collections/collections.ts +2 -0
- package/extensions/auth/collections/shares.ts +60 -0
- package/extensions/auth/config/extensionConfigSchema.d.ts +41 -0
- package/extensions/auth/config/permissionsAction/create.d.ts +18 -0
- package/extensions/auth/config/permissionsAction/delete.d.ts +3 -0
- package/extensions/auth/config/permissionsAction/read.d.ts +11 -0
- package/extensions/auth/config/permissionsAction/update.d.ts +18 -0
- package/extensions/auth/index.ts +0 -2
- package/extensions/auth/studio/auth.ts +25 -5
- package/extensions/auth/studio/index.ts +44 -2
- package/extensions/auth/studio/lib/components/pages/settings/index.svelte +1 -1
- package/extensions/auth/studio/lib/components/pages/settings/pages/activityFeed.svelte +1 -1
- package/extensions/auth/studio/lib/components/pages/settings/pages/rolesAndPermissions.svelte +1 -1
- package/extensions/auth/studio/lib/components/pages/settings/pages/users.svelte +1 -1
- package/extensions/auth/studio/lib/components/pages/userSettings/index.svelte +45 -32
- package/extensions/auth/studio/onStartup.ts +14 -2
- package/extensions/auth/studio/shared/permissions.ts +42 -0
- package/extensions/auth/tests/collections/shares.test.ts +657 -0
- package/extensions/auth/tests/configs/auth.ts +17 -0
- package/extensions/auth/tests/controllers/me.test.ts +104 -0
- package/extensions/auth/tests/permissions.test.ts +127 -0
- package/extensions/auth/tests/workflows/shareIntersection.test.ts +158 -0
- package/extensions/auth/workflows/baseWorkflow.ts +48 -26
- package/extensions/auth/workflows/currentUserPermissionsWorkflow.ts +32 -0
- package/extensions/auth/workflows/index.ts +12 -0
- package/extensions/auth/workflows/meAliasWorkflows.ts +26 -0
- package/extensions/auth/workflows/policiesWorkflows.ts +64 -117
- package/extensions/auth/workflows/shareIntersection.ts +64 -0
- package/extensions/auth/workflows/sharesWorkflows.ts +135 -0
- package/extensions/auth/workflows/utils.ts +132 -224
- package/package.json +3 -3
- package/dist/lib/components/pages/userSettings/components/account.svelte +0 -106
- package/dist/lib/components/pages/userSettings/components/account.svelte.d.ts +0 -14
- package/dist/lib/components/pages/userSettings/components/profile.svelte +0 -87
- package/dist/lib/components/pages/userSettings/components/profile.svelte.d.ts +0 -14
- package/extensions/auth/studio/lib/components/pages/userSettings/components/account.svelte +0 -106
- package/extensions/auth/studio/lib/components/pages/userSettings/components/profile.svelte +0 -87
|
@@ -4,6 +4,8 @@ import { onStartup, onRouteChange } from "./onStartup";
|
|
|
4
4
|
import LoginPage from "./lib/components/pages/loginPage/index.svelte";
|
|
5
5
|
import UserSettings from "./lib/components/pages/userSettings/index.svelte";
|
|
6
6
|
import Settings from "./lib/components/pages/settings/index.svelte";
|
|
7
|
+
import { isActionAllowed } from "./shared/permissions";
|
|
8
|
+
import type { CollectionPermissionActionsKeys } from "../config/extensionConfigSchema";
|
|
7
9
|
|
|
8
10
|
// TODO we should export the extension object directly without a function at all. because we dont set configuration in here
|
|
9
11
|
export default function extension(utils: ExtensionUtils): Extension {
|
|
@@ -16,12 +18,52 @@ export default function extension(utils: ExtensionUtils): Extension {
|
|
|
16
18
|
"pages.user_settings": UserSettings,
|
|
17
19
|
"pages.settings": Settings,
|
|
18
20
|
},
|
|
21
|
+
workflows: [
|
|
22
|
+
{
|
|
23
|
+
// Generic auth primitive: callers ask "can the current user access X?"
|
|
24
|
+
// and pass any combination of { collection, action, role }. The handler
|
|
25
|
+
// returns a boolean directly; callers read that and decide what to render.
|
|
26
|
+
// Note: returning a boolean means downstream handlers can't read the
|
|
27
|
+
// original request — if you ever need multiple extensions to influence
|
|
28
|
+
// this decision, switch back to a `{ ...input, allowed }` object shape.
|
|
29
|
+
eventName: "auth.canAccess",
|
|
30
|
+
handler: async (input) => {
|
|
31
|
+
// Admin shortcut — admins bypass everything, regardless of how
|
|
32
|
+
// their permissions snapshot was populated.
|
|
33
|
+
const user = utils.ctx.extensions.auth?.user;
|
|
34
|
+
if (user?.role === "admin") return true;
|
|
35
|
+
|
|
36
|
+
// Walk the permissions snapshot. Works for both logged-in non-admin
|
|
37
|
+
// users (snapshot from their role) and share-token recipients
|
|
38
|
+
// (snapshot from the share's embedded permissions). If nothing
|
|
39
|
+
// is populated — anonymous viewer — default to deny.
|
|
40
|
+
const permissions = utils.ctx.extensions.auth?.permissions;
|
|
41
|
+
if (permissions === undefined || permissions === null) return false;
|
|
42
|
+
if (permissions === true) return true;
|
|
43
|
+
|
|
44
|
+
const action: CollectionPermissionActionsKeys = input.action ?? "read";
|
|
45
|
+
|
|
46
|
+
const requiredCollections = Array.isArray(input.collection)
|
|
47
|
+
? input.collection
|
|
48
|
+
: input.collection ? [input.collection] : [];
|
|
49
|
+
|
|
50
|
+
for (const collectionName of requiredCollections) {
|
|
51
|
+
if (!isActionAllowed(collectionName, action, permissions)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return true;
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
19
60
|
dashboardNavs: {
|
|
20
61
|
middle: [
|
|
21
62
|
{
|
|
22
63
|
label: "Auth",
|
|
23
64
|
icon: utils.components.Icons.Key,
|
|
24
65
|
href: "/studio/extensions/auth/settings/users",
|
|
66
|
+
represents: "auth_users",
|
|
25
67
|
},
|
|
26
68
|
],
|
|
27
69
|
bottom: [
|
|
@@ -29,14 +71,14 @@ export default function extension(utils: ExtensionUtils): Extension {
|
|
|
29
71
|
label: "My User",
|
|
30
72
|
icon: utils.components.Icons.User,
|
|
31
73
|
onclick: () => {
|
|
32
|
-
utils.
|
|
74
|
+
utils.goto("/studio/extensions/auth/user_settings");
|
|
33
75
|
},
|
|
34
76
|
navs: [
|
|
35
77
|
{
|
|
36
78
|
label: "User Settings",
|
|
37
79
|
icon: utils.components.Icons.Settings,
|
|
38
80
|
onclick: () => {
|
|
39
|
-
utils.
|
|
81
|
+
utils.goto("/studio/extensions/auth/user_settings");
|
|
40
82
|
},
|
|
41
83
|
},
|
|
42
84
|
{
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
const components = props.utils.components;
|
|
9
9
|
const { Sidebar } = components;
|
|
10
10
|
const { Icons } = components;
|
|
11
|
-
const auth_settings_page = $derived(props.utils.
|
|
11
|
+
const auth_settings_page = $derived(props.utils.page.url.pathname.split("/")[5]);
|
|
12
12
|
const isSmall = $derived(!props.utils.mediaQueries.sm.current);
|
|
13
13
|
</script>
|
|
14
14
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
</div>
|
|
14
14
|
</div>
|
|
15
15
|
<components.Separator />
|
|
16
|
-
<div class="flex gap-4 border rounded-md overflow-hidden h-full bg-muted
|
|
16
|
+
<div class="flex gap-4 border rounded-md overflow-hidden h-full bg-muted-soft">
|
|
17
17
|
<components.DataTable
|
|
18
18
|
collectionName="auth_activity_feed"
|
|
19
19
|
/>
|
package/extensions/auth/studio/lib/components/pages/settings/pages/rolesAndPermissions.svelte
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
</div>
|
|
14
14
|
</div>
|
|
15
15
|
<components.Separator />
|
|
16
|
-
<div class="flex gap-4 border rounded-md overflow-hidden h-full bg-muted
|
|
16
|
+
<div class="flex gap-4 border rounded-md overflow-hidden h-full bg-muted-soft">
|
|
17
17
|
<components.DataTable
|
|
18
18
|
collectionName="auth_roles"
|
|
19
19
|
/>
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
</div>
|
|
14
14
|
</div>
|
|
15
15
|
<components.Separator />
|
|
16
|
-
<div class="flex gap-4 border rounded-md overflow-hidden h-full bg-muted
|
|
16
|
+
<div class="flex gap-4 border rounded-md overflow-hidden h-full bg-muted-soft">
|
|
17
17
|
<components.DataTable
|
|
18
18
|
collectionName="auth_users"
|
|
19
19
|
/>
|
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { ExtensionProps } from "@lobb-js/studio";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import { DetailView } from "@lobb-js/studio";
|
|
4
|
+
import { onMount } from "svelte";
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const isSmall = $derived(!props.utils.mediaQueries.sm.current);
|
|
6
|
+
const { utils }: ExtensionProps = $props();
|
|
7
|
+
const { Button, Separator, Skeleton, Icons } = utils.components;
|
|
9
8
|
|
|
10
|
-
let
|
|
9
|
+
let entry = $state<Record<string, any>>({});
|
|
10
|
+
let loading = $state(true);
|
|
11
|
+
|
|
12
|
+
onMount(async () => {
|
|
13
|
+
// /me is the only auth_users path a role without explicit auth_users
|
|
14
|
+
// read perms can reach (self-bypass at the policy layer).
|
|
15
|
+
const response = await utils.lobb.findOne("auth_users", "me");
|
|
16
|
+
const result = await response.json();
|
|
17
|
+
entry = result.data;
|
|
18
|
+
loading = false;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
async function handleSave() {
|
|
22
|
+
const response = await utils.lobb.updateOne("auth_users", "me", entry);
|
|
23
|
+
if (response.status >= 400) {
|
|
24
|
+
const result = await response.json();
|
|
25
|
+
utils.toast.error(result.message);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
utils.toast.success("Profile updated successfully.");
|
|
29
|
+
}
|
|
11
30
|
</script>
|
|
12
31
|
|
|
13
32
|
<div class="flex flex-col gap-4 p-4">
|
|
@@ -17,32 +36,26 @@
|
|
|
17
36
|
Manage your user settings.
|
|
18
37
|
</div>
|
|
19
38
|
</div>
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
>
|
|
28
|
-
Profile
|
|
29
|
-
</components.Button>
|
|
30
|
-
<components.Button
|
|
31
|
-
onclick={() => (selectedView = "account")}
|
|
32
|
-
variant="ghost"
|
|
33
|
-
class="flex justify-start text-muted-foreground hover:underline"
|
|
34
|
-
>
|
|
35
|
-
Account
|
|
36
|
-
</components.Button>
|
|
39
|
+
<Separator />
|
|
40
|
+
|
|
41
|
+
{#if loading}
|
|
42
|
+
<div class="flex max-w-xl flex-col gap-2">
|
|
43
|
+
<Skeleton class="h-8 w-full" />
|
|
44
|
+
<Skeleton class="h-8 w-[80%]" />
|
|
45
|
+
<Skeleton class="h-8 w-[60%]" />
|
|
37
46
|
</div>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
{:else}
|
|
48
|
+
<div class="max-w-xl">
|
|
49
|
+
<DetailView collectionName="auth_users" bind:entry />
|
|
50
|
+
<div class="px-4 pt-2">
|
|
51
|
+
<Button
|
|
52
|
+
class="self-start"
|
|
53
|
+
Icon={Icons.Pencil}
|
|
54
|
+
onclick={handleSave}
|
|
55
|
+
>
|
|
56
|
+
Save
|
|
57
|
+
</Button>
|
|
45
58
|
</div>
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
</div>
|
|
60
|
+
{/if}
|
|
48
61
|
</div>
|
|
@@ -2,9 +2,16 @@ import type { ExtensionUtils } from "@lobb-js/studio";
|
|
|
2
2
|
import { Auth } from "./auth";
|
|
3
3
|
|
|
4
4
|
const loginPath = "/studio/extensions/auth/login_page";
|
|
5
|
+
const publicPagesPrefix = "/studio/public/";
|
|
5
6
|
|
|
6
7
|
function isPublicPath(path: string, publicPaths: string[]) {
|
|
7
|
-
|
|
8
|
+
// Anything under /studio/public/ is a publicPages.* route — registered by
|
|
9
|
+
// an extension and intentionally exposed without auth (e.g. share-recipient
|
|
10
|
+
// views). Falls through to the app-level publicPaths config for legacy
|
|
11
|
+
// overrides.
|
|
12
|
+
if (path === loginPath) return true;
|
|
13
|
+
if (path.startsWith(publicPagesPrefix)) return true;
|
|
14
|
+
return publicPaths.some(p => path.startsWith(p));
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
export async function onStartup(utils: ExtensionUtils) {
|
|
@@ -26,6 +33,11 @@ export async function onStartup(utils: ExtensionUtils) {
|
|
|
26
33
|
const session = auth.getSession();
|
|
27
34
|
if (session) {
|
|
28
35
|
utils.ctx.extensions.auth.session = session;
|
|
36
|
+
try {
|
|
37
|
+
await auth.fetchMe();
|
|
38
|
+
} catch {
|
|
39
|
+
await auth.logout();
|
|
40
|
+
}
|
|
29
41
|
return;
|
|
30
42
|
}
|
|
31
43
|
|
|
@@ -43,6 +55,6 @@ export function onRouteChange(utils: ExtensionUtils, path: string) {
|
|
|
43
55
|
|
|
44
56
|
const publicPaths: string[] = utils.ctx.meta?.extensions?.auth?.studio?.publicPaths ?? [];
|
|
45
57
|
if (!isPublicPath(path, publicPaths)) {
|
|
46
|
-
utils.
|
|
58
|
+
utils.goto(`${loginPath}?redirect=${encodeURIComponent(path)}`);
|
|
47
59
|
}
|
|
48
60
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Lives under studio/ for packaging reasons — `svelte-package` only bundles
|
|
2
|
+
// files inside its `--input` root, so anything the studio entry imports has
|
|
3
|
+
// to be reachable from there. The backend imports this same file via
|
|
4
|
+
// `../studio/shared/permissions` so there's still one source of truth.
|
|
5
|
+
//
|
|
6
|
+
// Anything in this folder must stay environment-neutral (no DOM, no Node-only
|
|
7
|
+
// APIs, no top-level side effects) so it runs the same way on both sides.
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
CollectionPermissionActionsKeys,
|
|
11
|
+
PermissionsConfig,
|
|
12
|
+
} from "../../config/extensionConfigSchema.ts";
|
|
13
|
+
|
|
14
|
+
// Pure permission check shared by the server-side handlePolicy and the
|
|
15
|
+
// studio's auth.canAccess workflow. Walks the permissions tree and returns
|
|
16
|
+
// a yes/no — server-side then layers on guards/filters/error-throwing,
|
|
17
|
+
// client-side uses the answer to decide whether to render UI.
|
|
18
|
+
//
|
|
19
|
+
// An object value for action permission means "conditional access" (filter
|
|
20
|
+
// and/or fields restrictions). For UI gating that counts as allowed; the
|
|
21
|
+
// server enforces the actual restrictions on real requests.
|
|
22
|
+
export function isActionAllowed(
|
|
23
|
+
collection: string,
|
|
24
|
+
action: CollectionPermissionActionsKeys,
|
|
25
|
+
permissions: PermissionsConfig | undefined,
|
|
26
|
+
): boolean {
|
|
27
|
+
if (permissions === true) return true;
|
|
28
|
+
if (!permissions) return false;
|
|
29
|
+
const collPerm = permissions[collection];
|
|
30
|
+
if (collPerm === true) return true;
|
|
31
|
+
if (!collPerm) return false;
|
|
32
|
+
const actionPerm = collPerm[action];
|
|
33
|
+
if (actionPerm === true) return true;
|
|
34
|
+
// A conditional grant must actually list at least one constraint
|
|
35
|
+
// (filter / fields / payloadGuard / mutate). Empty `{}` collapses back
|
|
36
|
+
// to "no grant" — explicit `true` is required for unconditional access.
|
|
37
|
+
return (
|
|
38
|
+
typeof actionPerm === "object" &&
|
|
39
|
+
actionPerm !== null &&
|
|
40
|
+
Object.keys(actionPerm).length > 0
|
|
41
|
+
);
|
|
42
|
+
}
|