@lobb-js/lobb-ext-auth 0.10.4 → 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 (56) 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 +4 -6
  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/dist/tests/login.spec.d.ts +0 -1
  48. package/dist/tests/login.spec.js +0 -27
  49. package/dist/tests/package.json +0 -1
  50. package/dist/tests/playwright.config.cjs +0 -27
  51. package/dist/tests/playwright.config.d.cts +0 -2
  52. package/extensions/auth/studio/lib/components/pages/userSettings/components/account.svelte +0 -106
  53. package/extensions/auth/studio/lib/components/pages/userSettings/components/profile.svelte +0 -87
  54. package/extensions/auth/studio/tests/login.spec.ts +0 -34
  55. package/extensions/auth/studio/tests/package.json +0 -1
  56. package/extensions/auth/studio/tests/playwright.config.cjs +0 -27
package/README.md CHANGED
@@ -1,2 +1,3 @@
1
1
  # @lobb-js/lobb-ext-auth
2
2
 
3
+
package/dist/auth.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ExtensionUtils } from "@lobb-js/studio";
1
+ import { type ExtensionUtils } from "@lobb-js/studio";
2
2
  interface LoginPayload {
3
3
  email: string;
4
4
  password: string;
@@ -9,5 +9,6 @@ export declare class Auth {
9
9
  getSession(): any;
10
10
  login(payload: LoginPayload): Promise<void>;
11
11
  logout(): Promise<void>;
12
+ fetchMe(): Promise<void>;
12
13
  }
13
14
  export {};
package/dist/auth.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { remountStudio } from "@lobb-js/studio";
1
2
  export class Auth {
2
3
  utils;
3
4
  constructor(utils) {
@@ -27,12 +28,16 @@ export class Auth {
27
28
  const result = await response.json();
28
29
  const session = result.data;
29
30
  localStorage.setItem("lobb_session", JSON.stringify(session));
30
- this.utils.ctx.extensions.auth.session = session;
31
31
  this.utils.lobb.setHeaders({
32
32
  Authorization: `Bearer ${session.access_token.token}`,
33
33
  });
34
34
  const redirectTo = new URLSearchParams(window.location.search).get("redirect");
35
- this.utils.location.navigate(redirectTo || "/studio");
35
+ this.utils.goto(redirectTo || "/studio");
36
+ // Remount the Studio so onStartup re-runs and populates ctx with the
37
+ // freshly logged-in user's /me data. Everything inside Studio (sidebar,
38
+ // extensions, etc.) then initialises against the new session without
39
+ // needing per-component reactivity on auth state.
40
+ remountStudio();
36
41
  }
37
42
  async logout() {
38
43
  await this.utils.lobb.request({
@@ -43,10 +48,24 @@ export class Auth {
43
48
  const currentPath = window.location.pathname;
44
49
  const loginPath = "/studio/extensions/auth/login_page";
45
50
  if (currentPath !== loginPath && currentPath !== "/studio") {
46
- this.utils.location.navigate(`${loginPath}?redirect=${encodeURIComponent(currentPath)}`);
51
+ this.utils.goto(`${loginPath}?redirect=${encodeURIComponent(currentPath)}`);
47
52
  }
48
53
  else {
49
- this.utils.location.navigate(loginPath);
54
+ this.utils.goto(loginPath);
50
55
  }
56
+ // Remount so onStartup re-runs against the cleared session — clears any
57
+ // stale ctx.extensions.auth.user / permissions from the previous user.
58
+ remountStudio();
59
+ }
60
+ async fetchMe() {
61
+ const response = await this.utils.lobb.request({
62
+ method: "GET",
63
+ route: "/api/collections/auth_users/me",
64
+ });
65
+ if (response.status >= 400)
66
+ throw new Error("Failed to fetch /me");
67
+ const result = await response.json();
68
+ this.utils.ctx.extensions.auth.user = result.data;
69
+ this.utils.ctx.extensions.auth.permissions = result.permissions ?? {};
51
70
  }
52
71
  }
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { onStartup, onRouteChange } from "./onStartup";
3
3
  import LoginPage from "./lib/components/pages/loginPage/index.svelte";
4
4
  import UserSettings from "./lib/components/pages/userSettings/index.svelte";
5
5
  import Settings from "./lib/components/pages/settings/index.svelte";
6
+ import { isActionAllowed } from "../permissions";
6
7
  // TODO we should export the extension object directly without a function at all. because we dont set configuration in here
7
8
  export default function extension(utils) {
8
9
  return {
@@ -14,12 +15,50 @@ export default function extension(utils) {
14
15
  "pages.user_settings": UserSettings,
15
16
  "pages.settings": Settings,
16
17
  },
18
+ workflows: [
19
+ {
20
+ // Generic auth primitive: callers ask "can the current user access X?"
21
+ // and pass any combination of { collection, action, role }. The handler
22
+ // returns a boolean directly; callers read that and decide what to render.
23
+ // Note: returning a boolean means downstream handlers can't read the
24
+ // original request — if you ever need multiple extensions to influence
25
+ // this decision, switch back to a `{ ...input, allowed }` object shape.
26
+ eventName: "auth.canAccess",
27
+ handler: async (input) => {
28
+ // Admin shortcut — admins bypass everything, regardless of how
29
+ // their permissions snapshot was populated.
30
+ const user = utils.ctx.extensions.auth?.user;
31
+ if (user?.role === "admin")
32
+ return true;
33
+ // Walk the permissions snapshot. Works for both logged-in non-admin
34
+ // users (snapshot from their role) and share-token recipients
35
+ // (snapshot from the share's embedded permissions). If nothing
36
+ // is populated — anonymous viewer — default to deny.
37
+ const permissions = utils.ctx.extensions.auth?.permissions;
38
+ if (permissions === undefined || permissions === null)
39
+ return false;
40
+ if (permissions === true)
41
+ return true;
42
+ const action = input.action ?? "read";
43
+ const requiredCollections = Array.isArray(input.collection)
44
+ ? input.collection
45
+ : input.collection ? [input.collection] : [];
46
+ for (const collectionName of requiredCollections) {
47
+ if (!isActionAllowed(collectionName, action, permissions)) {
48
+ return false;
49
+ }
50
+ }
51
+ return true;
52
+ },
53
+ },
54
+ ],
17
55
  dashboardNavs: {
18
56
  middle: [
19
57
  {
20
58
  label: "Auth",
21
59
  icon: utils.components.Icons.Key,
22
60
  href: "/studio/extensions/auth/settings/users",
61
+ represents: "auth_users",
23
62
  },
24
63
  ],
25
64
  bottom: [
@@ -27,14 +66,14 @@ export default function extension(utils) {
27
66
  label: "My User",
28
67
  icon: utils.components.Icons.User,
29
68
  onclick: () => {
30
- utils.location.navigate("/studio/extensions/auth/user_settings");
69
+ utils.goto("/studio/extensions/auth/user_settings");
31
70
  },
32
71
  navs: [
33
72
  {
34
73
  label: "User Settings",
35
74
  icon: utils.components.Icons.Settings,
36
75
  onclick: () => {
37
- utils.location.navigate("/studio/extensions/auth/user_settings");
76
+ utils.goto("/studio/extensions/auth/user_settings");
38
77
  },
39
78
  },
40
79
  {
@@ -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>
package/dist/onStartup.js CHANGED
@@ -1,7 +1,16 @@
1
1
  import { Auth } from "./auth";
2
2
  const loginPath = "/studio/extensions/auth/login_page";
3
+ const publicPagesPrefix = "/studio/public/";
3
4
  function isPublicPath(path, publicPaths) {
4
- return path === loginPath || publicPaths.some(p => path.startsWith(p));
5
+ // Anything under /studio/public/ is a publicPages.* route — registered by
6
+ // an extension and intentionally exposed without auth (e.g. share-recipient
7
+ // views). Falls through to the app-level publicPaths config for legacy
8
+ // overrides.
9
+ if (path === loginPath)
10
+ return true;
11
+ if (path.startsWith(publicPagesPrefix))
12
+ return true;
13
+ return publicPaths.some(p => path.startsWith(p));
5
14
  }
6
15
  export async function onStartup(utils) {
7
16
  const auth = new Auth(utils);
@@ -18,6 +27,12 @@ export async function onStartup(utils) {
18
27
  const session = auth.getSession();
19
28
  if (session) {
20
29
  utils.ctx.extensions.auth.session = session;
30
+ try {
31
+ await auth.fetchMe();
32
+ }
33
+ catch {
34
+ await auth.logout();
35
+ }
21
36
  return;
22
37
  }
23
38
  const publicPaths = utils.ctx.meta?.extensions?.auth?.studio?.publicPaths ?? [];
@@ -32,6 +47,6 @@ export function onRouteChange(utils, path) {
32
47
  return;
33
48
  const publicPaths = utils.ctx.meta?.extensions?.auth?.studio?.publicPaths ?? [];
34
49
  if (!isPublicPath(path, publicPaths)) {
35
- utils.location.navigate(`${loginPath}?redirect=${encodeURIComponent(path)}`);
50
+ utils.goto(`${loginPath}?redirect=${encodeURIComponent(path)}`);
36
51
  }
37
52
  }
@@ -1,6 +1,7 @@
1
1
  import type { CollectionConfig, Lobb } from "@lobb-js/core";
2
2
  import { usersCollection } from "./users.ts";
3
3
  import { sessionsCollection } from "./sessions.ts";
4
+ import { sharesCollection } from "./shares.ts";
4
5
  import type { ExtensionConfig } from "../config/extensionConfigSchema.ts";
5
6
  import { activityFeedCollection } from "./activityFeed.ts";
6
7
 
@@ -11,6 +12,7 @@ export function collections(
11
12
  const collectionsSchemas: Record<string, CollectionConfig> = {};
12
13
  collectionsSchemas["auth_users"] = usersCollection;
13
14
  collectionsSchemas["auth_sessions"] = sessionsCollection;
15
+ collectionsSchemas["auth_shares"] = sharesCollection;
14
16
 
15
17
  // Convert the role field to an enum based on configured roles
16
18
  const roleNames = Object.keys(extensionConfig.roles ?? {}).filter(r => r !== "public");
@@ -0,0 +1,60 @@
1
+ import type { CollectionConfig } from "@lobb-js/core";
2
+
3
+ // A share is a bearer credential that grants its holder a specific snapshot
4
+ // of permissions until a fixed expiry. Unlike a session it isn't tied to a
5
+ // user identity — anyone with the token gets exactly the listed permissions.
6
+ //
7
+ // `permissions` is stored as text holding a JSON-serialised PermissionsConfig
8
+ // (same shape as a role's permissions in the auth extension config).
9
+ export const sharesCollection: CollectionConfig = {
10
+ indexes: {},
11
+ fields: {
12
+ id: {
13
+ type: "integer",
14
+ },
15
+ // Auto-generated server-side by the auth_generateShareToken workflow on
16
+ // create — clients should not supply this; the generated token comes
17
+ // back in the create response.
18
+ token: {
19
+ type: "string",
20
+ length: 255,
21
+ unique: true,
22
+ },
23
+ label: {
24
+ type: "string",
25
+ length: 255,
26
+ },
27
+ permissions: {
28
+ type: "text",
29
+ required: true,
30
+ },
31
+ // Absolute expiry timestamp — the source of truth stored in the DB and
32
+ // used by every-request validity checks and the periodic cleanup.
33
+ // Required at the schema level; callers can pass `expires_in_seconds`
34
+ // instead, which the auth_normalizeShareExpiry workflow converts to
35
+ // this field at the service layer (before the store-level required
36
+ // check fires).
37
+ expires_at: {
38
+ type: "datetime",
39
+ required: true,
40
+ },
41
+ // Convenience write-only field: instead of computing an absolute
42
+ // timestamp, callers can pass a duration in seconds. Declared as a
43
+ // virtual integer so it shows up in the OpenAPI schema for documentation
44
+ // but never gets a database column — the workflow converts it to
45
+ // expires_at and strips it before insert.
46
+ expires_in_seconds: {
47
+ type: "integer",
48
+ virtual: true,
49
+ ui: { hidden: true },
50
+ },
51
+ created_by: {
52
+ type: "integer",
53
+ references: {
54
+ collection: "auth_users",
55
+ field: "id",
56
+ },
57
+ required: true,
58
+ },
59
+ },
60
+ };
@@ -0,0 +1,41 @@
1
+ import type { CollectionConfig } from "@lobb-js/core";
2
+ import type { CreatePermissionAction } from "./permissionsAction/create.ts";
3
+ import type { ReadPermissionAction } from "./permissionsAction/read.ts";
4
+ import type { UpdatePermissionAction } from "./permissionsAction/update.ts";
5
+ import type { DeletePermissionAction } from "./permissionsAction/delete.ts";
6
+ export interface User {
7
+ id: number;
8
+ email: string;
9
+ password: string;
10
+ role: string;
11
+ }
12
+ export interface PermissionAction {
13
+ }
14
+ export type CollectionPermissionsActions = {
15
+ create?: true | CreatePermissionAction;
16
+ read?: true | ReadPermissionAction;
17
+ update?: true | UpdatePermissionAction;
18
+ delete?: true | DeletePermissionAction;
19
+ };
20
+ export type CollectionPermissionActionsKeys = keyof CollectionPermissionsActions;
21
+ export type CollectionPermissionsConfig = true | CollectionPermissionsActions;
22
+ export type PermissionsConfig = true | Record<string, CollectionPermissionsConfig | undefined>;
23
+ export type RolesConfig = {
24
+ permissions: PermissionsConfig;
25
+ };
26
+ export type ExtensionConfig = {
27
+ admin: {
28
+ password: string;
29
+ email: string;
30
+ [key: string]: any;
31
+ };
32
+ dashboard_access_roles?: string[];
33
+ roles?: Record<string, RolesConfig | undefined>;
34
+ extend_users?: {
35
+ indexes?: CollectionConfig["indexes"];
36
+ fields?: Omit<CollectionConfig["fields"], "id">;
37
+ };
38
+ studio?: {
39
+ publicPaths?: string[];
40
+ };
41
+ };
@@ -0,0 +1,18 @@
1
+ import type { PermissionAction, User } from "../extensionConfigSchema.ts";
2
+ interface FieldTrasnsformerFnParams {
3
+ value: unknown;
4
+ payload: Record<string, unknown>;
5
+ user?: User;
6
+ }
7
+ type FieldTrasnsformerFn = (params: FieldTrasnsformerFnParams) => unknown;
8
+ interface CreateGuardProps {
9
+ payload: Record<string, unknown>;
10
+ user?: User;
11
+ }
12
+ type CreateGuardFn = (props: CreateGuardProps) => true | void;
13
+ export interface CreatePermissionAction extends PermissionAction {
14
+ payloadGuard?: CreateGuardFn;
15
+ fields?: Record<string, true>;
16
+ mutate?: Record<string, FieldTrasnsformerFn>;
17
+ }
18
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { PermissionAction } from "../extensionConfigSchema.ts";
2
+ export interface DeletePermissionAction extends PermissionAction {
3
+ }
@@ -0,0 +1,11 @@
1
+ import type { Filter } from "@lobb-js/core";
2
+ import type { PermissionAction, User } from "../extensionConfigSchema.ts";
3
+ export interface ReadPermissionAction extends PermissionAction {
4
+ /**
5
+ * Filter that gets passed to the db select query condition
6
+ */
7
+ filter?: Filter<{
8
+ user?: User;
9
+ }>;
10
+ fields?: Record<string, true>;
11
+ }
@@ -0,0 +1,18 @@
1
+ import type { PermissionAction, User } from "../extensionConfigSchema.ts";
2
+ interface FieldTrasnsformerFnParams {
3
+ value: unknown;
4
+ payload: Record<string, unknown>;
5
+ user?: User;
6
+ }
7
+ type FieldTrasnsformerFn = (params: FieldTrasnsformerFnParams) => unknown;
8
+ interface UpdateGuardProps {
9
+ payload: Record<string, unknown>;
10
+ user?: User;
11
+ }
12
+ type UpdateGuardFn = (props: UpdateGuardProps) => true | void;
13
+ export interface UpdatePermissionAction extends PermissionAction {
14
+ payloadGuard?: UpdateGuardFn;
15
+ fields?: Record<string, true>;
16
+ mutate?: Record<string, FieldTrasnsformerFn>;
17
+ }
18
+ export {};
@@ -1,7 +1,6 @@
1
1
  import type { Extension } from "@lobb-js/core";
2
2
  import type { ExtensionConfig } from "./config/extensionConfigSchema.ts";
3
3
 
4
- import { init } from "./database/init.ts";
5
4
  import { collections } from "./collections/collections.ts";
6
5
  import { meta } from "./meta/meta.ts";
7
6
  import { migrations } from "./database/migrations.ts";
@@ -11,7 +10,6 @@ export default function auth(extensionConfig: ExtensionConfig): Extension {
11
10
  return {
12
11
  name: "auth",
13
12
  icon: "Key",
14
- init: (lobb) => init(lobb, extensionConfig),
15
13
  collections: (lobb) => collections(lobb, extensionConfig),
16
14
  migrations: migrations,
17
15
  meta: (lobb) => meta(lobb, extensionConfig),
@@ -0,0 +1,2 @@
1
+ import type { CollectionPermissionActionsKeys, PermissionsConfig } from "./config/extensionConfigSchema.ts";
2
+ export declare function isActionAllowed(collection: string, action: CollectionPermissionActionsKeys, permissions: PermissionsConfig | undefined): boolean;
@@ -0,0 +1,34 @@
1
+ import type {
2
+ CollectionPermissionActionsKeys,
3
+ PermissionsConfig,
4
+ } from "./config/extensionConfigSchema.ts";
5
+
6
+ // Pure permission check shared by the server-side handlePolicy and the
7
+ // studio's auth.canAccess workflow. Walks the permissions tree and returns
8
+ // a yes/no — server-side then layers on guards/filters/error-throwing,
9
+ // client-side uses the answer to decide whether to render UI.
10
+ //
11
+ // An object value for action permission means "conditional access" (filter
12
+ // and/or fields restrictions). For UI gating that counts as allowed; the
13
+ // server enforces the actual restrictions on real requests.
14
+ export function isActionAllowed(
15
+ collection: string,
16
+ action: CollectionPermissionActionsKeys,
17
+ permissions: PermissionsConfig | undefined,
18
+ ): boolean {
19
+ if (permissions === true) return true;
20
+ if (!permissions) return false;
21
+ const collPerm = permissions[collection];
22
+ if (collPerm === true) return true;
23
+ if (!collPerm) return false;
24
+ const actionPerm = collPerm[action];
25
+ if (actionPerm === true) return true;
26
+ // A conditional grant must actually list at least one constraint
27
+ // (filter / fields / payloadGuard / mutate). Empty `{}` collapses back
28
+ // to "no grant" — explicit `true` is required for unconditional access.
29
+ return (
30
+ typeof actionPerm === "object" &&
31
+ actionPerm !== null &&
32
+ Object.keys(actionPerm).length > 0
33
+ );
34
+ }
@@ -1,4 +1,4 @@
1
- import type { ExtensionUtils } from "@lobb-js/studio";
1
+ import { remountStudio, type ExtensionUtils } from "@lobb-js/studio";
2
2
 
3
3
  interface LoginPayload {
4
4
  email: string;
@@ -39,12 +39,18 @@ export class Auth {
39
39
  const session = result.data;
40
40
  localStorage.setItem("lobb_session", JSON.stringify(session));
41
41
 
42
- this.utils.ctx.extensions.auth.session = session;
43
42
  this.utils.lobb.setHeaders({
44
43
  Authorization: `Bearer ${session.access_token.token}`,
45
44
  });
45
+
46
46
  const redirectTo = new URLSearchParams(window.location.search).get("redirect");
47
- this.utils.location.navigate(redirectTo || "/studio");
47
+ this.utils.goto(redirectTo || "/studio");
48
+
49
+ // Remount the Studio so onStartup re-runs and populates ctx with the
50
+ // freshly logged-in user's /me data. Everything inside Studio (sidebar,
51
+ // extensions, etc.) then initialises against the new session without
52
+ // needing per-component reactivity on auth state.
53
+ remountStudio();
48
54
  }
49
55
 
50
56
  public async logout() {
@@ -56,9 +62,23 @@ export class Auth {
56
62
  const currentPath = window.location.pathname;
57
63
  const loginPath = "/studio/extensions/auth/login_page";
58
64
  if (currentPath !== loginPath && currentPath !== "/studio") {
59
- this.utils.location.navigate(`${loginPath}?redirect=${encodeURIComponent(currentPath)}`);
65
+ this.utils.goto(`${loginPath}?redirect=${encodeURIComponent(currentPath)}`);
60
66
  } else {
61
- this.utils.location.navigate(loginPath);
67
+ this.utils.goto(loginPath);
62
68
  }
69
+ // Remount so onStartup re-runs against the cleared session — clears any
70
+ // stale ctx.extensions.auth.user / permissions from the previous user.
71
+ remountStudio();
72
+ }
73
+
74
+ public async fetchMe(): Promise<void> {
75
+ const response = await this.utils.lobb.request({
76
+ method: "GET",
77
+ route: "/api/collections/auth_users/me",
78
+ });
79
+ if (response.status >= 400) throw new Error("Failed to fetch /me");
80
+ const result = await response.json();
81
+ this.utils.ctx.extensions.auth.user = result.data;
82
+ this.utils.ctx.extensions.auth.permissions = result.permissions ?? {};
63
83
  }
64
84
  }