@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 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.12",
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.5",
18
- "@checkstack/common": "0.6.2",
19
- "@checkstack/ui": "1.1.0",
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.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.3",
31
- "@checkstack/tsconfig": "0.0.3",
32
- "@checkstack/scripts": "0.1.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
- console.error("Login failed:", error);
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 handleSocialLogin = async (provider: string) => {
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
- // SAML uses a custom endpoint, not the better-auth OAuth flow
80
- if (provider === "saml") {
81
- globalThis.location.href = "/api/auth-saml/saml/login";
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
- await authApi.signInWithSocial(provider);
85
- // Navigation will happen automatically after OAuth redirect
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((s) => s.type === "credential");
92
- const socialStrategies = strategies.filter((s) => s.type === "social");
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 hasSocial = socialStrategies.length > 0;
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 && hasSocial
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
- {/* Credential Form */}
173
- {hasCredential && (
174
- <form className="space-y-4" onSubmit={handleCredentialLogin}>
175
- <div className="space-y-2">
176
- <Label htmlFor="email">Email</Label>
177
- <Input
178
- id="email"
179
- placeholder="name@example.com"
180
- type="email"
181
- required
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
- {/* Separator */}
211
- {hasCredential && hasSocial && (
212
- <div className="relative">
213
- <div className="absolute inset-0 flex items-center">
214
- <span className="w-full border-t border-border" />
215
- </div>
216
- <div className="relative flex justify-center text-xs uppercase">
217
- <span className="bg-card px-2 text-muted-foreground">
218
- Or continue with
219
- </span>
220
- </div>
221
- </div>
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
- {/* Social Provider Buttons */}
225
- {hasSocial && (
226
- <div className="space-y-2">
227
- {socialStrategies.map((strategy) => (
228
- <SocialProviderButton
229
- key={strategy.id}
230
- displayName={strategy.displayName}
231
- icon={strategy.icon}
232
- onClick={() => handleSocialLogin(strategy.id)}
233
- />
234
- ))}
235
- </div>
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>