realtimex-crm 0.5.7 → 0.6.3
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/bin/realtimex-crm.js +155 -0
- 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
|
@@ -1,28 +1,48 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
|
-
import { useResetPassword } from "ra-supabase-core";
|
|
3
2
|
import { Form, required, useNotify, useTranslate } from "ra-core";
|
|
4
3
|
import { Layout } from "@/components/supabase/layout";
|
|
5
4
|
import type { FieldValues, SubmitHandler } from "react-hook-form";
|
|
6
5
|
import { TextInput } from "@/components/admin/text-input";
|
|
7
6
|
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { OtpInput } from "@/components/supabase/otp-input";
|
|
8
|
+
import { supabase } from "@/components/atomic-crm/providers/supabase/supabase";
|
|
8
9
|
|
|
9
|
-
interface
|
|
10
|
+
interface EmailFormData {
|
|
10
11
|
email: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
type Step = 'email' | 'otp';
|
|
15
|
+
|
|
13
16
|
export const ForgotPasswordPage = () => {
|
|
14
17
|
const [loading, setLoading] = useState(false);
|
|
18
|
+
const [step, setStep] = useState<Step>('email');
|
|
19
|
+
const [email, setEmail] = useState('');
|
|
20
|
+
const [otp, setOtp] = useState('');
|
|
21
|
+
const [otpError, setOtpError] = useState(false);
|
|
15
22
|
|
|
16
23
|
const notify = useNotify();
|
|
17
24
|
const translate = useTranslate();
|
|
18
|
-
const [, { mutateAsync: resetPassword }] = useResetPassword();
|
|
19
25
|
|
|
20
|
-
const
|
|
26
|
+
const submitEmail = async (values: EmailFormData) => {
|
|
21
27
|
try {
|
|
22
28
|
setLoading(true);
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
// Normalize email to lowercase
|
|
30
|
+
const normalizedEmail = values.email.trim().toLowerCase();
|
|
31
|
+
setEmail(normalizedEmail);
|
|
32
|
+
|
|
33
|
+
const { error } = await supabase.auth.signInWithOtp({
|
|
34
|
+
email: normalizedEmail,
|
|
35
|
+
options: {
|
|
36
|
+
shouldCreateUser: false, // Only allow existing users to reset password
|
|
37
|
+
},
|
|
25
38
|
});
|
|
39
|
+
|
|
40
|
+
if (error) {
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
notify('A 6-digit code has been sent to your email', { type: 'success' });
|
|
45
|
+
setStep('otp');
|
|
26
46
|
} catch (error: any) {
|
|
27
47
|
notify(
|
|
28
48
|
typeof error === "string"
|
|
@@ -47,38 +67,163 @@ export const ForgotPasswordPage = () => {
|
|
|
47
67
|
}
|
|
48
68
|
};
|
|
49
69
|
|
|
70
|
+
const verifyOtp = async (otpCode: string) => {
|
|
71
|
+
try {
|
|
72
|
+
setLoading(true);
|
|
73
|
+
setOtpError(false);
|
|
74
|
+
|
|
75
|
+
// Trim whitespace from OTP code
|
|
76
|
+
const cleanOtp = otpCode.trim();
|
|
77
|
+
|
|
78
|
+
console.log('Verifying OTP (forgot password):', { email, token: cleanOtp, type: 'email' });
|
|
79
|
+
|
|
80
|
+
const { data, error } = await supabase.auth.verifyOtp({
|
|
81
|
+
email: email.trim().toLowerCase(), // Normalize email
|
|
82
|
+
token: cleanOtp,
|
|
83
|
+
type: 'magiclink', // Changed from 'email' - some Supabase versions treat OTP as magiclink
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
console.log('OTP Verification result:', { data, error });
|
|
87
|
+
|
|
88
|
+
if (error) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!data.session) {
|
|
93
|
+
throw new Error('Failed to create session');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log('Session created successfully for password reset');
|
|
97
|
+
|
|
98
|
+
// User is now logged in, redirect to change password page
|
|
99
|
+
notify('Code verified! Please set your new password.', { type: 'success' });
|
|
100
|
+
|
|
101
|
+
// IMPORTANT: Don't call login() - user is already authenticated via Supabase
|
|
102
|
+
// The OTP verification already set the session
|
|
103
|
+
// Calling login({}) with empty params could cause session confusion
|
|
104
|
+
|
|
105
|
+
// Navigate to change password page with reload
|
|
106
|
+
console.log('Navigating to /change-password');
|
|
107
|
+
window.location.href = '#/change-password';
|
|
108
|
+
window.location.reload();
|
|
109
|
+
} catch (error: any) {
|
|
110
|
+
setOtpError(true);
|
|
111
|
+
notify(
|
|
112
|
+
typeof error === "string"
|
|
113
|
+
? error
|
|
114
|
+
: typeof error === "undefined" || !error.message
|
|
115
|
+
? "Invalid or expired code"
|
|
116
|
+
: error.message,
|
|
117
|
+
{
|
|
118
|
+
type: "warning",
|
|
119
|
+
messageArgs: {
|
|
120
|
+
_:
|
|
121
|
+
typeof error === "string"
|
|
122
|
+
? error
|
|
123
|
+
: error && error.message
|
|
124
|
+
? error.message
|
|
125
|
+
: undefined,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
} finally {
|
|
130
|
+
setLoading(false);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const handleOtpComplete = (otpCode: string) => {
|
|
135
|
+
verifyOtp(otpCode);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const handleResendCode = async () => {
|
|
139
|
+
setOtp('');
|
|
140
|
+
setOtpError(false);
|
|
141
|
+
await submitEmail({ email });
|
|
142
|
+
};
|
|
143
|
+
|
|
50
144
|
return (
|
|
51
145
|
<Layout>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
146
|
+
{step === 'email' ? (
|
|
147
|
+
<>
|
|
148
|
+
<div className="flex flex-col space-y-2 text-center">
|
|
149
|
+
<h1 className="text-2xl font-semibold tracking-tight">
|
|
150
|
+
{translate("ra-supabase.reset_password.forgot_password", {
|
|
151
|
+
_: "Forgot password?",
|
|
152
|
+
})}
|
|
153
|
+
</h1>
|
|
154
|
+
<p>
|
|
155
|
+
{translate("ra-supabase.reset_password.forgot_password_details", {
|
|
156
|
+
_: "Enter your email to receive a 6-digit code.",
|
|
157
|
+
})}
|
|
158
|
+
</p>
|
|
159
|
+
</div>
|
|
160
|
+
<Form<EmailFormData>
|
|
161
|
+
className="space-y-8"
|
|
162
|
+
onSubmit={submitEmail as SubmitHandler<FieldValues>}
|
|
163
|
+
>
|
|
164
|
+
<TextInput
|
|
165
|
+
source="email"
|
|
166
|
+
label={translate("ra.auth.email", {
|
|
167
|
+
_: "Email",
|
|
168
|
+
})}
|
|
169
|
+
autoComplete="email"
|
|
170
|
+
validate={required()}
|
|
171
|
+
/>
|
|
172
|
+
<Button type="submit" className="cursor-pointer w-full" disabled={loading}>
|
|
173
|
+
{translate("ra.action.reset_password", {
|
|
174
|
+
_: "Send code",
|
|
175
|
+
})}
|
|
176
|
+
</Button>
|
|
177
|
+
</Form>
|
|
178
|
+
</>
|
|
179
|
+
) : (
|
|
180
|
+
<>
|
|
181
|
+
<div className="flex flex-col space-y-2 text-center">
|
|
182
|
+
<h1 className="text-2xl font-semibold tracking-tight">
|
|
183
|
+
Enter verification code
|
|
184
|
+
</h1>
|
|
185
|
+
<p className="text-sm text-muted-foreground">
|
|
186
|
+
We've sent a 6-digit code to {email}
|
|
187
|
+
</p>
|
|
188
|
+
</div>
|
|
189
|
+
<div className="space-y-6">
|
|
190
|
+
<div className="space-y-4">
|
|
191
|
+
<OtpInput
|
|
192
|
+
length={6}
|
|
193
|
+
value={otp}
|
|
194
|
+
onChange={setOtp}
|
|
195
|
+
onComplete={handleOtpComplete}
|
|
196
|
+
disabled={loading}
|
|
197
|
+
error={otpError}
|
|
198
|
+
/>
|
|
199
|
+
{otpError && (
|
|
200
|
+
<p className="text-sm text-destructive text-center">
|
|
201
|
+
Invalid or expired code. Please try again.
|
|
202
|
+
</p>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
<div className="flex flex-col gap-2">
|
|
206
|
+
<Button
|
|
207
|
+
type="button"
|
|
208
|
+
className="cursor-pointer w-full"
|
|
209
|
+
disabled={loading || otp.length !== 6}
|
|
210
|
+
onClick={() => verifyOtp(otp)}
|
|
211
|
+
>
|
|
212
|
+
{loading ? 'Verifying...' : 'Verify code'}
|
|
213
|
+
</Button>
|
|
214
|
+
<Button
|
|
215
|
+
type="button"
|
|
216
|
+
variant="outline"
|
|
217
|
+
className="cursor-pointer w-full"
|
|
218
|
+
disabled={loading}
|
|
219
|
+
onClick={handleResendCode}
|
|
220
|
+
>
|
|
221
|
+
Resend code
|
|
222
|
+
</Button>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</>
|
|
226
|
+
)}
|
|
82
227
|
</Layout>
|
|
83
228
|
);
|
|
84
229
|
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useRef, useState, KeyboardEvent, ClipboardEvent } from 'react';
|
|
2
|
+
import { Input } from '@/components/ui/input';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
interface OtpInputProps {
|
|
6
|
+
length?: number;
|
|
7
|
+
value: string;
|
|
8
|
+
onChange: (value: string) => void;
|
|
9
|
+
onComplete?: (value: string) => void;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
error?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function OtpInput({
|
|
15
|
+
length = 6,
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
onComplete,
|
|
19
|
+
disabled = false,
|
|
20
|
+
error = false,
|
|
21
|
+
}: OtpInputProps) {
|
|
22
|
+
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
|
23
|
+
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
|
24
|
+
|
|
25
|
+
const handleChange = (index: number, inputValue: string) => {
|
|
26
|
+
// Only allow digits
|
|
27
|
+
const digit = inputValue.replace(/[^0-9]/g, '');
|
|
28
|
+
|
|
29
|
+
if (digit.length === 0) {
|
|
30
|
+
// Handle backspace/delete
|
|
31
|
+
const newValue = value.split('');
|
|
32
|
+
newValue[index] = '';
|
|
33
|
+
const updatedValue = newValue.join('');
|
|
34
|
+
onChange(updatedValue);
|
|
35
|
+
|
|
36
|
+
// Move to previous input
|
|
37
|
+
if (index > 0) {
|
|
38
|
+
inputRefs.current[index - 1]?.focus();
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Update the value at the current index
|
|
44
|
+
const newValue = value.split('');
|
|
45
|
+
newValue[index] = digit[0];
|
|
46
|
+
const updatedValue = newValue.join('');
|
|
47
|
+
onChange(updatedValue);
|
|
48
|
+
|
|
49
|
+
// Move to next input if not the last one
|
|
50
|
+
if (index < length - 1) {
|
|
51
|
+
inputRefs.current[index + 1]?.focus();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if OTP is complete
|
|
55
|
+
if (updatedValue.length === length && onComplete) {
|
|
56
|
+
onComplete(updatedValue);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleKeyDown = (index: number, e: KeyboardEvent<HTMLInputElement>) => {
|
|
61
|
+
if (e.key === 'Backspace' && !value[index] && index > 0) {
|
|
62
|
+
// If current input is empty and backspace is pressed, move to previous
|
|
63
|
+
inputRefs.current[index - 1]?.focus();
|
|
64
|
+
} else if (e.key === 'ArrowLeft' && index > 0) {
|
|
65
|
+
inputRefs.current[index - 1]?.focus();
|
|
66
|
+
} else if (e.key === 'ArrowRight' && index < length - 1) {
|
|
67
|
+
inputRefs.current[index + 1]?.focus();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
const pastedData = e.clipboardData.getData('text/plain');
|
|
74
|
+
const digits = pastedData.replace(/[^0-9]/g, '').slice(0, length);
|
|
75
|
+
|
|
76
|
+
onChange(digits);
|
|
77
|
+
|
|
78
|
+
// Focus the next empty input or the last input
|
|
79
|
+
const nextIndex = Math.min(digits.length, length - 1);
|
|
80
|
+
inputRefs.current[nextIndex]?.focus();
|
|
81
|
+
|
|
82
|
+
// Check if OTP is complete
|
|
83
|
+
if (digits.length === length && onComplete) {
|
|
84
|
+
onComplete(digits);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="flex gap-2 justify-center">
|
|
90
|
+
{Array.from({ length }).map((_, index) => (
|
|
91
|
+
<Input
|
|
92
|
+
key={index}
|
|
93
|
+
ref={(el) => {
|
|
94
|
+
inputRefs.current[index] = el;
|
|
95
|
+
}}
|
|
96
|
+
type="text"
|
|
97
|
+
inputMode="numeric"
|
|
98
|
+
maxLength={1}
|
|
99
|
+
value={value[index] || ''}
|
|
100
|
+
onChange={(e) => handleChange(index, e.target.value)}
|
|
101
|
+
onKeyDown={(e) => handleKeyDown(index, e)}
|
|
102
|
+
onPaste={handlePaste}
|
|
103
|
+
onFocus={() => setFocusedIndex(index)}
|
|
104
|
+
onBlur={() => setFocusedIndex(null)}
|
|
105
|
+
disabled={disabled}
|
|
106
|
+
className={cn(
|
|
107
|
+
'w-12 h-12 text-center text-lg font-semibold',
|
|
108
|
+
error && 'border-destructive focus-visible:ring-destructive',
|
|
109
|
+
focusedIndex === index && 'ring-2 ring-ring'
|
|
110
|
+
)}
|
|
111
|
+
aria-label={`Digit ${index + 1}`}
|
|
112
|
+
/>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
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 { OtpInput } from "@/components/supabase/otp-input";
|
|
8
|
+
import { supabase } from "@/components/atomic-crm/providers/supabase/supabase";
|
|
9
|
+
|
|
10
|
+
interface EmailFormData {
|
|
11
|
+
email: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type Step = 'email' | 'otp';
|
|
15
|
+
|
|
16
|
+
export const OtpLoginPage = () => {
|
|
17
|
+
const [loading, setLoading] = useState(false);
|
|
18
|
+
const [step, setStep] = useState<Step>('email');
|
|
19
|
+
const [email, setEmail] = useState('');
|
|
20
|
+
const [otp, setOtp] = useState('');
|
|
21
|
+
const [otpError, setOtpError] = useState(false);
|
|
22
|
+
|
|
23
|
+
const notify = useNotify();
|
|
24
|
+
const translate = useTranslate();
|
|
25
|
+
|
|
26
|
+
const submitEmail = async (values: EmailFormData) => {
|
|
27
|
+
try {
|
|
28
|
+
setLoading(true);
|
|
29
|
+
// Normalize email to lowercase
|
|
30
|
+
const normalizedEmail = values.email.trim().toLowerCase();
|
|
31
|
+
setEmail(normalizedEmail);
|
|
32
|
+
|
|
33
|
+
const { error } = await supabase.auth.signInWithOtp({
|
|
34
|
+
email: normalizedEmail,
|
|
35
|
+
options: {
|
|
36
|
+
shouldCreateUser: false, // Only allow existing users
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (error) {
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
notify('A 6-digit code has been sent to your email', { type: 'success' });
|
|
45
|
+
setStep('otp');
|
|
46
|
+
} catch (error: any) {
|
|
47
|
+
notify(
|
|
48
|
+
typeof error === "string"
|
|
49
|
+
? error
|
|
50
|
+
: typeof error === "undefined" || !error.message
|
|
51
|
+
? "ra.auth.sign_in_error"
|
|
52
|
+
: error.message,
|
|
53
|
+
{
|
|
54
|
+
type: "warning",
|
|
55
|
+
messageArgs: {
|
|
56
|
+
_:
|
|
57
|
+
typeof error === "string"
|
|
58
|
+
? error
|
|
59
|
+
: error && error.message
|
|
60
|
+
? error.message
|
|
61
|
+
: undefined,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
} finally {
|
|
66
|
+
setLoading(false);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const verifyOtp = async (otpCode: string) => {
|
|
71
|
+
try {
|
|
72
|
+
setLoading(true);
|
|
73
|
+
setOtpError(false);
|
|
74
|
+
|
|
75
|
+
// Trim whitespace from OTP code
|
|
76
|
+
const cleanOtp = otpCode.trim();
|
|
77
|
+
|
|
78
|
+
console.log('Verifying OTP:', { email, token: cleanOtp, type: 'email' });
|
|
79
|
+
|
|
80
|
+
const { data, error } = await supabase.auth.verifyOtp({
|
|
81
|
+
email: email.trim().toLowerCase(), // Normalize email
|
|
82
|
+
token: cleanOtp,
|
|
83
|
+
type: 'magiclink', // Changed from 'email' - some Supabase versions treat OTP as magiclink
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
console.log('OTP Verification result:', { data, error });
|
|
87
|
+
|
|
88
|
+
if (error) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!data.session) {
|
|
93
|
+
throw new Error('Failed to create session');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log('Session created successfully, user:', data.user);
|
|
97
|
+
|
|
98
|
+
// Check if user exists in sales table (access control)
|
|
99
|
+
console.log('Checking sales table for user_id:', data.user.id);
|
|
100
|
+
const { data: saleData, error: saleError } = await supabase
|
|
101
|
+
.from('sales')
|
|
102
|
+
.select('id, email_confirmed_at')
|
|
103
|
+
.eq('user_id', data.user.id)
|
|
104
|
+
.single();
|
|
105
|
+
|
|
106
|
+
console.log('Sales table query result:', { saleData, saleError });
|
|
107
|
+
|
|
108
|
+
if (saleError || !saleData) {
|
|
109
|
+
// User authenticated but not in sales table - deny access
|
|
110
|
+
console.error('User not found in sales table or query error');
|
|
111
|
+
await supabase.auth.signOut();
|
|
112
|
+
throw new Error('You do not have access to this application. Please contact your administrator.');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// User is logged in and authorized
|
|
116
|
+
console.log('User authorized, OTP verification complete');
|
|
117
|
+
notify('Login successful!', { type: 'success' });
|
|
118
|
+
|
|
119
|
+
// IMPORTANT: Don't call login() - user is already authenticated via Supabase
|
|
120
|
+
// The OTP verification already set the session
|
|
121
|
+
// Calling login({}) with empty params could cause session confusion
|
|
122
|
+
|
|
123
|
+
// Force reload to ensure clean state
|
|
124
|
+
console.log('Reloading to establish session...');
|
|
125
|
+
|
|
126
|
+
// Check if this is their first login (email not confirmed yet)
|
|
127
|
+
if (!saleData.email_confirmed_at) {
|
|
128
|
+
// Navigate to change password
|
|
129
|
+
window.location.href = '#/change-password';
|
|
130
|
+
window.location.reload();
|
|
131
|
+
} else {
|
|
132
|
+
// Navigate to dashboard
|
|
133
|
+
window.location.href = '#/';
|
|
134
|
+
window.location.reload();
|
|
135
|
+
}
|
|
136
|
+
} catch (error: any) {
|
|
137
|
+
setOtpError(true);
|
|
138
|
+
notify(
|
|
139
|
+
typeof error === "string"
|
|
140
|
+
? error
|
|
141
|
+
: typeof error === "undefined" || !error.message
|
|
142
|
+
? "Invalid or expired code"
|
|
143
|
+
: error.message,
|
|
144
|
+
{
|
|
145
|
+
type: "warning",
|
|
146
|
+
messageArgs: {
|
|
147
|
+
_:
|
|
148
|
+
typeof error === "string"
|
|
149
|
+
? error
|
|
150
|
+
: error && error.message
|
|
151
|
+
? error.message
|
|
152
|
+
: undefined,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
} finally {
|
|
157
|
+
setLoading(false);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const handleOtpComplete = (otpCode: string) => {
|
|
162
|
+
verifyOtp(otpCode);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const handleResendCode = async () => {
|
|
166
|
+
setOtp('');
|
|
167
|
+
setOtpError(false);
|
|
168
|
+
await submitEmail({ email });
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<Layout>
|
|
173
|
+
{step === 'email' ? (
|
|
174
|
+
<>
|
|
175
|
+
<div className="flex flex-col space-y-2 text-center">
|
|
176
|
+
<h1 className="text-2xl font-semibold tracking-tight">
|
|
177
|
+
Login with code
|
|
178
|
+
</h1>
|
|
179
|
+
<p className="text-sm text-muted-foreground">
|
|
180
|
+
Enter your email to receive a 6-digit login code
|
|
181
|
+
</p>
|
|
182
|
+
</div>
|
|
183
|
+
<Form<EmailFormData>
|
|
184
|
+
className="space-y-8"
|
|
185
|
+
onSubmit={submitEmail as SubmitHandler<FieldValues>}
|
|
186
|
+
>
|
|
187
|
+
<TextInput
|
|
188
|
+
source="email"
|
|
189
|
+
label={translate("ra.auth.email", {
|
|
190
|
+
_: "Email",
|
|
191
|
+
})}
|
|
192
|
+
autoComplete="email"
|
|
193
|
+
validate={required()}
|
|
194
|
+
/>
|
|
195
|
+
<Button type="submit" className="cursor-pointer w-full" disabled={loading}>
|
|
196
|
+
{translate("ra.action.send_code", {
|
|
197
|
+
_: "Send code",
|
|
198
|
+
})}
|
|
199
|
+
</Button>
|
|
200
|
+
</Form>
|
|
201
|
+
</>
|
|
202
|
+
) : (
|
|
203
|
+
<>
|
|
204
|
+
<div className="flex flex-col space-y-2 text-center">
|
|
205
|
+
<h1 className="text-2xl font-semibold tracking-tight">
|
|
206
|
+
Enter verification code
|
|
207
|
+
</h1>
|
|
208
|
+
<p className="text-sm text-muted-foreground">
|
|
209
|
+
We've sent a 6-digit code to {email}
|
|
210
|
+
</p>
|
|
211
|
+
</div>
|
|
212
|
+
<div className="space-y-6">
|
|
213
|
+
<div className="space-y-4">
|
|
214
|
+
<OtpInput
|
|
215
|
+
length={6}
|
|
216
|
+
value={otp}
|
|
217
|
+
onChange={setOtp}
|
|
218
|
+
onComplete={handleOtpComplete}
|
|
219
|
+
disabled={loading}
|
|
220
|
+
error={otpError}
|
|
221
|
+
/>
|
|
222
|
+
{otpError && (
|
|
223
|
+
<p className="text-sm text-destructive text-center">
|
|
224
|
+
Invalid or expired code. Please try again.
|
|
225
|
+
</p>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
<div className="flex flex-col gap-2">
|
|
229
|
+
<Button
|
|
230
|
+
type="button"
|
|
231
|
+
className="cursor-pointer w-full"
|
|
232
|
+
disabled={loading || otp.length !== 6}
|
|
233
|
+
onClick={() => verifyOtp(otp)}
|
|
234
|
+
>
|
|
235
|
+
{loading ? 'Verifying...' : 'Verify code'}
|
|
236
|
+
</Button>
|
|
237
|
+
<Button
|
|
238
|
+
type="button"
|
|
239
|
+
variant="outline"
|
|
240
|
+
className="cursor-pointer w-full"
|
|
241
|
+
disabled={loading}
|
|
242
|
+
onClick={handleResendCode}
|
|
243
|
+
>
|
|
244
|
+
Resend code
|
|
245
|
+
</Button>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</>
|
|
249
|
+
)}
|
|
250
|
+
</Layout>
|
|
251
|
+
);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
OtpLoginPage.path = "otp-login";
|