@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,249 @@
|
|
|
1
|
+
import React, { useEffect, useState, useMemo } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router-dom";
|
|
3
|
+
import {
|
|
4
|
+
useApi,
|
|
5
|
+
permissionApiRef,
|
|
6
|
+
rpcApiRef,
|
|
7
|
+
} from "@checkstack/frontend-api";
|
|
8
|
+
import { PageLayout, useToast, Tabs, TabPanel } from "@checkstack/ui";
|
|
9
|
+
import { authApiRef, AuthUser, Role, AuthStrategy, Permission } from "../api";
|
|
10
|
+
import {
|
|
11
|
+
permissions as authPermissions,
|
|
12
|
+
AuthApi,
|
|
13
|
+
} from "@checkstack/auth-common";
|
|
14
|
+
import { Shield, Settings2, Users, Key } from "lucide-react";
|
|
15
|
+
import { UsersTab } from "./UsersTab";
|
|
16
|
+
import { RolesTab } from "./RolesTab";
|
|
17
|
+
import { StrategiesTab } from "./StrategiesTab";
|
|
18
|
+
import { ApplicationsTab } from "./ApplicationsTab";
|
|
19
|
+
|
|
20
|
+
export const AuthSettingsPage: React.FC = () => {
|
|
21
|
+
const authApi = useApi(authApiRef);
|
|
22
|
+
const rpcApi = useApi(rpcApiRef);
|
|
23
|
+
const authClient = rpcApi.forPlugin(AuthApi);
|
|
24
|
+
const permissionApi = useApi(permissionApiRef);
|
|
25
|
+
const toast = useToast();
|
|
26
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
27
|
+
|
|
28
|
+
const session = authApi.useSession();
|
|
29
|
+
|
|
30
|
+
const [activeTab, setActiveTab] = useState<
|
|
31
|
+
"users" | "roles" | "strategies" | "applications"
|
|
32
|
+
>("users");
|
|
33
|
+
const [users, setUsers] = useState<(AuthUser & { roles: string[] })[]>([]);
|
|
34
|
+
const [roles, setRoles] = useState<Role[]>([]);
|
|
35
|
+
const [permissions, setPermissions] = useState<Permission[]>([]);
|
|
36
|
+
const [strategies, setStrategies] = useState<AuthStrategy[]>([]);
|
|
37
|
+
const [loading, setLoading] = useState(true);
|
|
38
|
+
|
|
39
|
+
const canReadUsers = permissionApi.usePermission(
|
|
40
|
+
authPermissions.usersRead.id
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Handle ?tab= URL parameters (from command palette)
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const tab = searchParams.get("tab");
|
|
46
|
+
|
|
47
|
+
if (tab && ["users", "roles", "strategies", "applications"].includes(tab)) {
|
|
48
|
+
setActiveTab(tab as "users" | "roles" | "strategies" | "applications");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Clear the URL params after processing
|
|
52
|
+
if (tab) {
|
|
53
|
+
searchParams.delete("tab");
|
|
54
|
+
searchParams.delete("action");
|
|
55
|
+
setSearchParams(searchParams, { replace: true });
|
|
56
|
+
}
|
|
57
|
+
}, [searchParams, setSearchParams]);
|
|
58
|
+
|
|
59
|
+
const canManageUsers = permissionApi.usePermission(
|
|
60
|
+
authPermissions.usersManage.id
|
|
61
|
+
);
|
|
62
|
+
const canCreateUsers = permissionApi.usePermission(
|
|
63
|
+
authPermissions.usersCreate.id
|
|
64
|
+
);
|
|
65
|
+
const canReadRoles = permissionApi.usePermission(
|
|
66
|
+
authPermissions.rolesRead.id
|
|
67
|
+
);
|
|
68
|
+
const canCreateRoles = permissionApi.usePermission(
|
|
69
|
+
authPermissions.rolesCreate.id
|
|
70
|
+
);
|
|
71
|
+
const canUpdateRoles = permissionApi.usePermission(
|
|
72
|
+
authPermissions.rolesUpdate.id
|
|
73
|
+
);
|
|
74
|
+
const canDeleteRoles = permissionApi.usePermission(
|
|
75
|
+
authPermissions.rolesDelete.id
|
|
76
|
+
);
|
|
77
|
+
const canManageRoles = permissionApi.usePermission(
|
|
78
|
+
authPermissions.rolesManage.id
|
|
79
|
+
);
|
|
80
|
+
const canManageStrategies = permissionApi.usePermission(
|
|
81
|
+
authPermissions.strategiesManage.id
|
|
82
|
+
);
|
|
83
|
+
const canManageRegistration = permissionApi.usePermission(
|
|
84
|
+
authPermissions.registrationManage.id
|
|
85
|
+
);
|
|
86
|
+
const canManageApplications = permissionApi.usePermission(
|
|
87
|
+
authPermissions.applicationsManage.id
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Compute loading and permission states for PageLayout
|
|
91
|
+
const permissionsLoading =
|
|
92
|
+
loading ||
|
|
93
|
+
canReadUsers.loading ||
|
|
94
|
+
canReadRoles.loading ||
|
|
95
|
+
canManageStrategies.loading ||
|
|
96
|
+
canManageApplications.loading;
|
|
97
|
+
|
|
98
|
+
const hasAnyPermission =
|
|
99
|
+
canReadUsers.allowed ||
|
|
100
|
+
canReadRoles.allowed ||
|
|
101
|
+
canManageStrategies.allowed ||
|
|
102
|
+
canManageApplications.allowed;
|
|
103
|
+
|
|
104
|
+
// Special case: if user is not logged in, show permission denied
|
|
105
|
+
const isAllowed = session.data?.user ? hasAnyPermission : false;
|
|
106
|
+
|
|
107
|
+
// Compute visible tabs based on permissions
|
|
108
|
+
const visibleTabs = useMemo(() => {
|
|
109
|
+
const tabs: Array<{
|
|
110
|
+
id: "users" | "roles" | "strategies" | "applications";
|
|
111
|
+
label: string;
|
|
112
|
+
icon: React.ReactNode;
|
|
113
|
+
}> = [];
|
|
114
|
+
if (canReadUsers.allowed)
|
|
115
|
+
tabs.push({
|
|
116
|
+
id: "users",
|
|
117
|
+
label: "Users & Roles",
|
|
118
|
+
icon: <Users size={18} />,
|
|
119
|
+
});
|
|
120
|
+
if (canReadRoles.allowed)
|
|
121
|
+
tabs.push({
|
|
122
|
+
id: "roles",
|
|
123
|
+
label: "Roles & Permissions",
|
|
124
|
+
icon: <Shield size={18} />,
|
|
125
|
+
});
|
|
126
|
+
if (canManageStrategies.allowed)
|
|
127
|
+
tabs.push({
|
|
128
|
+
id: "strategies",
|
|
129
|
+
label: "Auth Strategies",
|
|
130
|
+
icon: <Settings2 size={18} />,
|
|
131
|
+
});
|
|
132
|
+
if (canManageApplications.allowed)
|
|
133
|
+
tabs.push({
|
|
134
|
+
id: "applications",
|
|
135
|
+
label: "Applications",
|
|
136
|
+
icon: <Key size={18} />,
|
|
137
|
+
});
|
|
138
|
+
return tabs;
|
|
139
|
+
}, [
|
|
140
|
+
canReadUsers.allowed,
|
|
141
|
+
canReadRoles.allowed,
|
|
142
|
+
canManageStrategies.allowed,
|
|
143
|
+
canManageApplications.allowed,
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
// Auto-select first visible tab if current tab is not accessible
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (
|
|
149
|
+
visibleTabs.length > 0 &&
|
|
150
|
+
!visibleTabs.some((t) => t.id === activeTab)
|
|
151
|
+
) {
|
|
152
|
+
setActiveTab(visibleTabs[0].id);
|
|
153
|
+
}
|
|
154
|
+
}, [visibleTabs, activeTab]);
|
|
155
|
+
|
|
156
|
+
const fetchData = async () => {
|
|
157
|
+
setLoading(true);
|
|
158
|
+
try {
|
|
159
|
+
const usersData = (await authClient.getUsers()) as (AuthUser & {
|
|
160
|
+
roles: string[];
|
|
161
|
+
})[];
|
|
162
|
+
const rolesData = await authClient.getRoles();
|
|
163
|
+
const permissionsData = await authClient.getPermissions();
|
|
164
|
+
const strategiesData = await authClient.getStrategies();
|
|
165
|
+
setUsers(usersData);
|
|
166
|
+
setRoles(rolesData);
|
|
167
|
+
setPermissions(permissionsData);
|
|
168
|
+
setStrategies(strategiesData);
|
|
169
|
+
} catch (error: unknown) {
|
|
170
|
+
const message =
|
|
171
|
+
error instanceof Error ? error.message : "Failed to fetch data";
|
|
172
|
+
toast.error(message);
|
|
173
|
+
console.error(error);
|
|
174
|
+
} finally {
|
|
175
|
+
setLoading(false);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Initial data fetch
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
fetchData();
|
|
182
|
+
}, []);
|
|
183
|
+
|
|
184
|
+
// Get current user's role IDs for the RolesTab
|
|
185
|
+
const currentUserRoleIds =
|
|
186
|
+
users.find((u) => u.id === session.data?.user?.id)?.roles || [];
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<PageLayout
|
|
190
|
+
title="Authentication Settings"
|
|
191
|
+
loading={permissionsLoading}
|
|
192
|
+
allowed={isAllowed}
|
|
193
|
+
>
|
|
194
|
+
<Tabs
|
|
195
|
+
items={visibleTabs}
|
|
196
|
+
activeTab={activeTab}
|
|
197
|
+
onTabChange={(tabId) =>
|
|
198
|
+
setActiveTab(
|
|
199
|
+
tabId as "users" | "roles" | "strategies" | "applications"
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
className="mb-6"
|
|
203
|
+
/>
|
|
204
|
+
|
|
205
|
+
<TabPanel id="users" activeTab={activeTab}>
|
|
206
|
+
<UsersTab
|
|
207
|
+
users={users}
|
|
208
|
+
roles={roles}
|
|
209
|
+
strategies={strategies}
|
|
210
|
+
currentUserId={session.data?.user?.id}
|
|
211
|
+
canReadUsers={canReadUsers.allowed}
|
|
212
|
+
canCreateUsers={canCreateUsers.allowed}
|
|
213
|
+
canManageUsers={canManageUsers.allowed}
|
|
214
|
+
canManageRoles={canManageRoles.allowed}
|
|
215
|
+
onDataChange={fetchData}
|
|
216
|
+
/>
|
|
217
|
+
</TabPanel>
|
|
218
|
+
|
|
219
|
+
<TabPanel id="roles" activeTab={activeTab}>
|
|
220
|
+
<RolesTab
|
|
221
|
+
roles={roles}
|
|
222
|
+
permissions={permissions}
|
|
223
|
+
userRoleIds={currentUserRoleIds}
|
|
224
|
+
canReadRoles={canReadRoles.allowed}
|
|
225
|
+
canCreateRoles={canCreateRoles.allowed}
|
|
226
|
+
canUpdateRoles={canUpdateRoles.allowed}
|
|
227
|
+
canDeleteRoles={canDeleteRoles.allowed}
|
|
228
|
+
onDataChange={fetchData}
|
|
229
|
+
/>
|
|
230
|
+
</TabPanel>
|
|
231
|
+
|
|
232
|
+
<TabPanel id="strategies" activeTab={activeTab}>
|
|
233
|
+
<StrategiesTab
|
|
234
|
+
strategies={strategies}
|
|
235
|
+
canManageStrategies={canManageStrategies.allowed}
|
|
236
|
+
canManageRegistration={canManageRegistration.allowed}
|
|
237
|
+
onDataChange={fetchData}
|
|
238
|
+
/>
|
|
239
|
+
</TabPanel>
|
|
240
|
+
|
|
241
|
+
<TabPanel id="applications" activeTab={activeTab}>
|
|
242
|
+
<ApplicationsTab
|
|
243
|
+
roles={roles}
|
|
244
|
+
canManageApplications={canManageApplications.allowed}
|
|
245
|
+
/>
|
|
246
|
+
</TabPanel>
|
|
247
|
+
</PageLayout>
|
|
248
|
+
);
|
|
249
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StrategyConfigCard,
|
|
3
|
+
type ConfigSection,
|
|
4
|
+
type LucideIconName,
|
|
5
|
+
} from "@checkstack/ui";
|
|
6
|
+
import type { AuthStrategy } from "../api";
|
|
7
|
+
|
|
8
|
+
export interface AuthStrategyCardProps {
|
|
9
|
+
strategy: AuthStrategy;
|
|
10
|
+
onToggle: (id: string, enabled: boolean) => Promise<void>;
|
|
11
|
+
onSaveConfig: (id: string, config: Record<string, unknown>) => Promise<void>;
|
|
12
|
+
saving?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
expanded?: boolean;
|
|
15
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
16
|
+
config?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Auth strategy card using the shared StrategyConfigCard component.
|
|
21
|
+
* Uses Toggle switch style for auth strategies.
|
|
22
|
+
*/
|
|
23
|
+
export function AuthStrategyCard({
|
|
24
|
+
strategy,
|
|
25
|
+
onToggle,
|
|
26
|
+
onSaveConfig,
|
|
27
|
+
saving,
|
|
28
|
+
disabled,
|
|
29
|
+
expanded,
|
|
30
|
+
onExpandedChange,
|
|
31
|
+
config,
|
|
32
|
+
}: AuthStrategyCardProps) {
|
|
33
|
+
// Check if config schema has properties
|
|
34
|
+
const hasConfigSchema =
|
|
35
|
+
strategy.configSchema &&
|
|
36
|
+
"properties" in strategy.configSchema &&
|
|
37
|
+
Object.keys(strategy.configSchema.properties as Record<string, unknown>)
|
|
38
|
+
.length > 0;
|
|
39
|
+
|
|
40
|
+
// Config is missing if schema has properties but no saved config
|
|
41
|
+
const configMissing = hasConfigSchema && strategy.config === undefined;
|
|
42
|
+
|
|
43
|
+
// Build config sections
|
|
44
|
+
const configSections: ConfigSection[] = [];
|
|
45
|
+
if (hasConfigSchema) {
|
|
46
|
+
configSections.push({
|
|
47
|
+
id: "config",
|
|
48
|
+
title: "Configuration",
|
|
49
|
+
schema: strategy.configSchema,
|
|
50
|
+
value: config ?? strategy.config,
|
|
51
|
+
onSave: async (newConfig) => {
|
|
52
|
+
await onSaveConfig(strategy.id, newConfig);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<StrategyConfigCard
|
|
59
|
+
strategy={{
|
|
60
|
+
id: strategy.id,
|
|
61
|
+
displayName: strategy.displayName,
|
|
62
|
+
description: strategy.description,
|
|
63
|
+
icon: strategy.icon as LucideIconName,
|
|
64
|
+
enabled: strategy.enabled,
|
|
65
|
+
}}
|
|
66
|
+
configSections={configSections}
|
|
67
|
+
onToggle={onToggle}
|
|
68
|
+
saving={saving}
|
|
69
|
+
toggleDisabled={disabled}
|
|
70
|
+
useToggleSwitch={true}
|
|
71
|
+
configMissing={configMissing}
|
|
72
|
+
expanded={expanded}
|
|
73
|
+
onExpandedChange={onExpandedChange}
|
|
74
|
+
instructions={strategy.adminInstructions}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import { Lock, ArrowLeft, CheckCircle, AlertCircle, Key } from "lucide-react";
|
|
4
|
+
import { passwordSchema } from "@checkstack/auth-common";
|
|
5
|
+
import {
|
|
6
|
+
Button,
|
|
7
|
+
Input,
|
|
8
|
+
Label,
|
|
9
|
+
Card,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
CardDescription,
|
|
13
|
+
CardContent,
|
|
14
|
+
CardFooter,
|
|
15
|
+
Alert,
|
|
16
|
+
AlertIcon,
|
|
17
|
+
AlertContent,
|
|
18
|
+
AlertTitle,
|
|
19
|
+
AlertDescription,
|
|
20
|
+
Checkbox,
|
|
21
|
+
} from "@checkstack/ui";
|
|
22
|
+
import { useAuthClient } from "../lib/auth-client";
|
|
23
|
+
|
|
24
|
+
export const ChangePasswordPage = () => {
|
|
25
|
+
const navigate = useNavigate();
|
|
26
|
+
|
|
27
|
+
const [currentPassword, setCurrentPassword] = useState("");
|
|
28
|
+
const [newPassword, setNewPassword] = useState("");
|
|
29
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
30
|
+
const [revokeOtherSessions, setRevokeOtherSessions] = useState(true);
|
|
31
|
+
const [loading, setLoading] = useState(false);
|
|
32
|
+
const [error, setError] = useState<string>();
|
|
33
|
+
const [success, setSuccess] = useState(false);
|
|
34
|
+
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
|
35
|
+
const authClient = useAuthClient();
|
|
36
|
+
|
|
37
|
+
// Validate new password on change
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (newPassword) {
|
|
40
|
+
const result = passwordSchema.safeParse(newPassword);
|
|
41
|
+
if (result.success) {
|
|
42
|
+
setValidationErrors([]);
|
|
43
|
+
} else {
|
|
44
|
+
setValidationErrors(result.error.issues.map((issue) => issue.message));
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
setValidationErrors([]);
|
|
48
|
+
}
|
|
49
|
+
}, [newPassword]);
|
|
50
|
+
|
|
51
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
setError(undefined);
|
|
54
|
+
|
|
55
|
+
// Frontend validation
|
|
56
|
+
if (newPassword !== confirmPassword) {
|
|
57
|
+
setError("New passwords do not match");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = passwordSchema.safeParse(newPassword);
|
|
62
|
+
if (!result.success) {
|
|
63
|
+
setError(result.error.issues[0].message);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!currentPassword) {
|
|
68
|
+
setError("Current password is required");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setLoading(true);
|
|
73
|
+
try {
|
|
74
|
+
const response = await authClient.changePassword({
|
|
75
|
+
currentPassword,
|
|
76
|
+
newPassword,
|
|
77
|
+
revokeOtherSessions,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (response.error) {
|
|
81
|
+
setError(response.error.message ?? "Failed to change password");
|
|
82
|
+
} else {
|
|
83
|
+
setSuccess(true);
|
|
84
|
+
}
|
|
85
|
+
} catch (error_) {
|
|
86
|
+
setError(
|
|
87
|
+
error_ instanceof Error ? error_.message : "Failed to change password"
|
|
88
|
+
);
|
|
89
|
+
} finally {
|
|
90
|
+
setLoading(false);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Success state
|
|
95
|
+
if (success) {
|
|
96
|
+
return (
|
|
97
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
98
|
+
<Card className="w-full max-w-md">
|
|
99
|
+
<CardHeader className="space-y-1 text-center">
|
|
100
|
+
<div className="mx-auto w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mb-2">
|
|
101
|
+
<CheckCircle className="h-6 w-6 text-primary" />
|
|
102
|
+
</div>
|
|
103
|
+
<CardTitle className="text-2xl font-bold">
|
|
104
|
+
Password Changed Successfully
|
|
105
|
+
</CardTitle>
|
|
106
|
+
<CardDescription>
|
|
107
|
+
Your password has been updated.
|
|
108
|
+
{revokeOtherSessions &&
|
|
109
|
+
" All other sessions have been signed out."}
|
|
110
|
+
</CardDescription>
|
|
111
|
+
</CardHeader>
|
|
112
|
+
<CardFooter>
|
|
113
|
+
<Button className="w-full" onClick={() => navigate("/")}>
|
|
114
|
+
Go to Dashboard
|
|
115
|
+
</Button>
|
|
116
|
+
</CardFooter>
|
|
117
|
+
</Card>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className="min-h-[80vh] flex items-center justify-center">
|
|
124
|
+
<Card className="w-full max-w-md">
|
|
125
|
+
<CardHeader className="space-y-1">
|
|
126
|
+
<CardTitle className="text-2xl font-bold">Change Password</CardTitle>
|
|
127
|
+
<CardDescription>
|
|
128
|
+
Enter your current password and choose a new password.
|
|
129
|
+
</CardDescription>
|
|
130
|
+
</CardHeader>
|
|
131
|
+
<form onSubmit={handleSubmit}>
|
|
132
|
+
<CardContent className="space-y-4">
|
|
133
|
+
{error && (
|
|
134
|
+
<Alert variant="error">
|
|
135
|
+
<AlertIcon>
|
|
136
|
+
<AlertCircle className="h-4 w-4" />
|
|
137
|
+
</AlertIcon>
|
|
138
|
+
<AlertContent>
|
|
139
|
+
<AlertTitle>Error</AlertTitle>
|
|
140
|
+
<AlertDescription>{error}</AlertDescription>
|
|
141
|
+
</AlertContent>
|
|
142
|
+
</Alert>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
<div className="space-y-2">
|
|
146
|
+
<Label htmlFor="currentPassword">Current Password</Label>
|
|
147
|
+
<div className="relative">
|
|
148
|
+
<Key className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
149
|
+
<Input
|
|
150
|
+
id="currentPassword"
|
|
151
|
+
type="password"
|
|
152
|
+
placeholder="Enter current password"
|
|
153
|
+
value={currentPassword}
|
|
154
|
+
onChange={(e) => setCurrentPassword(e.target.value)}
|
|
155
|
+
className="pl-10"
|
|
156
|
+
required
|
|
157
|
+
autoFocus
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div className="space-y-2">
|
|
163
|
+
<Label htmlFor="newPassword">New Password</Label>
|
|
164
|
+
<div className="relative">
|
|
165
|
+
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
166
|
+
<Input
|
|
167
|
+
id="newPassword"
|
|
168
|
+
type="password"
|
|
169
|
+
placeholder="Enter new password"
|
|
170
|
+
value={newPassword}
|
|
171
|
+
onChange={(e) => setNewPassword(e.target.value)}
|
|
172
|
+
className="pl-10"
|
|
173
|
+
required
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
{validationErrors.length > 0 && (
|
|
177
|
+
<ul className="text-sm text-muted-foreground list-disc pl-5 space-y-1">
|
|
178
|
+
{validationErrors.map((validationError, i) => (
|
|
179
|
+
<li key={i} className="text-destructive">
|
|
180
|
+
{validationError}
|
|
181
|
+
</li>
|
|
182
|
+
))}
|
|
183
|
+
</ul>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div className="space-y-2">
|
|
188
|
+
<Label htmlFor="confirmPassword">Confirm New Password</Label>
|
|
189
|
+
<div className="relative">
|
|
190
|
+
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
191
|
+
<Input
|
|
192
|
+
id="confirmPassword"
|
|
193
|
+
type="password"
|
|
194
|
+
placeholder="Confirm new password"
|
|
195
|
+
value={confirmPassword}
|
|
196
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
197
|
+
className="pl-10"
|
|
198
|
+
required
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
{confirmPassword && newPassword !== confirmPassword && (
|
|
202
|
+
<p className="text-sm text-destructive">
|
|
203
|
+
Passwords do not match
|
|
204
|
+
</p>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div className="text-xs text-muted-foreground">
|
|
209
|
+
Password must be at least 8 characters and contain:
|
|
210
|
+
<ul className="list-disc pl-5 mt-1">
|
|
211
|
+
<li>At least one uppercase letter</li>
|
|
212
|
+
<li>At least one lowercase letter</li>
|
|
213
|
+
<li>At least one number</li>
|
|
214
|
+
</ul>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div className="flex items-center space-x-2">
|
|
218
|
+
<Checkbox
|
|
219
|
+
id="revokeOtherSessions"
|
|
220
|
+
checked={revokeOtherSessions}
|
|
221
|
+
onCheckedChange={(checked) =>
|
|
222
|
+
setRevokeOtherSessions(checked === true)
|
|
223
|
+
}
|
|
224
|
+
/>
|
|
225
|
+
<Label
|
|
226
|
+
htmlFor="revokeOtherSessions"
|
|
227
|
+
className="text-sm font-normal"
|
|
228
|
+
>
|
|
229
|
+
Sign out of all other sessions
|
|
230
|
+
</Label>
|
|
231
|
+
</div>
|
|
232
|
+
</CardContent>
|
|
233
|
+
<CardFooter className="flex flex-col gap-4">
|
|
234
|
+
<Button
|
|
235
|
+
type="submit"
|
|
236
|
+
className="w-full"
|
|
237
|
+
disabled={
|
|
238
|
+
loading ||
|
|
239
|
+
validationErrors.length > 0 ||
|
|
240
|
+
newPassword !== confirmPassword ||
|
|
241
|
+
!currentPassword
|
|
242
|
+
}
|
|
243
|
+
>
|
|
244
|
+
{loading ? "Changing..." : "Change Password"}
|
|
245
|
+
</Button>
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
onClick={() => navigate(-1)}
|
|
249
|
+
className="text-sm text-primary hover:underline flex items-center justify-center gap-1"
|
|
250
|
+
>
|
|
251
|
+
<ArrowLeft className="h-4 w-4" />
|
|
252
|
+
Go Back
|
|
253
|
+
</button>
|
|
254
|
+
</CardFooter>
|
|
255
|
+
</form>
|
|
256
|
+
</Card>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
};
|