@checkstack/auth-frontend 0.0.4 → 0.2.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.
@@ -1,95 +1,141 @@
1
1
  import { describe, it, expect, mock, beforeEach } from "bun:test";
2
2
  import { authPlugin } from "./index";
3
- import { permissionApiRef } from "@checkstack/frontend-api";
4
- import { usePermissions } from "./hooks/usePermissions";
3
+ import { accessApiRef } from "@checkstack/frontend-api";
4
+ import type { AccessRule } from "@checkstack/common";
5
+ import { useAccessRules } from "./hooks/useAccessRules";
5
6
 
6
- // Mock the usePermissions hook
7
- mock.module("./hooks/usePermissions", () => ({
8
- usePermissions: mock(),
7
+ // Mock the useAccessRules hook
8
+ mock.module("./hooks/useAccessRules", () => ({
9
+ useAccessRules: mock(),
9
10
  }));
10
11
 
11
- describe("AuthPermissionApi", () => {
12
- let permissionApi: {
13
- usePermission: (p: string) => { loading: boolean; allowed: boolean };
12
+ // Test access rule objects
13
+ const testReadAccess: AccessRule = {
14
+ id: "test.read",
15
+ resource: "test",
16
+ level: "read",
17
+ description: "Test read access",
18
+ };
19
+
20
+ const testManageAccess: AccessRule = {
21
+ id: "test.manage",
22
+ resource: "test",
23
+ level: "manage",
24
+ description: "Test manage access",
25
+ };
26
+
27
+ const otherAccess: AccessRule = {
28
+ id: "other.read",
29
+ resource: "other",
30
+ level: "read",
31
+ description: "Other read access",
32
+ };
33
+
34
+ describe("AuthAccessApi", () => {
35
+ let accessApi: {
36
+ useAccess: (p: AccessRule) => { loading: boolean; allowed: boolean };
14
37
  };
15
38
 
16
39
  beforeEach(() => {
17
- const apiDef = authPlugin.apis?.find(
18
- (a) => a.ref.id === permissionApiRef.id
19
- );
20
- if (!apiDef) throw new Error("Permission API not found in plugin");
21
- permissionApi = apiDef.factory({ get: () => ({}) } as any) as any;
40
+ const apiDef = authPlugin.apis?.find((a) => a.ref.id === accessApiRef.id);
41
+ if (!apiDef) throw new Error("Access API not found in plugin");
42
+ accessApi = apiDef.factory({ get: () => ({}) } as any) as any;
22
43
  });
23
44
 
24
- it("should return true if user has the permission", () => {
25
- (usePermissions as ReturnType<typeof mock>).mockReturnValue({
26
- permissions: ["test.permission"],
45
+ it("should return true if user has the access rule", () => {
46
+ (useAccessRules as ReturnType<typeof mock>).mockReturnValue({
47
+ accessRules: ["test.read"],
27
48
  loading: false,
28
49
  });
29
50
 
30
- expect(permissionApi.usePermission("test.permission")).toEqual({
51
+ expect(accessApi.useAccess(testReadAccess)).toEqual({
31
52
  loading: false,
32
53
  allowed: true,
33
54
  });
34
55
  });
35
56
 
36
- it("should return false if user is missing the permission", () => {
37
- (usePermissions as ReturnType<typeof mock>).mockReturnValue({
38
- permissions: ["other.permission"],
57
+ it("should return false if user is missing the access rule", () => {
58
+ (useAccessRules as ReturnType<typeof mock>).mockReturnValue({
59
+ accessRules: ["other.read"],
39
60
  loading: false,
40
61
  });
41
62
 
42
- expect(permissionApi.usePermission("test.permission")).toEqual({
63
+ expect(accessApi.useAccess(testReadAccess)).toEqual({
43
64
  loading: false,
44
65
  allowed: false,
45
66
  });
46
67
  });
47
68
 
48
- it("should return false if no session data (no permissions)", () => {
49
- (usePermissions as ReturnType<typeof mock>).mockReturnValue({
50
- permissions: [],
69
+ it("should return false if no session data (no access rules)", () => {
70
+ (useAccessRules as ReturnType<typeof mock>).mockReturnValue({
71
+ accessRules: [],
51
72
  loading: false,
52
73
  });
53
74
 
54
- expect(permissionApi.usePermission("test.permission")).toEqual({
75
+ expect(accessApi.useAccess(testReadAccess)).toEqual({
55
76
  loading: false,
56
77
  allowed: false,
57
78
  });
58
79
  });
59
80
 
60
- it("should return false if no user permissions (empty array)", () => {
61
- (usePermissions as ReturnType<typeof mock>).mockReturnValue({
62
- permissions: [],
81
+ it("should return false if no user access rules (empty array)", () => {
82
+ (useAccessRules as ReturnType<typeof mock>).mockReturnValue({
83
+ accessRules: [],
63
84
  loading: false,
64
85
  });
65
86
 
66
- expect(permissionApi.usePermission("test.permission")).toEqual({
87
+ expect(accessApi.useAccess(testReadAccess)).toEqual({
67
88
  loading: false,
68
89
  allowed: false,
69
90
  });
70
91
  });
71
92
 
72
- it("should return true if user has the wildcard permission", () => {
73
- (usePermissions as ReturnType<typeof mock>).mockReturnValue({
74
- permissions: ["*"],
93
+ it("should return true if user has the wildcard access rule", () => {
94
+ (useAccessRules as ReturnType<typeof mock>).mockReturnValue({
95
+ accessRules: ["*"],
75
96
  loading: false,
76
97
  });
77
98
 
78
- expect(permissionApi.usePermission("any.permission")).toEqual({
99
+ expect(accessApi.useAccess(otherAccess)).toEqual({
79
100
  loading: false,
80
101
  allowed: true,
81
102
  });
82
103
  });
83
104
 
84
- it("should return loading state if permissions are loading", () => {
85
- (usePermissions as ReturnType<typeof mock>).mockReturnValue({
86
- permissions: [],
105
+ it("should return true if user has manage access for a manage check", () => {
106
+ (useAccessRules as ReturnType<typeof mock>).mockReturnValue({
107
+ accessRules: ["test.manage"],
108
+ loading: false,
109
+ });
110
+
111
+ expect(accessApi.useAccess(testManageAccess)).toEqual({
112
+ loading: false,
113
+ allowed: true,
114
+ });
115
+ });
116
+
117
+ it("should return loading state if access rules are loading", () => {
118
+ (useAccessRules as ReturnType<typeof mock>).mockReturnValue({
119
+ accessRules: [],
87
120
  loading: true,
88
121
  });
89
122
 
90
- expect(permissionApi.usePermission("test.permission")).toEqual({
123
+ expect(accessApi.useAccess(testReadAccess)).toEqual({
91
124
  loading: true,
92
125
  allowed: false,
93
126
  });
94
127
  });
128
+
129
+ it("should return true if user has manage access for a read check", () => {
130
+ (useAccessRules as ReturnType<typeof mock>).mockReturnValue({
131
+ accessRules: ["test.manage"],
132
+ loading: false,
133
+ });
134
+
135
+ // User has test.manage, which implies test.read
136
+ expect(accessApi.useAccess(testReadAccess)).toEqual({
137
+ loading: false,
138
+ allowed: true,
139
+ });
140
+ });
95
141
  });
package/src/index.tsx CHANGED
@@ -1,8 +1,8 @@
1
1
  import React from "react";
2
2
  import {
3
3
  ApiRef,
4
- permissionApiRef,
5
- PermissionApi,
4
+ accessApiRef,
5
+ AccessApi,
6
6
  createFrontendPlugin,
7
7
  createSlotExtension,
8
8
  NavbarRightSlot,
@@ -22,71 +22,52 @@ import { ChangePasswordPage } from "./components/ChangePasswordPage";
22
22
  import { authApiRef, AuthApi, AuthSession } from "./api";
23
23
  import { getAuthClientLazy } from "./lib/auth-client";
24
24
 
25
- import { usePermissions } from "./hooks/usePermissions";
25
+ import { useAccessRules } from "./hooks/useAccessRules";
26
26
 
27
- import { PermissionAction, qualifyPermissionId } from "@checkstack/common";
27
+ import type { AccessRule } from "@checkstack/common";
28
28
  import { useNavigate } from "react-router-dom";
29
29
  import { Settings2, Key } from "lucide-react";
30
30
  import { DropdownMenuItem } from "@checkstack/ui";
31
31
  import { UserMenuItemsContext } from "@checkstack/frontend-api";
32
32
  import { AuthSettingsPage } from "./components/AuthSettingsPage";
33
33
  import {
34
- permissions as authPermissions,
34
+ authAccess,
35
35
  authRoutes,
36
36
  pluginMetadata,
37
37
  } from "@checkstack/auth-common";
38
38
  import { resolveRoute } from "@checkstack/common";
39
39
 
40
- class AuthPermissionApi implements PermissionApi {
41
- usePermission(permission: string): { loading: boolean; allowed: boolean } {
42
- const { permissions, loading } = usePermissions();
43
-
44
- if (loading) {
45
- return { loading: true, allowed: false };
46
- }
47
-
48
- // If no user, or user has no permissions, return false
49
- if (!permissions || permissions.length === 0) {
50
- return { loading: false, allowed: false };
51
- }
52
- const allowed =
53
- permissions.includes("*") || permissions.includes(permission);
54
- return { loading: false, allowed };
55
- }
56
-
57
- useResourcePermission(
58
- resource: string,
59
- action: PermissionAction
60
- ): { loading: boolean; allowed: boolean } {
61
- const { permissions, loading } = usePermissions();
40
+ /**
41
+ * Unified access API implementation.
42
+ * Uses AccessRule objects for access checks.
43
+ */
44
+ class AuthAccessApi implements AccessApi {
45
+ useAccess(accessRule: AccessRule): { loading: boolean; allowed: boolean } {
46
+ const { accessRules, loading } = useAccessRules();
62
47
 
63
48
  if (loading) {
64
49
  return { loading: true, allowed: false };
65
50
  }
66
51
 
67
- if (!permissions || permissions.length === 0) {
52
+ // If no user, or user has no access rules, return false
53
+ if (!accessRules || accessRules.length === 0) {
68
54
  return { loading: false, allowed: false };
69
55
  }
70
56
 
71
- const isWildcard = permissions.includes("*");
72
- const hasResourceManage = permissions.includes(`${resource}.manage`);
73
- const hasSpecificPermission = permissions.includes(`${resource}.${action}`);
57
+ const accessRuleId = accessRule.id;
74
58
 
75
- // manage implies read
76
- const isAllowed =
77
- isWildcard ||
78
- hasResourceManage ||
79
- (action === "read" && hasResourceManage) ||
80
- hasSpecificPermission;
59
+ // Check wildcard, exact match, or manage implies read
60
+ const isWildcard = accessRules.includes("*");
61
+ const hasExact = accessRules.includes(accessRuleId);
81
62
 
82
- return { loading: false, allowed: isAllowed };
83
- }
63
+ // For read actions, also check if user has manage access for the same resource
64
+ const hasManage =
65
+ accessRule.level === "read"
66
+ ? accessRules.includes(`${accessRule.resource}.manage`)
67
+ : false;
84
68
 
85
- useManagePermission(resource: string): {
86
- loading: boolean;
87
- allowed: boolean;
88
- } {
89
- return this.useResourcePermission(resource, "manage");
69
+ const allowed = isWildcard || hasExact || hasManage;
70
+ return { loading: false, allowed };
90
71
  }
91
72
  }
92
73
 
@@ -168,6 +149,10 @@ class BetterAuthApi implements AuthApi {
168
149
  }
169
150
  }
170
151
 
152
+ // Re-export TeamAccessEditor for use in other plugins
153
+ export { TeamAccessEditor } from "./components/TeamAccessEditor";
154
+ export type { TeamAccessEditorProps } from "./components/TeamAccessEditor";
155
+
171
156
  export const authPlugin = createFrontendPlugin({
172
157
  metadata: pluginMetadata,
173
158
  apis: [
@@ -176,8 +161,8 @@ export const authPlugin = createFrontendPlugin({
176
161
  factory: () => new BetterAuthApi(),
177
162
  },
178
163
  {
179
- ref: permissionApiRef as ApiRef<unknown>,
180
- factory: () => new AuthPermissionApi(),
164
+ ref: accessApiRef as ApiRef<unknown>,
165
+ factory: () => new AuthAccessApi(),
181
166
  },
182
167
  ],
183
168
  routes: [
@@ -218,12 +203,9 @@ export const authPlugin = createFrontendPlugin({
218
203
  },
219
204
  createSlotExtension(UserMenuItemsSlot, {
220
205
  id: "auth.user-menu.settings",
221
- component: ({ permissions: userPerms }: UserMenuItemsContext) => {
206
+ component: ({ accessRules: userPerms }: UserMenuItemsContext) => {
222
207
  const navigate = useNavigate();
223
- const qualifiedId = qualifyPermissionId(
224
- pluginMetadata,
225
- authPermissions.strategiesManage
226
- );
208
+ const qualifiedId = `${pluginMetadata.pluginId}.${authAccess.strategies.id}`;
227
209
  const canManage =
228
210
  userPerms.includes("*") || userPerms.includes(qualifiedId);
229
211