@checkstack/auth-frontend 0.5.12 → 0.5.14
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 +23 -0
- package/package.json +11 -8
- package/src/api.ts +17 -1
- package/src/components/LoginPage.tsx +221 -76
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @checkstack/auth-frontend
|
|
2
2
|
|
|
3
|
+
## 0.5.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c0c0ed2: Introduce generic "Login Flows" to allow authentication strategies to define their own interaction patterns (form, redirect, or oauth) during registration. This fixes an issue where LDAP login attempts were incorrectly routed through the standard social login flow by instead providing a dedicated credential collection form for LDAP.
|
|
8
|
+
- Updated dependencies [c0c0ed2]
|
|
9
|
+
- Updated dependencies [c0c0ed2]
|
|
10
|
+
- Updated dependencies [c0c0ed2]
|
|
11
|
+
- @checkstack/auth-common@0.6.0
|
|
12
|
+
- @checkstack/ui@1.1.4
|
|
13
|
+
|
|
14
|
+
## 0.5.13
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 67158e2: Standardize package metadata, unify AJV versions to 8.18.0, and enforce monorepo architecture rules via updated ESLint configuration. This ensures consistent package discovery and runtime dependency safety across the platform.
|
|
19
|
+
- Updated dependencies [67158e2]
|
|
20
|
+
- Updated dependencies [6c743d4]
|
|
21
|
+
- @checkstack/auth-common@0.5.7
|
|
22
|
+
- @checkstack/common@0.6.4
|
|
23
|
+
- @checkstack/frontend-api@0.3.8
|
|
24
|
+
- @checkstack/ui@1.1.3
|
|
25
|
+
|
|
3
26
|
## 0.5.12
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/auth-frontend",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.tsx",
|
|
6
|
+
"checkstack": {
|
|
7
|
+
"type": "frontend"
|
|
8
|
+
},
|
|
6
9
|
"exports": {
|
|
7
10
|
".": "./src/index.tsx",
|
|
8
11
|
"./api": "./src/api.ts"
|
|
@@ -14,21 +17,21 @@
|
|
|
14
17
|
"test:e2e": "bunx playwright test"
|
|
15
18
|
},
|
|
16
19
|
"dependencies": {
|
|
17
|
-
"@checkstack/frontend-api": "0.3.
|
|
18
|
-
"@checkstack/common": "0.6.
|
|
19
|
-
"@checkstack/ui": "1.1.
|
|
20
|
+
"@checkstack/frontend-api": "0.3.8",
|
|
21
|
+
"@checkstack/common": "0.6.4",
|
|
22
|
+
"@checkstack/ui": "1.1.3",
|
|
20
23
|
"react": "^18.2.0",
|
|
21
24
|
"react-router-dom": "^6.22.0",
|
|
22
25
|
"lucide-react": "^0.344.0",
|
|
23
26
|
"better-auth": "^1.1.8",
|
|
24
|
-
"@checkstack/auth-common": "0.5.
|
|
27
|
+
"@checkstack/auth-common": "0.5.7"
|
|
25
28
|
},
|
|
26
29
|
"devDependencies": {
|
|
27
30
|
"typescript": "^5.0.0",
|
|
28
31
|
"@types/react": "^18.2.0",
|
|
29
32
|
"@playwright/test": "^1.49.0",
|
|
30
|
-
"@checkstack/test-utils-frontend": "0.0.
|
|
31
|
-
"@checkstack/tsconfig": "0.0.
|
|
32
|
-
"@checkstack/scripts": "0.1.
|
|
33
|
+
"@checkstack/test-utils-frontend": "0.0.4",
|
|
34
|
+
"@checkstack/tsconfig": "0.0.4",
|
|
35
|
+
"@checkstack/scripts": "0.1.2"
|
|
33
36
|
}
|
|
34
37
|
}
|
package/src/api.ts
CHANGED
|
@@ -49,11 +49,27 @@ export interface EnabledAuthStrategy {
|
|
|
49
49
|
id: string;
|
|
50
50
|
displayName: string;
|
|
51
51
|
description?: string;
|
|
52
|
-
type: "credential" | "social";
|
|
52
|
+
type: "credential" | "social" | "ldap" | "saml";
|
|
53
53
|
icon?: LucideIconName;
|
|
54
54
|
requiresManualRegistration: boolean;
|
|
55
|
+
clientFlow?: AuthClientFlow;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
export type AuthClientFlow =
|
|
59
|
+
| { type: "oauth" }
|
|
60
|
+
| { type: "redirect"; target: string }
|
|
61
|
+
| {
|
|
62
|
+
type: "form";
|
|
63
|
+
target: string;
|
|
64
|
+
fields: Array<{
|
|
65
|
+
name: string;
|
|
66
|
+
label: string;
|
|
67
|
+
type: "text" | "password";
|
|
68
|
+
placeholder?: string;
|
|
69
|
+
}>;
|
|
70
|
+
}
|
|
71
|
+
| { type: "credential" };
|
|
72
|
+
|
|
57
73
|
/**
|
|
58
74
|
* AuthApi provides better-auth client methods for authentication.
|
|
59
75
|
* For RPC calls (including getEnabledStrategies, user/role/strategy management), use:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
|
-
import { Link } from "react-router-dom";
|
|
3
|
-
import { LogIn, LogOut, AlertCircle } from "lucide-react";
|
|
2
|
+
import { Link, useNavigate } from "react-router-dom";
|
|
3
|
+
import { LogIn, LogOut, AlertCircle, ArrowLeft } from "lucide-react";
|
|
4
4
|
import {
|
|
5
5
|
useApi,
|
|
6
6
|
ExtensionSlot,
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
InfoBannerTitle,
|
|
37
37
|
InfoBannerDescription,
|
|
38
38
|
} from "@checkstack/ui";
|
|
39
|
-
import { authApiRef } from "../api";
|
|
39
|
+
import { authApiRef, EnabledAuthStrategy } from "../api";
|
|
40
40
|
import { useEnabledStrategies } from "../hooks/useEnabledStrategies";
|
|
41
41
|
import { useAccessRules } from "../hooks/useAccessRules";
|
|
42
42
|
import { useAuthClient } from "../lib/auth-client";
|
|
@@ -44,10 +44,16 @@ import { SocialProviderButton } from "./SocialProviderButton";
|
|
|
44
44
|
import { useEffect } from "react";
|
|
45
45
|
|
|
46
46
|
export const LoginPage = () => {
|
|
47
|
+
const [activeStrategy, setActiveStrategy] = useState<
|
|
48
|
+
EnabledAuthStrategy | undefined
|
|
49
|
+
>();
|
|
50
|
+
const [formValues, setFormValues] = useState<Record<string, string>>({});
|
|
51
|
+
const [loading, setLoading] = useState(false);
|
|
52
|
+
const [error, setError] = useState<string | undefined>();
|
|
47
53
|
const [email, setEmail] = useState("");
|
|
48
54
|
const [password, setPassword] = useState("");
|
|
49
|
-
const [loading, setLoading] = useState(false);
|
|
50
55
|
|
|
56
|
+
const navigate = useNavigate();
|
|
51
57
|
const authApi = useApi(authApiRef);
|
|
52
58
|
const authClient = usePluginClient(AuthApi);
|
|
53
59
|
const { strategies, loading: strategiesLoading } = useEnabledStrategies();
|
|
@@ -61,12 +67,12 @@ export const LoginPage = () => {
|
|
|
61
67
|
const handleCredentialLogin = async (e: React.FormEvent) => {
|
|
62
68
|
e.preventDefault();
|
|
63
69
|
setLoading(true);
|
|
70
|
+
setError(undefined);
|
|
64
71
|
try {
|
|
65
72
|
const { error } = await authApi.signIn(email, password);
|
|
66
73
|
if (error) {
|
|
67
|
-
|
|
74
|
+
setError(error.message);
|
|
68
75
|
} else {
|
|
69
|
-
// Use full page navigation to ensure session/permissions state refreshes
|
|
70
76
|
globalThis.location.href = "/";
|
|
71
77
|
}
|
|
72
78
|
} finally {
|
|
@@ -74,24 +80,78 @@ export const LoginPage = () => {
|
|
|
74
80
|
}
|
|
75
81
|
};
|
|
76
82
|
|
|
77
|
-
const
|
|
83
|
+
const handleProviderClick = async (strategy: EnabledAuthStrategy) => {
|
|
84
|
+
setError(undefined);
|
|
85
|
+
const flow = strategy.clientFlow;
|
|
86
|
+
|
|
87
|
+
// Use default OAuth flow if no clientFlow provided (backward compat)
|
|
88
|
+
if (!flow || flow.type === "oauth") {
|
|
89
|
+
try {
|
|
90
|
+
await authApi.signInWithSocial(strategy.id);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
setError("Failed to initialize social login");
|
|
93
|
+
console.error("Social login failed:", error);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (flow.type === "redirect") {
|
|
99
|
+
navigate(flow.target);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (flow.type === "form") {
|
|
104
|
+
setActiveStrategy(strategy);
|
|
105
|
+
setFormValues({});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleCustomFormSubmit = async (e: React.FormEvent) => {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
const flow = activeStrategy?.clientFlow;
|
|
113
|
+
if (flow?.type !== "form") return;
|
|
114
|
+
|
|
115
|
+
setLoading(true);
|
|
116
|
+
setError(undefined);
|
|
78
117
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
118
|
+
const response = await fetch(flow.target, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: { "Content-Type": "application/json" },
|
|
121
|
+
body: JSON.stringify(formValues),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (response.redirected) {
|
|
125
|
+
globalThis.location.href = response.url;
|
|
82
126
|
return;
|
|
83
127
|
}
|
|
84
|
-
|
|
85
|
-
|
|
128
|
+
|
|
129
|
+
if (response.ok) {
|
|
130
|
+
const data = await response.json().catch(() => ({ success: true }));
|
|
131
|
+
if (data.success) {
|
|
132
|
+
globalThis.location.href = "/";
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
setError(data.error?.message || "Authentication failed");
|
|
136
|
+
} else {
|
|
137
|
+
const data = await response.json().catch(() => ({}));
|
|
138
|
+
setError(data.error?.message || "Authentication failed");
|
|
139
|
+
}
|
|
86
140
|
} catch (error) {
|
|
87
141
|
console.error("Social login failed:", error);
|
|
88
142
|
}
|
|
89
143
|
};
|
|
90
144
|
|
|
91
|
-
const credentialStrategy = strategies.find(
|
|
92
|
-
|
|
145
|
+
const credentialStrategy = strategies.find(
|
|
146
|
+
(s) => s.id === "credential" || s.clientFlow?.type === "credential",
|
|
147
|
+
);
|
|
148
|
+
const providerStrategies = strategies.filter(
|
|
149
|
+
(s) =>
|
|
150
|
+
s.id !== "credential" &&
|
|
151
|
+
(!s.clientFlow || s.clientFlow.type !== "credential"),
|
|
152
|
+
);
|
|
93
153
|
const hasCredential = !!credentialStrategy;
|
|
94
|
-
const
|
|
154
|
+
const hasProviders = providerStrategies.length > 0;
|
|
95
155
|
|
|
96
156
|
// Loading state
|
|
97
157
|
if (strategiesLoading) {
|
|
@@ -144,7 +204,7 @@ export const LoginPage = () => {
|
|
|
144
204
|
<CardHeader className="flex flex-col space-y-1 items-center">
|
|
145
205
|
<CardTitle>Sign in to your account</CardTitle>
|
|
146
206
|
<CardDescription>
|
|
147
|
-
{hasCredential &&
|
|
207
|
+
{hasCredential && hasProviders
|
|
148
208
|
? "Choose your preferred sign-in method"
|
|
149
209
|
: hasCredential
|
|
150
210
|
? "Enter your credentials to access the dashboard"
|
|
@@ -169,70 +229,155 @@ export const LoginPage = () => {
|
|
|
169
229
|
</InfoBanner>
|
|
170
230
|
)}
|
|
171
231
|
|
|
172
|
-
{/*
|
|
173
|
-
{
|
|
174
|
-
<
|
|
175
|
-
<
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
value={email}
|
|
183
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
184
|
-
/>
|
|
185
|
-
</div>
|
|
186
|
-
<div className="space-y-2">
|
|
187
|
-
<Label htmlFor="password">Password</Label>
|
|
188
|
-
<Input
|
|
189
|
-
id="password"
|
|
190
|
-
required
|
|
191
|
-
type="password"
|
|
192
|
-
value={password}
|
|
193
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
194
|
-
/>
|
|
195
|
-
<div className="text-right">
|
|
196
|
-
<Link
|
|
197
|
-
to={resolveRoute(authRoutes.routes.forgotPassword)}
|
|
198
|
-
className="text-sm text-primary hover:underline"
|
|
199
|
-
>
|
|
200
|
-
Forgot password?
|
|
201
|
-
</Link>
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
|
-
<Button type="submit" className="w-full" disabled={loading}>
|
|
205
|
-
{loading ? "Signing In..." : "Sign In"}
|
|
206
|
-
</Button>
|
|
207
|
-
</form>
|
|
232
|
+
{/* Generic Error Alert */}
|
|
233
|
+
{error && (
|
|
234
|
+
<Alert variant="warning">
|
|
235
|
+
<AlertIcon>
|
|
236
|
+
<AlertCircle className="h-4 w-4" />
|
|
237
|
+
</AlertIcon>
|
|
238
|
+
<AlertContent>
|
|
239
|
+
<AlertDescription>{error}</AlertDescription>
|
|
240
|
+
</AlertContent>
|
|
241
|
+
</Alert>
|
|
208
242
|
)}
|
|
209
243
|
|
|
210
|
-
{/*
|
|
211
|
-
{
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
244
|
+
{/* Main Login Options */}
|
|
245
|
+
{!activeStrategy && (
|
|
246
|
+
<>
|
|
247
|
+
{/* Internal Credential Form */}
|
|
248
|
+
{hasCredential && (
|
|
249
|
+
<form className="space-y-4" onSubmit={handleCredentialLogin}>
|
|
250
|
+
<div className="space-y-2">
|
|
251
|
+
<Label htmlFor="email">Email</Label>
|
|
252
|
+
<Input
|
|
253
|
+
id="email"
|
|
254
|
+
placeholder="name@example.com"
|
|
255
|
+
type="email"
|
|
256
|
+
required
|
|
257
|
+
value={email}
|
|
258
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
<div className="space-y-2">
|
|
262
|
+
<Label htmlFor="password">Password</Label>
|
|
263
|
+
<Input
|
|
264
|
+
id="password"
|
|
265
|
+
required
|
|
266
|
+
type="password"
|
|
267
|
+
value={password}
|
|
268
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
269
|
+
/>
|
|
270
|
+
<div className="text-right">
|
|
271
|
+
<Link
|
|
272
|
+
to={resolveRoute(authRoutes.routes.forgotPassword)}
|
|
273
|
+
className="text-sm text-primary hover:underline"
|
|
274
|
+
>
|
|
275
|
+
Forgot password?
|
|
276
|
+
</Link>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
<Button type="submit" className="w-full" disabled={loading}>
|
|
280
|
+
{loading ? "Signing In..." : "Sign In"}
|
|
281
|
+
</Button>
|
|
282
|
+
</form>
|
|
283
|
+
)}
|
|
284
|
+
|
|
285
|
+
{/* Separator */}
|
|
286
|
+
{hasCredential && hasProviders && (
|
|
287
|
+
<div className="relative">
|
|
288
|
+
<div className="absolute inset-0 flex items-center">
|
|
289
|
+
<span className="w-full border-t border-border" />
|
|
290
|
+
</div>
|
|
291
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
292
|
+
<span className="bg-card px-2 text-muted-foreground">
|
|
293
|
+
Or continue with
|
|
294
|
+
</span>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
|
|
299
|
+
{/* Provider Buttons (OAuth, Redirect, or trigger Form view) */}
|
|
300
|
+
{hasProviders && (
|
|
301
|
+
<div className="space-y-2">
|
|
302
|
+
{providerStrategies.map((strategy) => (
|
|
303
|
+
<SocialProviderButton
|
|
304
|
+
key={strategy.id}
|
|
305
|
+
displayName={strategy.displayName}
|
|
306
|
+
icon={strategy.icon}
|
|
307
|
+
onClick={() => handleProviderClick(strategy)}
|
|
308
|
+
/>
|
|
309
|
+
))}
|
|
310
|
+
</div>
|
|
311
|
+
)}
|
|
312
|
+
</>
|
|
222
313
|
)}
|
|
223
314
|
|
|
224
|
-
{/*
|
|
225
|
-
{
|
|
226
|
-
<
|
|
227
|
-
{
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
315
|
+
{/* Custom Form View (e.g. LDAP) */}
|
|
316
|
+
{activeStrategy && activeStrategy.clientFlow?.type === "form" && (
|
|
317
|
+
<form className="space-y-4" onSubmit={handleCustomFormSubmit}>
|
|
318
|
+
{(() => {
|
|
319
|
+
const flow = activeStrategy.clientFlow;
|
|
320
|
+
if (flow?.type !== "form") return;
|
|
321
|
+
|
|
322
|
+
return (
|
|
323
|
+
<>
|
|
324
|
+
<div className="flex items-center mb-2">
|
|
325
|
+
<Button
|
|
326
|
+
variant="ghost"
|
|
327
|
+
size="sm"
|
|
328
|
+
className="p-0 h-auto hover:bg-transparent -ml-1 text-muted-foreground hover:text-foreground"
|
|
329
|
+
onClick={() => {
|
|
330
|
+
setActiveStrategy(undefined);
|
|
331
|
+
setError(undefined);
|
|
332
|
+
}}
|
|
333
|
+
>
|
|
334
|
+
<ArrowLeft className="h-4 w-4 mr-1" />
|
|
335
|
+
Back
|
|
336
|
+
</Button>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<div className="space-y-2">
|
|
340
|
+
<h4 className="font-medium text-sm">
|
|
341
|
+
Log in with {activeStrategy.displayName}
|
|
342
|
+
</h4>
|
|
343
|
+
{activeStrategy.description && (
|
|
344
|
+
<p className="text-xs text-muted-foreground">
|
|
345
|
+
{activeStrategy.description}
|
|
346
|
+
</p>
|
|
347
|
+
)}
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
{flow.fields.map((field) => (
|
|
351
|
+
<div key={field.name} className="space-y-2">
|
|
352
|
+
<Label htmlFor={field.name}>{field.label}</Label>
|
|
353
|
+
<Input
|
|
354
|
+
id={field.name}
|
|
355
|
+
name={field.name}
|
|
356
|
+
type={field.type}
|
|
357
|
+
placeholder={field.placeholder}
|
|
358
|
+
required
|
|
359
|
+
value={formValues[field.name] || ""}
|
|
360
|
+
onChange={(e) =>
|
|
361
|
+
setFormValues((prev) => ({
|
|
362
|
+
...prev,
|
|
363
|
+
[field.name]: e.target.value,
|
|
364
|
+
}))
|
|
365
|
+
}
|
|
366
|
+
/>
|
|
367
|
+
</div>
|
|
368
|
+
))}
|
|
369
|
+
|
|
370
|
+
<Button
|
|
371
|
+
type="submit"
|
|
372
|
+
className="w-full"
|
|
373
|
+
disabled={loading}
|
|
374
|
+
>
|
|
375
|
+
{loading ? "Processing..." : "Continue"}
|
|
376
|
+
</Button>
|
|
377
|
+
</>
|
|
378
|
+
);
|
|
379
|
+
})()}
|
|
380
|
+
</form>
|
|
236
381
|
)}
|
|
237
382
|
</div>
|
|
238
383
|
</CardContent>
|