@checkstack/auth-frontend 0.0.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/src/index.tsx ADDED
@@ -0,0 +1,271 @@
1
+ import React from "react";
2
+ import {
3
+ ApiRef,
4
+ permissionApiRef,
5
+ PermissionApi,
6
+ createFrontendPlugin,
7
+ createSlotExtension,
8
+ NavbarRightSlot,
9
+ UserMenuItemsSlot,
10
+ UserMenuItemsBottomSlot,
11
+ } from "@checkstack/frontend-api";
12
+ import {
13
+ LoginPage,
14
+ LoginNavbarAction,
15
+ LogoutMenuItem,
16
+ } from "./components/LoginPage";
17
+ import { RegisterPage } from "./components/RegisterPage";
18
+ import { AuthErrorPage } from "./components/AuthErrorPage";
19
+ import { ForgotPasswordPage } from "./components/ForgotPasswordPage";
20
+ import { ResetPasswordPage } from "./components/ResetPasswordPage";
21
+ import { ChangePasswordPage } from "./components/ChangePasswordPage";
22
+ import { authApiRef, AuthApi, AuthSession } from "./api";
23
+ import { getAuthClientLazy } from "./lib/auth-client";
24
+
25
+ import { usePermissions } from "./hooks/usePermissions";
26
+
27
+ import {
28
+ PermissionAction,
29
+ qualifyPermissionId,
30
+ } from "@checkstack/common";
31
+ import { useNavigate } from "react-router-dom";
32
+ import { Settings2, Key } from "lucide-react";
33
+ import { DropdownMenuItem } from "@checkstack/ui";
34
+ import { UserMenuItemsContext } from "@checkstack/frontend-api";
35
+ import { AuthSettingsPage } from "./components/AuthSettingsPage";
36
+ import {
37
+ permissions as authPermissions,
38
+ authRoutes,
39
+ pluginMetadata,
40
+ } from "@checkstack/auth-common";
41
+ import { resolveRoute } from "@checkstack/common";
42
+
43
+ class AuthPermissionApi implements PermissionApi {
44
+ usePermission(permission: string): { loading: boolean; allowed: boolean } {
45
+ const { permissions, loading } = usePermissions();
46
+
47
+ if (loading) {
48
+ return { loading: true, allowed: false };
49
+ }
50
+
51
+ // If no user, or user has no permissions, return false
52
+ if (!permissions || permissions.length === 0) {
53
+ return { loading: false, allowed: false };
54
+ }
55
+ const allowed =
56
+ permissions.includes("*") || permissions.includes(permission);
57
+ return { loading: false, allowed };
58
+ }
59
+
60
+ useResourcePermission(
61
+ resource: string,
62
+ action: PermissionAction
63
+ ): { loading: boolean; allowed: boolean } {
64
+ const { permissions, loading } = usePermissions();
65
+
66
+ if (loading) {
67
+ return { loading: true, allowed: false };
68
+ }
69
+
70
+ if (!permissions || permissions.length === 0) {
71
+ return { loading: false, allowed: false };
72
+ }
73
+
74
+ const isWildcard = permissions.includes("*");
75
+ const hasResourceManage = permissions.includes(`${resource}.manage`);
76
+ const hasSpecificPermission = permissions.includes(`${resource}.${action}`);
77
+
78
+ // manage implies read
79
+ const isAllowed =
80
+ isWildcard ||
81
+ hasResourceManage ||
82
+ (action === "read" && hasResourceManage) ||
83
+ hasSpecificPermission;
84
+
85
+ return { loading: false, allowed: isAllowed };
86
+ }
87
+
88
+ useManagePermission(resource: string): {
89
+ loading: boolean;
90
+ allowed: boolean;
91
+ } {
92
+ return this.useResourcePermission(resource, "manage");
93
+ }
94
+ }
95
+
96
+ /**
97
+ * BetterAuthApi wraps only better-auth client methods.
98
+ * For RPC calls, use rpcApiRef.forPlugin<AuthClient>("auth") directly.
99
+ */
100
+ class BetterAuthApi implements AuthApi {
101
+ async signIn(email: string, password: string) {
102
+ const res = await getAuthClientLazy().signIn.email({ email, password });
103
+ if (res.error) {
104
+ const error = new Error(res.error.message || res.error.statusText);
105
+ error.name = res.error.code || "AuthError";
106
+ return { data: undefined, error };
107
+ }
108
+
109
+ const data = res.data as typeof res.data & {
110
+ session?: AuthSession["session"];
111
+ };
112
+ return {
113
+ data: {
114
+ session: data.session || {
115
+ token: data.token,
116
+ id: "session-id",
117
+ userId: data.user.id,
118
+ expiresAt: new Date(),
119
+ },
120
+ user: data.user,
121
+ } as AuthSession,
122
+ error: undefined,
123
+ };
124
+ }
125
+
126
+ async signInWithSocial(provider: string) {
127
+ // Use current origin as callback URL (works in dev and production)
128
+ const frontendUrl = globalThis.location?.origin || "http://localhost:5173";
129
+ await getAuthClientLazy().signIn.social({
130
+ provider,
131
+ callbackURL: frontendUrl,
132
+ errorCallbackURL: `${frontendUrl}${resolveRoute(
133
+ authRoutes.routes.error
134
+ )}`,
135
+ });
136
+ }
137
+
138
+ async signOut() {
139
+ await getAuthClientLazy().signOut({
140
+ fetchOptions: {
141
+ onSuccess: () => {
142
+ // Redirect to frontend root after successful logout
143
+ globalThis.location.href = "/";
144
+ },
145
+ },
146
+ });
147
+ }
148
+
149
+ async getSession() {
150
+ const res = await getAuthClientLazy().getSession();
151
+ if (res.error) {
152
+ const error = new Error(res.error.message || res.error.statusText);
153
+ error.name = res.error.code || "AuthError";
154
+ return { data: undefined, error };
155
+ }
156
+ if (!res.data) return { data: undefined, error: undefined };
157
+
158
+ return {
159
+ data: res.data as AuthSession,
160
+ error: undefined,
161
+ };
162
+ }
163
+
164
+ useSession() {
165
+ const { data, isPending, error } = getAuthClientLazy().useSession();
166
+ return {
167
+ data: data as AuthSession | undefined,
168
+ isPending,
169
+ error: error as Error | undefined,
170
+ };
171
+ }
172
+ }
173
+
174
+ export const authPlugin = createFrontendPlugin({
175
+ metadata: pluginMetadata,
176
+ apis: [
177
+ {
178
+ ref: authApiRef as ApiRef<unknown>,
179
+ factory: () => new BetterAuthApi(),
180
+ },
181
+ {
182
+ ref: permissionApiRef as ApiRef<unknown>,
183
+ factory: () => new AuthPermissionApi(),
184
+ },
185
+ ],
186
+ routes: [
187
+ {
188
+ route: authRoutes.routes.login,
189
+ element: <LoginPage />,
190
+ },
191
+ {
192
+ route: authRoutes.routes.register,
193
+ element: <RegisterPage />,
194
+ },
195
+ {
196
+ route: authRoutes.routes.error,
197
+ element: <AuthErrorPage />,
198
+ },
199
+ {
200
+ route: authRoutes.routes.settings,
201
+ element: <AuthSettingsPage />,
202
+ },
203
+ {
204
+ route: authRoutes.routes.forgotPassword,
205
+ element: <ForgotPasswordPage />,
206
+ },
207
+ {
208
+ route: authRoutes.routes.resetPassword,
209
+ element: <ResetPasswordPage />,
210
+ },
211
+ {
212
+ route: authRoutes.routes.changePassword,
213
+ element: <ChangePasswordPage />,
214
+ },
215
+ ],
216
+ extensions: [
217
+ {
218
+ id: "auth.navbar.action",
219
+ slot: NavbarRightSlot,
220
+ component: LoginNavbarAction,
221
+ },
222
+ createSlotExtension(UserMenuItemsSlot, {
223
+ id: "auth.user-menu.settings",
224
+ component: ({ permissions: userPerms }: UserMenuItemsContext) => {
225
+ const navigate = useNavigate();
226
+ const qualifiedId = qualifyPermissionId(
227
+ pluginMetadata,
228
+ authPermissions.strategiesManage
229
+ );
230
+ const canManage =
231
+ userPerms.includes("*") || userPerms.includes(qualifiedId);
232
+
233
+ if (!canManage) return <React.Fragment />;
234
+
235
+ return (
236
+ <DropdownMenuItem
237
+ onClick={() => navigate(resolveRoute(authRoutes.routes.settings))}
238
+ icon={<Settings2 className="h-4 w-4" />}
239
+ >
240
+ Auth Settings
241
+ </DropdownMenuItem>
242
+ );
243
+ },
244
+ }),
245
+ createSlotExtension(UserMenuItemsSlot, {
246
+ id: "auth.user-menu.change-password",
247
+ component: ({ hasCredentialAccount }: UserMenuItemsContext) => {
248
+ const navigate = useNavigate();
249
+
250
+ // Only show for credential-authenticated users
251
+ // The changePassword API requires current password, so only credential users can use it
252
+ if (!hasCredentialAccount) return <React.Fragment />;
253
+
254
+ return (
255
+ <DropdownMenuItem
256
+ onClick={() =>
257
+ navigate(resolveRoute(authRoutes.routes.changePassword))
258
+ }
259
+ icon={<Key className="h-4 w-4" />}
260
+ >
261
+ Change Password
262
+ </DropdownMenuItem>
263
+ );
264
+ },
265
+ }),
266
+ createSlotExtension(UserMenuItemsBottomSlot, {
267
+ id: "auth.user-menu.logout",
268
+ component: LogoutMenuItem,
269
+ }),
270
+ ],
271
+ });
@@ -0,0 +1,55 @@
1
+ import { useMemo } from "react";
2
+ import { createAuthClient } from "better-auth/react";
3
+ import { useRuntimeConfig } from "@checkstack/frontend-api";
4
+
5
+ // Cache for lazy-initialized client
6
+ let cachedClient: ReturnType<typeof createAuthClient> | undefined;
7
+ let configPromise: Promise<string> | undefined;
8
+
9
+ /**
10
+ * React hook to get the auth client with proper runtime config.
11
+ * Uses RuntimeConfigProvider to get the base URL.
12
+ */
13
+ export function useAuthClient() {
14
+ const { baseUrl } = useRuntimeConfig();
15
+
16
+ return useMemo(
17
+ () =>
18
+ createAuthClient({
19
+ baseURL: baseUrl,
20
+ basePath: "/api/auth",
21
+ }),
22
+ [baseUrl]
23
+ );
24
+ }
25
+
26
+ /**
27
+ * Lazy-initialized auth client for class-based APIs.
28
+ * Fetches config from /api/config if not already cached.
29
+ * Use useAuthClient hook in React components instead.
30
+ */
31
+ export function getAuthClientLazy(): ReturnType<typeof createAuthClient> {
32
+ if (!cachedClient) {
33
+ // Create with default URL initially
34
+ cachedClient = createAuthClient({
35
+ baseURL: "http://localhost:3000",
36
+ basePath: "/api/auth",
37
+ });
38
+
39
+ // Fetch real config and update
40
+ if (!configPromise) {
41
+ configPromise = fetch("/api/config")
42
+ .then((res) => res.json())
43
+ .then((data: { baseUrl: string }) => data.baseUrl)
44
+ .catch(() => "http://localhost:3000");
45
+ }
46
+
47
+ configPromise.then((baseUrl) => {
48
+ cachedClient = createAuthClient({
49
+ baseURL: baseUrl,
50
+ basePath: "/api/auth",
51
+ });
52
+ });
53
+ }
54
+ return cachedClient;
55
+ }
@@ -0,0 +1,3 @@
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:27abb4affaed83ae85739b62b71e5e860d2e6ee7b4fa91c915f5a3c26227b944
3
+ size 4253
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/frontend.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }