Implement OTP-based email verification system

Add custom email verification using 6-digit OTP codes via Mailketing API:

Database:
- Create auth_otps table with 15-minute expiry
- Add indexes and RLS policies for security
- Add cleanup function for expired tokens
- Insert default auth_email_verification template

Edge Functions:
- send-auth-otp: Generate OTP, store in DB, send via Mailketing
- verify-auth-otp: Validate OTP, confirm email in Supabase Auth

Frontend:
- Add OTP input state to auth page
- Implement send/verify OTP in useAuth hook
- Add resend countdown timer (60 seconds)
- Update auth flow: signup → OTP verification → login

Features:
- Instant email delivery (no queue/cron)
- 6-digit OTP with 15-minute expiry
- Resend OTP with cooldown
- Admin-configurable email templates
- Indonesian UI text

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-01-02 13:27:46 +07:00
parent b1aefea526
commit 0d29c953c1
5 changed files with 615 additions and 4 deletions

View File

@@ -8,8 +8,10 @@ interface AuthContextType {
loading: boolean;
isAdmin: boolean;
signIn: (email: string, password: string) => Promise<{ error: Error | null }>;
signUp: (email: string, password: string, name: string) => Promise<{ error: Error | null }>;
signUp: (email: string, password: string, name: string) => Promise<{ error: Error | null; data?: { user?: User; session?: Session } }>;
signOut: () => Promise<void>;
sendAuthOTP: (userId: string, email: string) => Promise<{ success: boolean; message: string }>;
verifyAuthOTP: (userId: string, otpCode: string) => Promise<{ success: boolean; message: string }>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
@@ -87,7 +89,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const signUp = async (email: string, password: string, name: string) => {
const redirectUrl = `${window.location.origin}/`;
const { error } = await supabase.auth.signUp({
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
@@ -95,15 +97,69 @@ export function AuthProvider({ children }: { children: ReactNode }) {
data: { name }
}
});
return { error };
return { error, data };
};
const signOut = async () => {
await supabase.auth.signOut();
};
const sendAuthOTP = async (userId: string, email: string) => {
try {
const { data: { session } } = await supabase.auth.getSession();
const response = await fetch(
`${import.meta.env.VITE_SUPABASE_URL}/functions/v1/send-auth-otp`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${session?.access_token || import.meta.env.VITE_SUPABASE_ANON_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user_id: userId, email }),
}
);
const result = await response.json();
return result;
} catch (error: any) {
console.error('Error sending OTP:', error);
return {
success: false,
message: error.message || 'Failed to send OTP'
};
}
};
const verifyAuthOTP = async (userId: string, otpCode: string) => {
try {
const { data: { session } } = await supabase.auth.getSession();
const response = await fetch(
`${import.meta.env.VITE_SUPABASE_URL}/functions/v1/verify-auth-otp`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${session?.access_token || import.meta.env.VITE_SUPABASE_ANON_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user_id: userId, otp_code: otpCode }),
}
);
const result = await response.json();
return result;
} catch (error: any) {
console.error('Error verifying OTP:', error);
return {
success: false,
message: error.message || 'Failed to verify OTP'
};
}
};
return (
<AuthContext.Provider value={{ user, session, loading, isAdmin, signIn, signUp, signOut }}>
<AuthContext.Provider value={{ user, session, loading, isAdmin, signIn, signUp, signOut, sendAuthOTP, verifyAuthOTP }}>
{children}
</AuthContext.Provider>
);