@checkmate-monitor/auth-frontend 0.1.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.
- package/CHANGELOG.md +55 -0
- package/e2e/login.e2e.ts +63 -0
- package/package.json +34 -0
- package/playwright-report/data/774b616fd991c36e57f6aa95d67906b877dff5d1.md +20 -0
- package/playwright-report/data/d37ef869a8ef03c489f7ca3b80d67da69614c383.png +3 -0
- package/playwright-report/index.html +85 -0
- package/playwright.config.ts +5 -0
- package/src/api.ts +77 -0
- package/src/components/AuthErrorPage.tsx +94 -0
- package/src/components/AuthSettingsPage.tsx +1206 -0
- package/src/components/AuthStrategyCard.tsx +73 -0
- package/src/components/CreateUserDialog.tsx +156 -0
- package/src/components/ForgotPasswordPage.tsx +130 -0
- package/src/components/LoginPage.tsx +302 -0
- package/src/components/RegisterPage.tsx +349 -0
- package/src/components/ResetPasswordPage.tsx +261 -0
- package/src/components/RoleDialog.tsx +284 -0
- package/src/components/SocialProviderButton.tsx +55 -0
- package/src/hooks/useEnabledStrategies.ts +54 -0
- package/src/hooks/usePermissions.ts +36 -0
- package/src/index.test.tsx +95 -0
- package/src/index.tsx +241 -0
- package/src/lib/auth-client.ts +6 -0
- package/test-results/login-Login-Page-should-show-login-form-elements-chromium/test-failed-1.png +3 -0
- package/tsconfig.json +6 -0
package/src/index.tsx
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApiRef,
|
|
3
|
+
permissionApiRef,
|
|
4
|
+
PermissionApi,
|
|
5
|
+
createFrontendPlugin,
|
|
6
|
+
NavbarSlot,
|
|
7
|
+
UserMenuItemsSlot,
|
|
8
|
+
UserMenuItemsBottomSlot,
|
|
9
|
+
} from "@checkmate-monitor/frontend-api";
|
|
10
|
+
import {
|
|
11
|
+
LoginPage,
|
|
12
|
+
LoginNavbarAction,
|
|
13
|
+
LogoutMenuItem,
|
|
14
|
+
} from "./components/LoginPage";
|
|
15
|
+
import { RegisterPage } from "./components/RegisterPage";
|
|
16
|
+
import { AuthErrorPage } from "./components/AuthErrorPage";
|
|
17
|
+
import { ForgotPasswordPage } from "./components/ForgotPasswordPage";
|
|
18
|
+
import { ResetPasswordPage } from "./components/ResetPasswordPage";
|
|
19
|
+
import { authApiRef, AuthApi, AuthSession } from "./api";
|
|
20
|
+
import { authClient } from "./lib/auth-client";
|
|
21
|
+
|
|
22
|
+
import { usePermissions } from "./hooks/usePermissions";
|
|
23
|
+
|
|
24
|
+
import { PermissionAction } from "@checkmate-monitor/common";
|
|
25
|
+
import { useNavigate } from "react-router-dom";
|
|
26
|
+
import { Settings2 } from "lucide-react";
|
|
27
|
+
import { DropdownMenuItem } from "@checkmate-monitor/ui";
|
|
28
|
+
import { useApi } from "@checkmate-monitor/frontend-api";
|
|
29
|
+
import { AuthSettingsPage } from "./components/AuthSettingsPage";
|
|
30
|
+
import {
|
|
31
|
+
permissions as authPermissions,
|
|
32
|
+
authRoutes,
|
|
33
|
+
pluginMetadata,
|
|
34
|
+
} from "@checkmate-monitor/auth-common";
|
|
35
|
+
import { resolveRoute } from "@checkmate-monitor/common";
|
|
36
|
+
|
|
37
|
+
class AuthPermissionApi implements PermissionApi {
|
|
38
|
+
usePermission(permission: string): { loading: boolean; allowed: boolean } {
|
|
39
|
+
const { permissions, loading } = usePermissions();
|
|
40
|
+
|
|
41
|
+
if (loading) {
|
|
42
|
+
return { loading: true, allowed: false };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// If no user, or user has no permissions, return false
|
|
46
|
+
if (!permissions || permissions.length === 0) {
|
|
47
|
+
return { loading: false, allowed: false };
|
|
48
|
+
}
|
|
49
|
+
const allowed =
|
|
50
|
+
permissions.includes("*") || permissions.includes(permission);
|
|
51
|
+
return { loading: false, allowed };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
useResourcePermission(
|
|
55
|
+
resource: string,
|
|
56
|
+
action: PermissionAction
|
|
57
|
+
): { loading: boolean; allowed: boolean } {
|
|
58
|
+
const { permissions, loading } = usePermissions();
|
|
59
|
+
|
|
60
|
+
if (loading) {
|
|
61
|
+
return { loading: true, allowed: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!permissions || permissions.length === 0) {
|
|
65
|
+
return { loading: false, allowed: false };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const isWildcard = permissions.includes("*");
|
|
69
|
+
const hasResourceManage = permissions.includes(`${resource}.manage`);
|
|
70
|
+
const hasSpecificPermission = permissions.includes(`${resource}.${action}`);
|
|
71
|
+
|
|
72
|
+
// manage implies read
|
|
73
|
+
const isAllowed =
|
|
74
|
+
isWildcard ||
|
|
75
|
+
hasResourceManage ||
|
|
76
|
+
(action === "read" && hasResourceManage) ||
|
|
77
|
+
hasSpecificPermission;
|
|
78
|
+
|
|
79
|
+
return { loading: false, allowed: isAllowed };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
useManagePermission(resource: string): {
|
|
83
|
+
loading: boolean;
|
|
84
|
+
allowed: boolean;
|
|
85
|
+
} {
|
|
86
|
+
return this.useResourcePermission(resource, "manage");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* BetterAuthApi wraps only better-auth client methods.
|
|
92
|
+
* For RPC calls, use rpcApiRef.forPlugin<AuthClient>("auth") directly.
|
|
93
|
+
*/
|
|
94
|
+
class BetterAuthApi implements AuthApi {
|
|
95
|
+
async signIn(email: string, password: string) {
|
|
96
|
+
const res = await authClient.signIn.email({ email, password });
|
|
97
|
+
if (res.error) {
|
|
98
|
+
const error = new Error(res.error.message || res.error.statusText);
|
|
99
|
+
error.name = res.error.code || "AuthError";
|
|
100
|
+
return { data: undefined, error };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const data = res.data as typeof res.data & {
|
|
104
|
+
session?: AuthSession["session"];
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
data: {
|
|
108
|
+
session: data.session || {
|
|
109
|
+
token: data.token,
|
|
110
|
+
id: "session-id",
|
|
111
|
+
userId: data.user.id,
|
|
112
|
+
expiresAt: new Date(),
|
|
113
|
+
},
|
|
114
|
+
user: data.user,
|
|
115
|
+
} as AuthSession,
|
|
116
|
+
error: undefined,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async signInWithSocial(provider: string) {
|
|
121
|
+
const frontendUrl =
|
|
122
|
+
import.meta.env.VITE_FRONTEND_URL || "http://localhost:5173";
|
|
123
|
+
await authClient.signIn.social({
|
|
124
|
+
provider,
|
|
125
|
+
callbackURL: frontendUrl,
|
|
126
|
+
errorCallbackURL: `${frontendUrl}${resolveRoute(
|
|
127
|
+
authRoutes.routes.error
|
|
128
|
+
)}`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async signOut() {
|
|
133
|
+
await authClient.signOut({
|
|
134
|
+
fetchOptions: {
|
|
135
|
+
onSuccess: () => {
|
|
136
|
+
// Redirect to frontend root after successful logout
|
|
137
|
+
globalThis.location.href = "/";
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async getSession() {
|
|
144
|
+
const res = await authClient.getSession();
|
|
145
|
+
if (res.error) {
|
|
146
|
+
const error = new Error(res.error.message || res.error.statusText);
|
|
147
|
+
error.name = res.error.code || "AuthError";
|
|
148
|
+
return { data: undefined, error };
|
|
149
|
+
}
|
|
150
|
+
if (!res.data) return { data: undefined, error: undefined };
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
data: res.data as AuthSession,
|
|
154
|
+
error: undefined,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
useSession() {
|
|
159
|
+
const { data, isPending, error } = authClient.useSession();
|
|
160
|
+
return {
|
|
161
|
+
data: data as AuthSession | undefined,
|
|
162
|
+
isPending,
|
|
163
|
+
error: error as Error | undefined,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const authPlugin = createFrontendPlugin({
|
|
169
|
+
metadata: pluginMetadata,
|
|
170
|
+
apis: [
|
|
171
|
+
{
|
|
172
|
+
ref: authApiRef as ApiRef<unknown>,
|
|
173
|
+
factory: () => new BetterAuthApi(),
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
ref: permissionApiRef as ApiRef<unknown>,
|
|
177
|
+
factory: () => new AuthPermissionApi(),
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
routes: [
|
|
181
|
+
{
|
|
182
|
+
route: authRoutes.routes.login,
|
|
183
|
+
element: <LoginPage />,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
route: authRoutes.routes.register,
|
|
187
|
+
element: <RegisterPage />,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
route: authRoutes.routes.error,
|
|
191
|
+
element: <AuthErrorPage />,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
route: authRoutes.routes.settings,
|
|
195
|
+
element: <AuthSettingsPage />,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
route: authRoutes.routes.forgotPassword,
|
|
199
|
+
element: <ForgotPasswordPage />,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
route: authRoutes.routes.resetPassword,
|
|
203
|
+
element: <ResetPasswordPage />,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
extensions: [
|
|
207
|
+
{
|
|
208
|
+
id: "auth.navbar.action",
|
|
209
|
+
slot: NavbarSlot,
|
|
210
|
+
component: LoginNavbarAction,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: "auth.user-menu.settings",
|
|
214
|
+
slot: UserMenuItemsSlot,
|
|
215
|
+
component: () => {
|
|
216
|
+
// Use a wrapper component to use hooks
|
|
217
|
+
const navigate = useNavigate();
|
|
218
|
+
const permissionApi = useApi(permissionApiRef);
|
|
219
|
+
const canManage = permissionApi.usePermission(
|
|
220
|
+
authPermissions.strategiesManage.id
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
if (!canManage.allowed) return;
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<DropdownMenuItem
|
|
227
|
+
onClick={() => navigate(resolveRoute(authRoutes.routes.settings))}
|
|
228
|
+
icon={<Settings2 className="h-4 w-4" />}
|
|
229
|
+
>
|
|
230
|
+
Auth Settings
|
|
231
|
+
</DropdownMenuItem>
|
|
232
|
+
);
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: "auth.user-menu.logout",
|
|
237
|
+
slot: UserMenuItemsBottomSlot,
|
|
238
|
+
component: LogoutMenuItem,
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
});
|