@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/CHANGELOG.md +207 -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 +78 -0
- package/src/components/ApplicationsTab.tsx +452 -0
- package/src/components/AuthErrorPage.tsx +94 -0
- package/src/components/AuthSettingsPage.tsx +249 -0
- package/src/components/AuthStrategyCard.tsx +77 -0
- package/src/components/ChangePasswordPage.tsx +259 -0
- package/src/components/CreateUserDialog.tsx +156 -0
- package/src/components/ForgotPasswordPage.tsx +131 -0
- package/src/components/LoginPage.tsx +330 -0
- package/src/components/RegisterPage.tsx +350 -0
- package/src/components/ResetPasswordPage.tsx +262 -0
- package/src/components/RoleDialog.tsx +284 -0
- package/src/components/RolesTab.tsx +219 -0
- package/src/components/SocialProviderButton.tsx +30 -0
- package/src/components/StrategiesTab.tsx +276 -0
- package/src/components/UsersTab.tsx +234 -0
- package/src/hooks/useEnabledStrategies.ts +54 -0
- package/src/hooks/usePermissions.ts +43 -0
- package/src/index.test.tsx +95 -0
- package/src/index.tsx +271 -0
- package/src/lib/auth-client.ts +55 -0
- package/test-results/login-Login-Page-should-show-login-form-elements-chromium/test-failed-1.png +3 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogContent,
|
|
5
|
+
DialogDescription,
|
|
6
|
+
DialogFooter,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
Button,
|
|
10
|
+
Input,
|
|
11
|
+
Label,
|
|
12
|
+
} from "@checkstack/ui";
|
|
13
|
+
import { passwordSchema } from "@checkstack/auth-common";
|
|
14
|
+
|
|
15
|
+
interface CreateUserDialogProps {
|
|
16
|
+
open: boolean;
|
|
17
|
+
onOpenChange: (open: boolean) => void;
|
|
18
|
+
onSubmit: (data: {
|
|
19
|
+
name: string;
|
|
20
|
+
email: string;
|
|
21
|
+
password: string;
|
|
22
|
+
}) => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const CreateUserDialog: React.FC<CreateUserDialogProps> = ({
|
|
26
|
+
open,
|
|
27
|
+
onOpenChange,
|
|
28
|
+
onSubmit,
|
|
29
|
+
}) => {
|
|
30
|
+
const [name, setName] = useState("");
|
|
31
|
+
const [email, setEmail] = useState("");
|
|
32
|
+
const [password, setPassword] = useState("");
|
|
33
|
+
const [loading, setLoading] = useState(false);
|
|
34
|
+
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
|
35
|
+
|
|
36
|
+
// Reset form when dialog opens/closes
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!open) {
|
|
39
|
+
setName("");
|
|
40
|
+
setEmail("");
|
|
41
|
+
setPassword("");
|
|
42
|
+
setValidationErrors([]);
|
|
43
|
+
}
|
|
44
|
+
}, [open]);
|
|
45
|
+
|
|
46
|
+
// Validate password on change
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (password) {
|
|
49
|
+
const result = passwordSchema.safeParse(password);
|
|
50
|
+
if (result.success) {
|
|
51
|
+
setValidationErrors([]);
|
|
52
|
+
} else {
|
|
53
|
+
setValidationErrors(result.error.issues.map((issue) => issue.message));
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
setValidationErrors([]);
|
|
57
|
+
}
|
|
58
|
+
}, [password]);
|
|
59
|
+
|
|
60
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
|
|
63
|
+
// Validate password before submitting
|
|
64
|
+
const result = passwordSchema.safeParse(password);
|
|
65
|
+
if (!result.success) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setLoading(true);
|
|
70
|
+
try {
|
|
71
|
+
await onSubmit({ name, email, password });
|
|
72
|
+
onOpenChange(false);
|
|
73
|
+
} finally {
|
|
74
|
+
setLoading(false);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const isValid =
|
|
79
|
+
name.trim().length > 0 &&
|
|
80
|
+
email.trim().length > 0 &&
|
|
81
|
+
password.length > 0 &&
|
|
82
|
+
validationErrors.length === 0;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
86
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
87
|
+
<DialogHeader>
|
|
88
|
+
<DialogTitle>Create User</DialogTitle>
|
|
89
|
+
<DialogDescription>
|
|
90
|
+
Create a new user account with email and password credentials.
|
|
91
|
+
</DialogDescription>
|
|
92
|
+
</DialogHeader>
|
|
93
|
+
<form onSubmit={handleSubmit}>
|
|
94
|
+
<div className="grid gap-4 py-4">
|
|
95
|
+
<div className="grid gap-2">
|
|
96
|
+
<Label htmlFor="create-name">Name</Label>
|
|
97
|
+
<Input
|
|
98
|
+
id="create-name"
|
|
99
|
+
value={name}
|
|
100
|
+
onChange={(e) => setName(e.target.value)}
|
|
101
|
+
placeholder="John Doe"
|
|
102
|
+
required
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="grid gap-2">
|
|
106
|
+
<Label htmlFor="create-email">Email</Label>
|
|
107
|
+
<Input
|
|
108
|
+
id="create-email"
|
|
109
|
+
type="email"
|
|
110
|
+
value={email}
|
|
111
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
112
|
+
placeholder="john@example.com"
|
|
113
|
+
required
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
<div className="grid gap-2">
|
|
117
|
+
<Label htmlFor="create-password">Password</Label>
|
|
118
|
+
<Input
|
|
119
|
+
id="create-password"
|
|
120
|
+
type="password"
|
|
121
|
+
value={password}
|
|
122
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
123
|
+
required
|
|
124
|
+
/>
|
|
125
|
+
{validationErrors.length > 0 && (
|
|
126
|
+
<ul className="text-sm text-muted-foreground list-disc pl-5 space-y-1">
|
|
127
|
+
{validationErrors.map((error, i) => (
|
|
128
|
+
<li key={i} className="text-destructive">
|
|
129
|
+
{error}
|
|
130
|
+
</li>
|
|
131
|
+
))}
|
|
132
|
+
</ul>
|
|
133
|
+
)}
|
|
134
|
+
<p className="text-xs text-muted-foreground">
|
|
135
|
+
At least 8 characters with uppercase, lowercase, and number
|
|
136
|
+
</p>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
<DialogFooter>
|
|
140
|
+
<Button
|
|
141
|
+
type="button"
|
|
142
|
+
variant="outline"
|
|
143
|
+
onClick={() => onOpenChange(false)}
|
|
144
|
+
disabled={loading}
|
|
145
|
+
>
|
|
146
|
+
Cancel
|
|
147
|
+
</Button>
|
|
148
|
+
<Button type="submit" disabled={!isValid || loading}>
|
|
149
|
+
{loading ? "Creating..." : "Create User"}
|
|
150
|
+
</Button>
|
|
151
|
+
</DialogFooter>
|
|
152
|
+
</form>
|
|
153
|
+
</DialogContent>
|
|
154
|
+
</Dialog>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { Mail, ArrowLeft, CheckCircle } from "lucide-react";
|
|
4
|
+
import { authRoutes } from "@checkstack/auth-common";
|
|
5
|
+
import { resolveRoute } from "@checkstack/common";
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
Input,
|
|
9
|
+
Label,
|
|
10
|
+
Card,
|
|
11
|
+
CardHeader,
|
|
12
|
+
CardTitle,
|
|
13
|
+
CardDescription,
|
|
14
|
+
CardContent,
|
|
15
|
+
CardFooter,
|
|
16
|
+
} from "@checkstack/ui";
|
|
17
|
+
import { useAuthClient } from "../lib/auth-client";
|
|
18
|
+
|
|
19
|
+
export const ForgotPasswordPage = () => {
|
|
20
|
+
const [email, setEmail] = useState("");
|
|
21
|
+
const [loading, setLoading] = useState(false);
|
|
22
|
+
const [submitted, setSubmitted] = useState(false);
|
|
23
|
+
const authClient = useAuthClient();
|
|
24
|
+
|
|
25
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
setLoading(true);
|
|
28
|
+
try {
|
|
29
|
+
// Always show success message to prevent user enumeration
|
|
30
|
+
await authClient.requestPasswordReset({
|
|
31
|
+
email,
|
|
32
|
+
redirectTo: "/auth/reset-password",
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// Silently handle errors to prevent timing attacks
|
|
36
|
+
console.error("Password reset request failed:", error);
|
|
37
|
+
} finally {
|
|
38
|
+
setLoading(false);
|
|
39
|
+
setSubmitted(true);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (submitted) {
|
|
44
|
+
return (
|
|
45
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
46
|
+
<Card className="w-full max-w-md">
|
|
47
|
+
<CardHeader className="space-y-1 text-center">
|
|
48
|
+
<div className="mx-auto w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mb-2">
|
|
49
|
+
<CheckCircle className="h-6 w-6 text-primary" />
|
|
50
|
+
</div>
|
|
51
|
+
<CardTitle className="text-2xl font-bold">
|
|
52
|
+
Check Your Email
|
|
53
|
+
</CardTitle>
|
|
54
|
+
<CardDescription>
|
|
55
|
+
If an account exists for <strong>{email}</strong>, you will
|
|
56
|
+
receive a password reset link shortly.
|
|
57
|
+
</CardDescription>
|
|
58
|
+
</CardHeader>
|
|
59
|
+
<CardContent className="text-center text-sm text-muted-foreground">
|
|
60
|
+
<p>
|
|
61
|
+
Didn't receive an email? Check your spam folder or make sure you
|
|
62
|
+
entered the correct email address.
|
|
63
|
+
</p>
|
|
64
|
+
</CardContent>
|
|
65
|
+
<CardFooter className="flex flex-col gap-4">
|
|
66
|
+
<Button
|
|
67
|
+
variant="outline"
|
|
68
|
+
className="w-full"
|
|
69
|
+
onClick={() => setSubmitted(false)}
|
|
70
|
+
>
|
|
71
|
+
Try Another Email
|
|
72
|
+
</Button>
|
|
73
|
+
<Link
|
|
74
|
+
to={resolveRoute(authRoutes.routes.login)}
|
|
75
|
+
className="text-sm text-primary hover:underline flex items-center justify-center gap-1"
|
|
76
|
+
>
|
|
77
|
+
<ArrowLeft className="h-4 w-4" />
|
|
78
|
+
Back to Login
|
|
79
|
+
</Link>
|
|
80
|
+
</CardFooter>
|
|
81
|
+
</Card>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
88
|
+
<Card className="w-full max-w-md">
|
|
89
|
+
<CardHeader className="space-y-1">
|
|
90
|
+
<CardTitle className="text-2xl font-bold">Forgot Password</CardTitle>
|
|
91
|
+
<CardDescription>
|
|
92
|
+
Enter your email address and we'll send you a link to reset your
|
|
93
|
+
password.
|
|
94
|
+
</CardDescription>
|
|
95
|
+
</CardHeader>
|
|
96
|
+
<form onSubmit={handleSubmit}>
|
|
97
|
+
<CardContent className="space-y-4">
|
|
98
|
+
<div className="space-y-2">
|
|
99
|
+
<Label htmlFor="email">Email</Label>
|
|
100
|
+
<div className="relative">
|
|
101
|
+
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
102
|
+
<Input
|
|
103
|
+
id="email"
|
|
104
|
+
type="email"
|
|
105
|
+
placeholder="Enter your email"
|
|
106
|
+
value={email}
|
|
107
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
108
|
+
className="pl-10"
|
|
109
|
+
required
|
|
110
|
+
autoFocus
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</CardContent>
|
|
115
|
+
<CardFooter className="flex flex-col gap-4">
|
|
116
|
+
<Button type="submit" className="w-full" disabled={loading}>
|
|
117
|
+
{loading ? "Sending..." : "Send Reset Link"}
|
|
118
|
+
</Button>
|
|
119
|
+
<Link
|
|
120
|
+
to={resolveRoute(authRoutes.routes.login)}
|
|
121
|
+
className="text-sm text-primary hover:underline flex items-center justify-center gap-1"
|
|
122
|
+
>
|
|
123
|
+
<ArrowLeft className="h-4 w-4" />
|
|
124
|
+
Back to Login
|
|
125
|
+
</Link>
|
|
126
|
+
</CardFooter>
|
|
127
|
+
</form>
|
|
128
|
+
</Card>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Link, useNavigate } from "react-router-dom";
|
|
3
|
+
import { LogIn, LogOut, AlertCircle } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
useApi,
|
|
6
|
+
ExtensionSlot,
|
|
7
|
+
pluginRegistry,
|
|
8
|
+
rpcApiRef,
|
|
9
|
+
UserMenuItemsSlot,
|
|
10
|
+
UserMenuItemsBottomSlot,
|
|
11
|
+
UserMenuItemsContext,
|
|
12
|
+
} from "@checkstack/frontend-api";
|
|
13
|
+
import { AuthApi, authRoutes } from "@checkstack/auth-common";
|
|
14
|
+
import { resolveRoute } from "@checkstack/common";
|
|
15
|
+
import {
|
|
16
|
+
Button,
|
|
17
|
+
Input,
|
|
18
|
+
Label,
|
|
19
|
+
Card,
|
|
20
|
+
CardHeader,
|
|
21
|
+
CardTitle,
|
|
22
|
+
CardDescription,
|
|
23
|
+
CardContent,
|
|
24
|
+
CardFooter,
|
|
25
|
+
UserMenu,
|
|
26
|
+
DropdownMenuItem,
|
|
27
|
+
DropdownMenuSeparator,
|
|
28
|
+
Alert,
|
|
29
|
+
AlertIcon,
|
|
30
|
+
AlertContent,
|
|
31
|
+
AlertTitle,
|
|
32
|
+
AlertDescription,
|
|
33
|
+
InfoBanner,
|
|
34
|
+
InfoBannerIcon,
|
|
35
|
+
InfoBannerContent,
|
|
36
|
+
InfoBannerTitle,
|
|
37
|
+
InfoBannerDescription,
|
|
38
|
+
} from "@checkstack/ui";
|
|
39
|
+
import { authApiRef } from "../api";
|
|
40
|
+
import { useEnabledStrategies } from "../hooks/useEnabledStrategies";
|
|
41
|
+
import { usePermissions } from "../hooks/usePermissions";
|
|
42
|
+
import { useAuthClient } from "../lib/auth-client";
|
|
43
|
+
import { SocialProviderButton } from "./SocialProviderButton";
|
|
44
|
+
import { useEffect } from "react";
|
|
45
|
+
|
|
46
|
+
export const LoginPage = () => {
|
|
47
|
+
const [email, setEmail] = useState("");
|
|
48
|
+
const [password, setPassword] = useState("");
|
|
49
|
+
const [loading, setLoading] = useState(false);
|
|
50
|
+
const navigate = useNavigate();
|
|
51
|
+
const authApi = useApi(authApiRef);
|
|
52
|
+
const rpcApi = useApi(rpcApiRef);
|
|
53
|
+
const authRpcClient = rpcApi.forPlugin(AuthApi);
|
|
54
|
+
const { strategies, loading: strategiesLoading } = useEnabledStrategies();
|
|
55
|
+
const [registrationAllowed, setRegistrationAllowed] = useState<boolean>(true);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
authRpcClient
|
|
59
|
+
.getRegistrationStatus()
|
|
60
|
+
.then(({ allowRegistration }) => {
|
|
61
|
+
setRegistrationAllowed(allowRegistration);
|
|
62
|
+
})
|
|
63
|
+
.catch((error: Error) => {
|
|
64
|
+
console.error("Failed to check registration status:", error);
|
|
65
|
+
setRegistrationAllowed(true);
|
|
66
|
+
});
|
|
67
|
+
}, [authRpcClient]);
|
|
68
|
+
|
|
69
|
+
const handleCredentialLogin = async (e: React.FormEvent) => {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
setLoading(true);
|
|
72
|
+
try {
|
|
73
|
+
const { error } = await authApi.signIn(email, password);
|
|
74
|
+
if (error) {
|
|
75
|
+
console.error("Login failed:", error);
|
|
76
|
+
} else {
|
|
77
|
+
navigate("/");
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
setLoading(false);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleSocialLogin = async (provider: string) => {
|
|
85
|
+
try {
|
|
86
|
+
await authApi.signInWithSocial(provider);
|
|
87
|
+
// Navigation will happen automatically after OAuth redirect
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error("Social login failed:", error);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const credentialStrategy = strategies.find((s) => s.type === "credential");
|
|
94
|
+
const socialStrategies = strategies.filter((s) => s.type === "social");
|
|
95
|
+
const hasCredential = !!credentialStrategy;
|
|
96
|
+
const hasSocial = socialStrategies.length > 0;
|
|
97
|
+
|
|
98
|
+
// Loading state
|
|
99
|
+
if (strategiesLoading) {
|
|
100
|
+
return (
|
|
101
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
102
|
+
<Card className="w-full max-w-md">
|
|
103
|
+
<CardContent className="pt-6">
|
|
104
|
+
<div className="space-y-4">
|
|
105
|
+
<div className="h-4 bg-muted animate-pulse rounded" />
|
|
106
|
+
<div className="h-10 bg-muted animate-pulse rounded" />
|
|
107
|
+
<div className="h-10 bg-muted animate-pulse rounded" />
|
|
108
|
+
<div className="h-10 bg-muted animate-pulse rounded" />
|
|
109
|
+
</div>
|
|
110
|
+
</CardContent>
|
|
111
|
+
</Card>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// No strategies enabled
|
|
117
|
+
if (strategies.length === 0) {
|
|
118
|
+
return (
|
|
119
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
120
|
+
<Card className="w-full max-w-md">
|
|
121
|
+
<CardHeader className="flex flex-col space-y-1 items-center">
|
|
122
|
+
<CardTitle>Authentication Unavailable</CardTitle>
|
|
123
|
+
</CardHeader>
|
|
124
|
+
<CardContent>
|
|
125
|
+
<Alert variant="warning">
|
|
126
|
+
<AlertIcon>
|
|
127
|
+
<AlertCircle className="h-4 w-4" />
|
|
128
|
+
</AlertIcon>
|
|
129
|
+
<AlertContent>
|
|
130
|
+
<AlertTitle>No authentication methods enabled</AlertTitle>
|
|
131
|
+
<AlertDescription>
|
|
132
|
+
Please contact your system administrator to enable
|
|
133
|
+
authentication methods.
|
|
134
|
+
</AlertDescription>
|
|
135
|
+
</AlertContent>
|
|
136
|
+
</Alert>
|
|
137
|
+
</CardContent>
|
|
138
|
+
</Card>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
145
|
+
<Card className="w-full max-w-md">
|
|
146
|
+
<CardHeader className="flex flex-col space-y-1 items-center">
|
|
147
|
+
<CardTitle>Sign in to your account</CardTitle>
|
|
148
|
+
<CardDescription>
|
|
149
|
+
{hasCredential && hasSocial
|
|
150
|
+
? "Choose your preferred sign-in method"
|
|
151
|
+
: hasCredential
|
|
152
|
+
? "Enter your credentials to access the dashboard"
|
|
153
|
+
: "Continue with your account"}
|
|
154
|
+
</CardDescription>
|
|
155
|
+
</CardHeader>
|
|
156
|
+
<CardContent>
|
|
157
|
+
<div className="space-y-4">
|
|
158
|
+
{/* Registration Disabled Banner */}
|
|
159
|
+
{!registrationAllowed && (
|
|
160
|
+
<InfoBanner variant="warning">
|
|
161
|
+
<InfoBannerIcon>
|
|
162
|
+
<AlertCircle className="h-4 w-4" />
|
|
163
|
+
</InfoBannerIcon>
|
|
164
|
+
<InfoBannerContent>
|
|
165
|
+
<InfoBannerTitle>Registration Disabled</InfoBannerTitle>
|
|
166
|
+
<InfoBannerDescription>
|
|
167
|
+
New user registration is currently disabled. Please contact
|
|
168
|
+
an administrator if you need access.
|
|
169
|
+
</InfoBannerDescription>
|
|
170
|
+
</InfoBannerContent>
|
|
171
|
+
</InfoBanner>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* Credential Form */}
|
|
175
|
+
{hasCredential && (
|
|
176
|
+
<form className="space-y-4" onSubmit={handleCredentialLogin}>
|
|
177
|
+
<div className="space-y-2">
|
|
178
|
+
<Label htmlFor="email">Email</Label>
|
|
179
|
+
<Input
|
|
180
|
+
id="email"
|
|
181
|
+
placeholder="name@example.com"
|
|
182
|
+
type="email"
|
|
183
|
+
required
|
|
184
|
+
value={email}
|
|
185
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
186
|
+
/>
|
|
187
|
+
</div>
|
|
188
|
+
<div className="space-y-2">
|
|
189
|
+
<Label htmlFor="password">Password</Label>
|
|
190
|
+
<Input
|
|
191
|
+
id="password"
|
|
192
|
+
required
|
|
193
|
+
type="password"
|
|
194
|
+
value={password}
|
|
195
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
196
|
+
/>
|
|
197
|
+
<div className="text-right">
|
|
198
|
+
<Link
|
|
199
|
+
to={resolveRoute(authRoutes.routes.forgotPassword)}
|
|
200
|
+
className="text-sm text-primary hover:underline"
|
|
201
|
+
>
|
|
202
|
+
Forgot password?
|
|
203
|
+
</Link>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
<Button type="submit" className="w-full" disabled={loading}>
|
|
207
|
+
{loading ? "Signing In..." : "Sign In"}
|
|
208
|
+
</Button>
|
|
209
|
+
</form>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{/* Separator */}
|
|
213
|
+
{hasCredential && hasSocial && (
|
|
214
|
+
<div className="relative">
|
|
215
|
+
<div className="absolute inset-0 flex items-center">
|
|
216
|
+
<span className="w-full border-t border-border" />
|
|
217
|
+
</div>
|
|
218
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
219
|
+
<span className="bg-card px-2 text-muted-foreground">
|
|
220
|
+
Or continue with
|
|
221
|
+
</span>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
{/* Social Provider Buttons */}
|
|
227
|
+
{hasSocial && (
|
|
228
|
+
<div className="space-y-2">
|
|
229
|
+
{socialStrategies.map((strategy) => (
|
|
230
|
+
<SocialProviderButton
|
|
231
|
+
key={strategy.id}
|
|
232
|
+
displayName={strategy.displayName}
|
|
233
|
+
icon={strategy.icon}
|
|
234
|
+
onClick={() => handleSocialLogin(strategy.id)}
|
|
235
|
+
/>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
</CardContent>
|
|
241
|
+
{registrationAllowed &&
|
|
242
|
+
strategies.some((s) => s.requiresManualRegistration) && (
|
|
243
|
+
<CardFooter className="flex justify-center border-t border-border mt-4 pt-4">
|
|
244
|
+
<div className="text-sm">
|
|
245
|
+
Don't have an account?{" "}
|
|
246
|
+
<Link
|
|
247
|
+
to={resolveRoute(authRoutes.routes.register)}
|
|
248
|
+
className="underline text-primary hover:text-primary/90 font-medium"
|
|
249
|
+
>
|
|
250
|
+
Sign up
|
|
251
|
+
</Link>
|
|
252
|
+
</div>
|
|
253
|
+
</CardFooter>
|
|
254
|
+
)}
|
|
255
|
+
</Card>
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export const LogoutMenuItem = (_props: UserMenuItemsContext) => {
|
|
261
|
+
const authApi = useApi(authApiRef);
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<DropdownMenuItem
|
|
265
|
+
onClick={() => authApi.signOut()}
|
|
266
|
+
icon={<LogOut className="h-4 w-4" />}
|
|
267
|
+
>
|
|
268
|
+
Logout
|
|
269
|
+
</DropdownMenuItem>
|
|
270
|
+
);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export const LoginNavbarAction = () => {
|
|
274
|
+
const authApi = useApi(authApiRef);
|
|
275
|
+
const { data: session, isPending } = authApi.useSession();
|
|
276
|
+
const { permissions, loading: permissionsLoading } = usePermissions();
|
|
277
|
+
const authClient = useAuthClient();
|
|
278
|
+
const [hasCredentialAccount, setHasCredentialAccount] =
|
|
279
|
+
useState<boolean>(false);
|
|
280
|
+
const [credentialLoading, setCredentialLoading] = useState(true);
|
|
281
|
+
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
if (!session?.user) {
|
|
284
|
+
setCredentialLoading(false);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
authClient.listAccounts().then((result) => {
|
|
288
|
+
if (result.data) {
|
|
289
|
+
const hasCredential = result.data.some(
|
|
290
|
+
(account) => account.providerId === "credential"
|
|
291
|
+
);
|
|
292
|
+
setHasCredentialAccount(hasCredential);
|
|
293
|
+
}
|
|
294
|
+
setCredentialLoading(false);
|
|
295
|
+
});
|
|
296
|
+
}, [session?.user, authClient]);
|
|
297
|
+
|
|
298
|
+
if (isPending || permissionsLoading || credentialLoading) {
|
|
299
|
+
return <div className="w-20 h-9 bg-muted animate-pulse rounded-full" />;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (session?.user) {
|
|
303
|
+
// Check if we have any bottom items to decide if we need a separator
|
|
304
|
+
const bottomExtensions = pluginRegistry.getExtensions(
|
|
305
|
+
UserMenuItemsBottomSlot.id
|
|
306
|
+
);
|
|
307
|
+
const hasBottomItems = bottomExtensions.length > 0;
|
|
308
|
+
const menuContext: UserMenuItemsContext = {
|
|
309
|
+
permissions,
|
|
310
|
+
hasCredentialAccount,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<UserMenu user={session.user}>
|
|
315
|
+
<ExtensionSlot slot={UserMenuItemsSlot} context={menuContext} />
|
|
316
|
+
{hasBottomItems && <DropdownMenuSeparator />}
|
|
317
|
+
<ExtensionSlot slot={UserMenuItemsBottomSlot} context={menuContext} />
|
|
318
|
+
</UserMenu>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return (
|
|
323
|
+
<Link to={resolveRoute(authRoutes.routes.login)}>
|
|
324
|
+
<Button variant="outline" className="flex items-center rounded-full px-5">
|
|
325
|
+
<LogIn className="mr-2 h-4 w-4" />
|
|
326
|
+
Login
|
|
327
|
+
</Button>
|
|
328
|
+
</Link>
|
|
329
|
+
);
|
|
330
|
+
};
|