@lobb-js/lobb-ext-auth 0.11.0 → 0.11.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.
Files changed (48) hide show
  1. package/README.md +1 -0
  2. package/dist/auth.d.ts +2 -1
  3. package/dist/auth.js +23 -4
  4. package/dist/index.js +41 -2
  5. package/dist/lib/components/pages/settings/index.svelte +1 -1
  6. package/dist/lib/components/pages/settings/pages/activityFeed.svelte +1 -1
  7. package/dist/lib/components/pages/settings/pages/rolesAndPermissions.svelte +1 -1
  8. package/dist/lib/components/pages/settings/pages/users.svelte +1 -1
  9. package/dist/lib/components/pages/userSettings/index.svelte +45 -32
  10. package/dist/onStartup.js +17 -2
  11. package/extensions/auth/collections/collections.ts +2 -0
  12. package/extensions/auth/collections/shares.ts +60 -0
  13. package/extensions/auth/config/extensionConfigSchema.d.ts +41 -0
  14. package/extensions/auth/config/permissionsAction/create.d.ts +18 -0
  15. package/extensions/auth/config/permissionsAction/delete.d.ts +3 -0
  16. package/extensions/auth/config/permissionsAction/read.d.ts +11 -0
  17. package/extensions/auth/config/permissionsAction/update.d.ts +18 -0
  18. package/extensions/auth/index.ts +0 -2
  19. package/extensions/auth/permissions.d.ts +2 -0
  20. package/extensions/auth/permissions.ts +34 -0
  21. package/extensions/auth/studio/auth.ts +25 -5
  22. package/extensions/auth/studio/index.ts +44 -2
  23. package/extensions/auth/studio/lib/components/pages/settings/index.svelte +1 -1
  24. package/extensions/auth/studio/lib/components/pages/settings/pages/activityFeed.svelte +1 -1
  25. package/extensions/auth/studio/lib/components/pages/settings/pages/rolesAndPermissions.svelte +1 -1
  26. package/extensions/auth/studio/lib/components/pages/settings/pages/users.svelte +1 -1
  27. package/extensions/auth/studio/lib/components/pages/userSettings/index.svelte +45 -32
  28. package/extensions/auth/studio/onStartup.ts +14 -2
  29. package/extensions/auth/tests/collections/shares.test.ts +657 -0
  30. package/extensions/auth/tests/configs/auth.ts +17 -0
  31. package/extensions/auth/tests/controllers/me.test.ts +104 -0
  32. package/extensions/auth/tests/permissions.test.ts +127 -0
  33. package/extensions/auth/tests/workflows/shareIntersection.test.ts +158 -0
  34. package/extensions/auth/workflows/baseWorkflow.ts +48 -26
  35. package/extensions/auth/workflows/currentUserPermissionsWorkflow.ts +32 -0
  36. package/extensions/auth/workflows/index.ts +12 -0
  37. package/extensions/auth/workflows/meAliasWorkflows.ts +26 -0
  38. package/extensions/auth/workflows/policiesWorkflows.ts +64 -117
  39. package/extensions/auth/workflows/shareIntersection.ts +64 -0
  40. package/extensions/auth/workflows/sharesWorkflows.ts +135 -0
  41. package/extensions/auth/workflows/utils.ts +132 -224
  42. package/package.json +3 -3
  43. package/dist/lib/components/pages/userSettings/components/account.svelte +0 -106
  44. package/dist/lib/components/pages/userSettings/components/account.svelte.d.ts +0 -14
  45. package/dist/lib/components/pages/userSettings/components/profile.svelte +0 -87
  46. package/dist/lib/components/pages/userSettings/components/profile.svelte.d.ts +0 -14
  47. package/extensions/auth/studio/lib/components/pages/userSettings/components/account.svelte +0 -106
  48. 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 "../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.location.navigate("/studio/extensions/auth/user_settings");
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.location.navigate("/studio/extensions/auth/user_settings");
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.location.url.pathname.split("/")[5]);
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/30">
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
  />
@@ -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/30">
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/30">
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 Profile from "./components/profile.svelte";
4
- import Account from "./components/account.svelte";
3
+ import { DetailView } from "@lobb-js/studio";
4
+ import { onMount } from "svelte";
5
5
 
6
- const props: ExtensionProps = $props();
7
- const components = props.utils.components;
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 selectedView: "profile" | "account" = $state("profile");
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
- <components.Separator />
21
- <div class="flex gap-4 {isSmall ? "flex-col" : ""}">
22
- <div class="flex w-64 {isSmall ? "" : "flex-col"}">
23
- <components.Button
24
- onclick={() => (selectedView = "profile")}
25
- variant="ghost"
26
- class="flex justify-start text-muted-foreground hover:underline"
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
- {#if selectedView === "profile"}
39
- <Profile {...props} />
40
- {:else if selectedView === "account"}
41
- <Account {...props} />
42
- {:else}
43
- <div class="font-bold text-red-500">
44
- The "{selectedView}" view doesnt exist
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
- {/if}
47
- </div>
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
- return path === loginPath || publicPaths.some(p => path.startsWith(p));
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.location.navigate(`${loginPath}?redirect=${encodeURIComponent(path)}`);
58
+ utils.goto(`${loginPath}?redirect=${encodeURIComponent(path)}`);
47
59
  }
48
60
  }