realtimex-crm 0.5.7 → 0.6.1
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/dist/assets/{DealList-hFgyoSCd.js → DealList-B16NLW5g.js} +2 -2
- package/dist/assets/{DealList-hFgyoSCd.js.map → DealList-B16NLW5g.js.map} +1 -1
- package/dist/assets/index-C6XAWVQG.css +1 -0
- package/dist/assets/index-Cx_2Y6Ur.js +153 -0
- package/dist/assets/{index-BzM6--R_.js.map → index-Cx_2Y6Ur.js.map} +1 -1
- package/dist/index.html +1 -1
- package/dist/stats.html +1 -1
- package/package.json +1 -1
- package/src/components/admin/login-page.tsx +14 -6
- package/src/components/atomic-crm/providers/supabase/authProvider.ts +14 -0
- package/src/components/atomic-crm/providers/supabase/dataProvider.ts +46 -12
- package/src/components/atomic-crm/root/CRM.tsx +7 -0
- package/src/components/atomic-crm/sales/SalesList.tsx +200 -1
- package/src/components/atomic-crm/settings/SettingsPage.tsx +6 -18
- package/src/components/supabase/change-password-page.tsx +119 -0
- package/src/components/supabase/forgot-password-page.tsx +181 -36
- package/src/components/supabase/otp-input.tsx +116 -0
- package/src/components/supabase/otp-login-page.tsx +254 -0
- package/supabase/config.toml +248 -23
- package/supabase/functions/_shared/utils.ts +1 -1
- package/supabase/functions/setup/index.ts +58 -0
- package/supabase/functions/users/index.ts +72 -14
- package/supabase/migrations/20251218200545_add_email_confirmed_to_sales.sql +52 -0
- package/dist/assets/index-BzM6--R_.js +0 -153
- package/dist/assets/index-C5TuuS9H.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "realtimex-crm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "RealTimeX CRM - A full-featured CRM built with React, shadcn-admin-kit, and Supabase. Fork of Atomic CRM with RealTimeX App SDK integration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -89,12 +89,20 @@ export const LoginPage = (props: { redirectTo?: string }) => {
|
|
|
89
89
|
</Button>
|
|
90
90
|
</Form>
|
|
91
91
|
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
<div className="flex flex-col gap-2 text-sm text-center">
|
|
93
|
+
<Link
|
|
94
|
+
to={"/forgot-password"}
|
|
95
|
+
className="hover:underline"
|
|
96
|
+
>
|
|
97
|
+
Forgot your password?
|
|
98
|
+
</Link>
|
|
99
|
+
<Link
|
|
100
|
+
to={"/otp-login"}
|
|
101
|
+
className="hover:underline"
|
|
102
|
+
>
|
|
103
|
+
Login with email code (OTP)
|
|
104
|
+
</Link>
|
|
105
|
+
</div>
|
|
98
106
|
</div>
|
|
99
107
|
</div>
|
|
100
108
|
</div>
|
|
@@ -59,6 +59,20 @@ export const authProvider: AuthProvider = {
|
|
|
59
59
|
) {
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
|
+
// Users are on the change-password page, nothing to do
|
|
63
|
+
if (
|
|
64
|
+
window.location.pathname === "/change-password" ||
|
|
65
|
+
window.location.hash.includes("#/change-password")
|
|
66
|
+
) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Users are on the otp-login page, nothing to do
|
|
70
|
+
if (
|
|
71
|
+
window.location.pathname === "/otp-login" ||
|
|
72
|
+
window.location.hash.includes("#/otp-login")
|
|
73
|
+
) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
62
76
|
// Users are on the sign-up page, nothing to do
|
|
63
77
|
if (
|
|
64
78
|
window.location.pathname === "/sign-up" ||
|
|
@@ -105,19 +105,15 @@ const dataProviderWithCustomMethods = {
|
|
|
105
105
|
},
|
|
106
106
|
|
|
107
107
|
async signUp({ email, password, first_name, last_name }: SignUpData) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
first_name,
|
|
114
|
-
last_name,
|
|
115
|
-
},
|
|
116
|
-
},
|
|
108
|
+
// Use admin API via edge function to create first user
|
|
109
|
+
// This bypasses the signup restriction (enable_signup = false)
|
|
110
|
+
const { data, error } = await supabase.functions.invoke("setup", {
|
|
111
|
+
method: "POST",
|
|
112
|
+
body: { email, password, first_name, last_name },
|
|
117
113
|
});
|
|
118
114
|
|
|
119
|
-
if (!
|
|
120
|
-
console.error("signUp.error",
|
|
115
|
+
if (!data || error) {
|
|
116
|
+
console.error("signUp.error", error);
|
|
121
117
|
throw new Error("Failed to create account");
|
|
122
118
|
}
|
|
123
119
|
|
|
@@ -125,7 +121,7 @@ const dataProviderWithCustomMethods = {
|
|
|
125
121
|
getIsInitialized._is_initialized_cache = true;
|
|
126
122
|
|
|
127
123
|
return {
|
|
128
|
-
id:
|
|
124
|
+
id: data.data.id,
|
|
129
125
|
email,
|
|
130
126
|
password,
|
|
131
127
|
};
|
|
@@ -173,6 +169,44 @@ const dataProviderWithCustomMethods = {
|
|
|
173
169
|
|
|
174
170
|
return data;
|
|
175
171
|
},
|
|
172
|
+
async resendInvite(id: Identifier) {
|
|
173
|
+
const { data: sale, error } = await supabase.functions.invoke<Sale>(
|
|
174
|
+
"users",
|
|
175
|
+
{
|
|
176
|
+
method: "PUT",
|
|
177
|
+
body: {
|
|
178
|
+
sales_id: id,
|
|
179
|
+
action: "invite",
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (!sale || error) {
|
|
185
|
+
console.error("resendInvite.error", error);
|
|
186
|
+
throw new Error("Failed to resend invitation");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return sale;
|
|
190
|
+
},
|
|
191
|
+
async resetPassword(id: Identifier) {
|
|
192
|
+
const { data: sale, error } = await supabase.functions.invoke<Sale>(
|
|
193
|
+
"users",
|
|
194
|
+
{
|
|
195
|
+
method: "PUT",
|
|
196
|
+
body: {
|
|
197
|
+
sales_id: id,
|
|
198
|
+
action: "reset",
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (!sale || error) {
|
|
204
|
+
console.error("resetPassword.error", error);
|
|
205
|
+
throw new Error("Failed to send password reset");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return sale;
|
|
209
|
+
},
|
|
176
210
|
async updatePassword(id: Identifier) {
|
|
177
211
|
const { data: passwordUpdated, error } =
|
|
178
212
|
await supabase.functions.invoke<boolean>("updatePassword", {
|
|
@@ -10,6 +10,8 @@ import { Route } from "react-router";
|
|
|
10
10
|
import { Admin } from "@/components/admin/admin";
|
|
11
11
|
import { ForgotPasswordPage } from "@/components/supabase/forgot-password-page";
|
|
12
12
|
import { SetPasswordPage } from "@/components/supabase/set-password-page";
|
|
13
|
+
import { ChangePasswordPage } from "@/components/supabase/change-password-page";
|
|
14
|
+
import { OtpLoginPage } from "@/components/supabase/otp-login-page";
|
|
13
15
|
|
|
14
16
|
import companies from "../companies";
|
|
15
17
|
import contacts from "../contacts";
|
|
@@ -151,6 +153,11 @@ export const CRM = ({
|
|
|
151
153
|
path={ForgotPasswordPage.path}
|
|
152
154
|
element={<ForgotPasswordPage />}
|
|
153
155
|
/>
|
|
156
|
+
<Route
|
|
157
|
+
path={ChangePasswordPage.path}
|
|
158
|
+
element={<ChangePasswordPage />}
|
|
159
|
+
/>
|
|
160
|
+
<Route path={OtpLoginPage.path} element={<OtpLoginPage />} />
|
|
154
161
|
</CustomRoutes>
|
|
155
162
|
|
|
156
163
|
<CustomRoutes>
|
|
@@ -1,15 +1,43 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMutation } from "@tanstack/react-query";
|
|
2
|
+
import { KeyRound, MoreHorizontal, RefreshCw } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
useDataProvider,
|
|
6
|
+
useGetIdentity,
|
|
7
|
+
useListContext,
|
|
8
|
+
useNotify,
|
|
9
|
+
useRecordContext,
|
|
10
|
+
useRefresh,
|
|
11
|
+
} from "ra-core";
|
|
2
12
|
import { CreateButton } from "@/components/admin/create-button";
|
|
3
13
|
import { DataTable } from "@/components/admin/data-table";
|
|
4
14
|
import { ExportButton } from "@/components/admin/export-button";
|
|
5
15
|
import { List } from "@/components/admin/list";
|
|
6
16
|
import { SearchInput } from "@/components/admin/search-input";
|
|
7
17
|
import { Badge } from "@/components/ui/badge";
|
|
18
|
+
import { Button } from "@/components/ui/button";
|
|
8
19
|
import { Card } from "@/components/ui/card";
|
|
20
|
+
import {
|
|
21
|
+
Dialog,
|
|
22
|
+
DialogContent,
|
|
23
|
+
DialogDescription,
|
|
24
|
+
DialogFooter,
|
|
25
|
+
DialogHeader,
|
|
26
|
+
DialogTitle,
|
|
27
|
+
} from "@/components/ui/dialog";
|
|
28
|
+
import {
|
|
29
|
+
DropdownMenu,
|
|
30
|
+
DropdownMenuContent,
|
|
31
|
+
DropdownMenuItem,
|
|
32
|
+
DropdownMenuLabel,
|
|
33
|
+
DropdownMenuSeparator,
|
|
34
|
+
DropdownMenuTrigger,
|
|
35
|
+
} from "@/components/ui/dropdown-menu";
|
|
9
36
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
10
37
|
|
|
11
38
|
import { TopToolbar } from "../layout/TopToolbar";
|
|
12
39
|
import useAppBarHeight from "../misc/useAppBarHeight";
|
|
40
|
+
import type { CrmDataProvider } from "../providers/types";
|
|
13
41
|
|
|
14
42
|
const SalesListActions = () => (
|
|
15
43
|
<TopToolbar>
|
|
@@ -45,6 +73,174 @@ const OptionsField = (_props: { label?: string | boolean }) => {
|
|
|
45
73
|
);
|
|
46
74
|
};
|
|
47
75
|
|
|
76
|
+
const RowActions = () => {
|
|
77
|
+
const record = useRecordContext();
|
|
78
|
+
const dataProvider = useDataProvider<CrmDataProvider>();
|
|
79
|
+
const notify = useNotify();
|
|
80
|
+
const refresh = useRefresh();
|
|
81
|
+
const [inviteDialogOpen, setInviteDialogOpen] = React.useState(false);
|
|
82
|
+
const [resetDialogOpen, setResetDialogOpen] = React.useState(false);
|
|
83
|
+
|
|
84
|
+
const { mutate: resendInvite, isPending: isInvitePending } = useMutation({
|
|
85
|
+
mutationFn: async () => {
|
|
86
|
+
return dataProvider.resendInvite(record.id);
|
|
87
|
+
},
|
|
88
|
+
onSuccess: () => {
|
|
89
|
+
notify("Invitation email resent successfully");
|
|
90
|
+
setInviteDialogOpen(false);
|
|
91
|
+
refresh();
|
|
92
|
+
},
|
|
93
|
+
onError: () => {
|
|
94
|
+
notify("Failed to resend invitation email", { type: "error" });
|
|
95
|
+
setInviteDialogOpen(false);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const { mutate: resetPassword, isPending: isResetPending } = useMutation({
|
|
100
|
+
mutationFn: async () => {
|
|
101
|
+
return dataProvider.resetPassword(record.id);
|
|
102
|
+
},
|
|
103
|
+
onSuccess: () => {
|
|
104
|
+
notify("Password reset email sent successfully");
|
|
105
|
+
setResetDialogOpen(false);
|
|
106
|
+
refresh();
|
|
107
|
+
},
|
|
108
|
+
onError: () => {
|
|
109
|
+
notify("Failed to send password reset email", { type: "error" });
|
|
110
|
+
setResetDialogOpen(false);
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!record) return null;
|
|
115
|
+
|
|
116
|
+
// Determine which action to show based on confirmation status
|
|
117
|
+
const isConfirmed = record.email_confirmed_at !== null;
|
|
118
|
+
const isDisabled = record.disabled === true;
|
|
119
|
+
|
|
120
|
+
console.log("[RowActions] Record:", {
|
|
121
|
+
email: record.email,
|
|
122
|
+
email_confirmed_at: record.email_confirmed_at,
|
|
123
|
+
isConfirmed,
|
|
124
|
+
isDisabled,
|
|
125
|
+
inviteDialogOpen,
|
|
126
|
+
resetDialogOpen,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<>
|
|
131
|
+
<DropdownMenu>
|
|
132
|
+
<DropdownMenuTrigger asChild>
|
|
133
|
+
<Button variant="ghost" className="h-8 w-8 p-0">
|
|
134
|
+
<span className="sr-only">Open menu</span>
|
|
135
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
136
|
+
</Button>
|
|
137
|
+
</DropdownMenuTrigger>
|
|
138
|
+
<DropdownMenuContent align="end">
|
|
139
|
+
<DropdownMenuLabel>Email Actions</DropdownMenuLabel>
|
|
140
|
+
<DropdownMenuSeparator />
|
|
141
|
+
{!isConfirmed && (
|
|
142
|
+
<DropdownMenuItem
|
|
143
|
+
onClick={(e) => {
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
e.stopPropagation();
|
|
146
|
+
console.log("[Menu] Clicked Resend Invite");
|
|
147
|
+
setInviteDialogOpen(true);
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<RefreshCw className="mr-2 h-4 w-4" />
|
|
151
|
+
Resend Invite
|
|
152
|
+
</DropdownMenuItem>
|
|
153
|
+
)}
|
|
154
|
+
{isConfirmed && !isDisabled && (
|
|
155
|
+
<DropdownMenuItem onClick={() => setResetDialogOpen(true)}>
|
|
156
|
+
<KeyRound className="mr-2 h-4 w-4" />
|
|
157
|
+
Send Password Reset
|
|
158
|
+
</DropdownMenuItem>
|
|
159
|
+
)}
|
|
160
|
+
</DropdownMenuContent>
|
|
161
|
+
</DropdownMenu>
|
|
162
|
+
|
|
163
|
+
{/* Resend Invite Dialog */}
|
|
164
|
+
<Dialog open={inviteDialogOpen} onOpenChange={setInviteDialogOpen}>
|
|
165
|
+
<DialogContent
|
|
166
|
+
onClick={(e) => e.stopPropagation()}
|
|
167
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
168
|
+
>
|
|
169
|
+
<DialogHeader>
|
|
170
|
+
<DialogTitle>Resend Invitation</DialogTitle>
|
|
171
|
+
<DialogDescription>
|
|
172
|
+
Send a new invitation email to <strong>{record.email}</strong>?
|
|
173
|
+
<br />
|
|
174
|
+
<br />
|
|
175
|
+
This will send them a fresh invitation link to set up their account.
|
|
176
|
+
</DialogDescription>
|
|
177
|
+
</DialogHeader>
|
|
178
|
+
<DialogFooter>
|
|
179
|
+
<Button
|
|
180
|
+
variant="outline"
|
|
181
|
+
onClick={(e) => {
|
|
182
|
+
e.stopPropagation();
|
|
183
|
+
setInviteDialogOpen(false);
|
|
184
|
+
}}
|
|
185
|
+
disabled={isInvitePending}
|
|
186
|
+
>
|
|
187
|
+
Cancel
|
|
188
|
+
</Button>
|
|
189
|
+
<Button
|
|
190
|
+
onClick={(e) => {
|
|
191
|
+
e.stopPropagation();
|
|
192
|
+
resendInvite();
|
|
193
|
+
}}
|
|
194
|
+
disabled={isInvitePending}
|
|
195
|
+
>
|
|
196
|
+
{isInvitePending ? "Sending..." : "Send Invitation"}
|
|
197
|
+
</Button>
|
|
198
|
+
</DialogFooter>
|
|
199
|
+
</DialogContent>
|
|
200
|
+
</Dialog>
|
|
201
|
+
|
|
202
|
+
{/* Password Reset Dialog */}
|
|
203
|
+
<Dialog open={resetDialogOpen} onOpenChange={setResetDialogOpen}>
|
|
204
|
+
<DialogContent
|
|
205
|
+
onClick={(e) => e.stopPropagation()}
|
|
206
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
207
|
+
>
|
|
208
|
+
<DialogHeader>
|
|
209
|
+
<DialogTitle>Send Password Reset</DialogTitle>
|
|
210
|
+
<DialogDescription>
|
|
211
|
+
Send a password reset email to <strong>{record.email}</strong>?
|
|
212
|
+
<br />
|
|
213
|
+
<br />
|
|
214
|
+
This will send them a link to reset their password.
|
|
215
|
+
</DialogDescription>
|
|
216
|
+
</DialogHeader>
|
|
217
|
+
<DialogFooter>
|
|
218
|
+
<Button
|
|
219
|
+
variant="outline"
|
|
220
|
+
onClick={(e) => {
|
|
221
|
+
e.stopPropagation();
|
|
222
|
+
setResetDialogOpen(false);
|
|
223
|
+
}}
|
|
224
|
+
disabled={isResetPending}
|
|
225
|
+
>
|
|
226
|
+
Cancel
|
|
227
|
+
</Button>
|
|
228
|
+
<Button
|
|
229
|
+
onClick={(e) => {
|
|
230
|
+
e.stopPropagation();
|
|
231
|
+
resetPassword();
|
|
232
|
+
}}
|
|
233
|
+
disabled={isResetPending}
|
|
234
|
+
>
|
|
235
|
+
{isResetPending ? "Sending..." : "Send Reset Link"}
|
|
236
|
+
</Button>
|
|
237
|
+
</DialogFooter>
|
|
238
|
+
</DialogContent>
|
|
239
|
+
</Dialog>
|
|
240
|
+
</>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
48
244
|
export function SalesList() {
|
|
49
245
|
return (
|
|
50
246
|
<List
|
|
@@ -91,6 +287,9 @@ const SalesListLayout = () => {
|
|
|
91
287
|
<DataTable.Col label={false}>
|
|
92
288
|
<OptionsField />
|
|
93
289
|
</DataTable.Col>
|
|
290
|
+
<DataTable.Col label={false}>
|
|
291
|
+
<RowActions />
|
|
292
|
+
</DataTable.Col>
|
|
94
293
|
</DataTable>
|
|
95
294
|
);
|
|
96
295
|
};
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "ra-core";
|
|
11
11
|
import { useState } from "react";
|
|
12
12
|
import { useFormState } from "react-hook-form";
|
|
13
|
+
import { useNavigate } from "react-router";
|
|
13
14
|
import { RecordField } from "@/components/admin/record-field";
|
|
14
15
|
import { TextInput } from "@/components/admin/text-input";
|
|
15
16
|
import { Button } from "@/components/ui/button";
|
|
@@ -78,28 +79,14 @@ const SettingsForm = ({
|
|
|
78
79
|
setEditMode: (value: boolean) => void;
|
|
79
80
|
}) => {
|
|
80
81
|
const notify = useNotify();
|
|
82
|
+
const navigate = useNavigate();
|
|
81
83
|
const record = useRecordContext<Sale>();
|
|
82
84
|
const { identity, refetch } = useGetIdentity();
|
|
83
85
|
const { isDirty } = useFormState();
|
|
84
86
|
const dataProvider = useDataProvider<CrmDataProvider>();
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
mutationFn: async () => {
|
|
89
|
-
if (!identity) {
|
|
90
|
-
throw new Error("Record not found");
|
|
91
|
-
}
|
|
92
|
-
return dataProvider.updatePassword(identity.id);
|
|
93
|
-
},
|
|
94
|
-
onSuccess: () => {
|
|
95
|
-
notify("A reset password email has been sent to your email address");
|
|
96
|
-
},
|
|
97
|
-
onError: (e) => {
|
|
98
|
-
notify(`${e}`, {
|
|
99
|
-
type: "error",
|
|
100
|
-
});
|
|
101
|
-
},
|
|
102
|
-
});
|
|
88
|
+
// Removed old updatePassword mutation - now uses OTP flow
|
|
89
|
+
// Users will be redirected to forgot-password page which sends OTP
|
|
103
90
|
|
|
104
91
|
const { mutate: mutateSale } = useMutation({
|
|
105
92
|
mutationKey: ["signup"],
|
|
@@ -120,7 +107,8 @@ const SettingsForm = ({
|
|
|
120
107
|
if (!identity) return null;
|
|
121
108
|
|
|
122
109
|
const handleClickOpenPasswordChange = () => {
|
|
123
|
-
|
|
110
|
+
// Redirect to forgot-password page which uses OTP flow
|
|
111
|
+
navigate('/forgot-password');
|
|
124
112
|
};
|
|
125
113
|
|
|
126
114
|
const handleAvatarUpdate = async (values: any) => {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Form, required, useNotify, useTranslate } from "ra-core";
|
|
3
|
+
import { Layout } from "@/components/supabase/layout";
|
|
4
|
+
import type { FieldValues, SubmitHandler } from "react-hook-form";
|
|
5
|
+
import { TextInput } from "@/components/admin/text-input";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { supabase } from "@/components/atomic-crm/providers/supabase/supabase";
|
|
8
|
+
import { useNavigate } from "react-router";
|
|
9
|
+
|
|
10
|
+
interface FormData {
|
|
11
|
+
password: string;
|
|
12
|
+
confirm: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ChangePasswordPage = () => {
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
|
+
|
|
18
|
+
const notify = useNotify();
|
|
19
|
+
const translate = useTranslate();
|
|
20
|
+
const navigate = useNavigate();
|
|
21
|
+
|
|
22
|
+
const validate = (values: FormData) => {
|
|
23
|
+
const errors: Record<string, string> = {};
|
|
24
|
+
|
|
25
|
+
if (!values.password) {
|
|
26
|
+
errors.password = translate("ra.validation.required");
|
|
27
|
+
} else if (values.password.length < 6) {
|
|
28
|
+
errors.password = "Password must be at least 6 characters";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!values.confirm) {
|
|
32
|
+
errors.confirm = translate("ra.validation.required");
|
|
33
|
+
} else if (values.password !== values.confirm) {
|
|
34
|
+
errors.confirm = "Passwords do not match";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return errors;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const submit = async (values: FormData) => {
|
|
41
|
+
try {
|
|
42
|
+
setLoading(true);
|
|
43
|
+
|
|
44
|
+
const { error } = await supabase.auth.updateUser({
|
|
45
|
+
password: values.password,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (error) {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
notify("Password updated successfully", { type: "success" });
|
|
53
|
+
navigate("/");
|
|
54
|
+
} catch (error: any) {
|
|
55
|
+
notify(
|
|
56
|
+
typeof error === "string"
|
|
57
|
+
? error
|
|
58
|
+
: typeof error === "undefined" || !error.message
|
|
59
|
+
? "Failed to update password"
|
|
60
|
+
: error.message,
|
|
61
|
+
{
|
|
62
|
+
type: "warning",
|
|
63
|
+
messageArgs: {
|
|
64
|
+
_:
|
|
65
|
+
typeof error === "string"
|
|
66
|
+
? error
|
|
67
|
+
: error && error.message
|
|
68
|
+
? error.message
|
|
69
|
+
: undefined,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
} finally {
|
|
74
|
+
setLoading(false);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Layout>
|
|
80
|
+
<div className="flex flex-col space-y-2 text-center">
|
|
81
|
+
<h1 className="text-2xl font-semibold tracking-tight">
|
|
82
|
+
Set new password
|
|
83
|
+
</h1>
|
|
84
|
+
<p className="text-sm text-muted-foreground">
|
|
85
|
+
Choose a secure password for your account
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
<Form<FormData>
|
|
89
|
+
className="space-y-6"
|
|
90
|
+
onSubmit={submit as SubmitHandler<FieldValues>}
|
|
91
|
+
validate={validate}
|
|
92
|
+
>
|
|
93
|
+
<TextInput
|
|
94
|
+
source="password"
|
|
95
|
+
type="password"
|
|
96
|
+
label={translate("ra.auth.password", {
|
|
97
|
+
_: "New password",
|
|
98
|
+
})}
|
|
99
|
+
autoComplete="new-password"
|
|
100
|
+
validate={required()}
|
|
101
|
+
/>
|
|
102
|
+
<TextInput
|
|
103
|
+
source="confirm"
|
|
104
|
+
type="password"
|
|
105
|
+
label={translate("ra.auth.confirm_password", {
|
|
106
|
+
_: "Confirm password",
|
|
107
|
+
})}
|
|
108
|
+
autoComplete="new-password"
|
|
109
|
+
validate={required()}
|
|
110
|
+
/>
|
|
111
|
+
<Button type="submit" className="cursor-pointer w-full" disabled={loading}>
|
|
112
|
+
{loading ? "Updating..." : "Update password"}
|
|
113
|
+
</Button>
|
|
114
|
+
</Form>
|
|
115
|
+
</Layout>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
ChangePasswordPage.path = "change-password";
|