mirror of
https://github.com/willchen96/mike.git
synced 2026-06-26 21:39:39 +02:00
- Add SecurityPage component for managing MFA settings, including enrollment and verification. - Create MfaLoginGate to handle MFA verification state during login. - Develop MfaVerificationPopup for user input of verification codes. - Implement VerifyMfaPage for the MFA verification process after login. - Introduce reusable VerificationCodeInput component for entering verification codes. - Integrate Supabase MFA API for managing factors and verification. - Add loading states and error handling for a better user experience.
292 lines
12 KiB
TypeScript
292 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { supabase } from "@/lib/supabase";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import Link from "next/link";
|
|
import { SiteLogo } from "@/components/site-logo";
|
|
import { CheckCircle2 } from "lucide-react";
|
|
import { useAuth } from "@/contexts/AuthContext";
|
|
import { updateUserProfile } from "@/app/lib/mikeApi";
|
|
|
|
const authGlassCardClassName =
|
|
"rounded-2xl border border-white/70 bg-white/72 p-8 shadow-[0_4px_14px_rgba(15,23,42,0.045),inset_0_1px_0_rgba(255,255,255,0.86),inset_0_-8px_18px_rgba(255,255,255,0.12)] backdrop-blur-2xl";
|
|
const authInputClassName =
|
|
"rounded-lg border border-transparent bg-gray-100 px-3 shadow-none focus-visible:border-gray-200 focus-visible:ring-2 focus-visible:ring-gray-300/45";
|
|
|
|
export default function SignupPage() {
|
|
const router = useRouter();
|
|
const { isAuthenticated, authLoading } = useAuth();
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
const [name, setName] = useState("");
|
|
const [organisation, setOrganisation] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [success, setSuccess] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!authLoading && isAuthenticated && !success) {
|
|
router.replace("/assistant");
|
|
}
|
|
}, [authLoading, isAuthenticated, router, success]);
|
|
|
|
const handleSignup = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
// Validate passwords match
|
|
if (password !== confirmPassword) {
|
|
setError("Passwords do not match");
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
// Validate password length
|
|
if (password.length < 6) {
|
|
setError("Password must be at least 6 characters");
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const { data, error } = await supabase.auth.signUp({
|
|
email,
|
|
password,
|
|
});
|
|
|
|
if (error) throw error;
|
|
|
|
if (data.session) {
|
|
const trimmedName = name.trim();
|
|
const trimmedOrg = organisation.trim();
|
|
if (trimmedName || trimmedOrg) {
|
|
try {
|
|
await updateUserProfile({
|
|
...(trimmedName && { displayName: trimmedName }),
|
|
...(trimmedOrg && { organisation: trimmedOrg }),
|
|
});
|
|
} catch (profileError) {
|
|
console.error(
|
|
"[signup] failed to persist profile fields",
|
|
profileError,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
setSuccess(true);
|
|
setTimeout(() => {
|
|
router.push("/assistant");
|
|
}, 2000);
|
|
} catch (error: unknown) {
|
|
setError(
|
|
error instanceof Error
|
|
? error.message
|
|
: "An error occurred during signup",
|
|
);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Success View
|
|
if (success) {
|
|
return (
|
|
<div className="min-h-dvh bg-gray-50/80 flex items-start justify-center px-6 pt-32 md:pt-40 pb-10 relative">
|
|
<div className="absolute top-4 md:top-8 left-1/2 -translate-x-1/2">
|
|
<SiteLogo size="lg" asLink />
|
|
</div>
|
|
<div className="w-full max-w-md">
|
|
<div
|
|
className={`${authGlassCardClassName} p-10 text-center`}
|
|
>
|
|
<div className="mx-auto w-12 h-12 bg-green-50 rounded-full flex items-center justify-center mb-6">
|
|
<CheckCircle2 className="h-6 w-6 text-green-600" />
|
|
</div>
|
|
<h2 className="text-2xl font-semibold text-gray-900 mb-3">
|
|
Account created!
|
|
</h2>
|
|
<p className="text-gray-600 leading-relaxed">
|
|
Redirecting you to the home page...
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Default Signup Form View
|
|
return (
|
|
<div className="min-h-dvh bg-gray-50/80 flex items-start justify-center px-6 pt-32 md:pt-40 pb-10 relative">
|
|
<div className="absolute top-4 md:top-8 left-1/2 -translate-x-1/2">
|
|
<SiteLogo size="lg" asLink />
|
|
</div>
|
|
<div className="w-full max-w-md">
|
|
<div className={`${authGlassCardClassName} mb-4`}>
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h2 className="text-left text-2xl font-serif">
|
|
Create Account
|
|
</h2>
|
|
<div className="bg-gray-200/70 p-1 rounded-lg flex text-xs font-medium shadow-[inset_0_1px_0_rgba(255,255,255,0.65),inset_0_-3px_8px_rgba(148,163,184,0.16)] backdrop-blur-xl">
|
|
<Link
|
|
href="/login"
|
|
className="px-3 py-1 text-gray-500 hover:text-gray-900"
|
|
>
|
|
Log in
|
|
</Link>
|
|
<span className="px-3 py-1 bg-white/85 rounded-md shadow-[0_1px_4px_rgba(15,23,42,0.06)] text-gray-900">
|
|
Sign up
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleSignup} className="space-y-4">
|
|
<div>
|
|
<label
|
|
htmlFor="name"
|
|
className="block text-sm font-medium text-gray-700 mb-2"
|
|
>
|
|
Name{" "}
|
|
<span className="text-gray-400 font-normal">
|
|
(optional)
|
|
</span>
|
|
</label>
|
|
<Input
|
|
id="name"
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Your name"
|
|
className={`w-full ${authInputClassName}`}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
htmlFor="organisation"
|
|
className="block text-sm font-medium text-gray-700 mb-2"
|
|
>
|
|
Organisation{" "}
|
|
<span className="text-gray-400 font-normal">
|
|
(optional)
|
|
</span>
|
|
</label>
|
|
<Input
|
|
id="organisation"
|
|
type="text"
|
|
value={organisation}
|
|
onChange={(e) =>
|
|
setOrganisation(e.target.value)
|
|
}
|
|
placeholder="Your organisation"
|
|
className={`w-full ${authInputClassName}`}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
htmlFor="email"
|
|
className="block text-sm font-medium text-gray-700 mb-2"
|
|
>
|
|
Email
|
|
</label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder="Enter your email"
|
|
required
|
|
className={`w-full ${authInputClassName}`}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
htmlFor="password"
|
|
className="block text-sm font-medium text-gray-700 mb-2"
|
|
>
|
|
Password
|
|
</label>
|
|
<Input
|
|
id="password"
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="Create a password (min. 6 characters)"
|
|
required
|
|
className={`w-full ${authInputClassName}`}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
htmlFor="confirmPassword"
|
|
className="block text-sm font-medium text-gray-700 mb-2"
|
|
>
|
|
Confirm Password
|
|
</label>
|
|
<Input
|
|
id="confirmPassword"
|
|
type="password"
|
|
value={confirmPassword}
|
|
onChange={(e) =>
|
|
setConfirmPassword(e.target.value)
|
|
}
|
|
placeholder="Confirm your password"
|
|
required
|
|
className={`w-full ${authInputClassName}`}
|
|
/>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="text-red-600 text-sm bg-red-50 p-3 rounded">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<Button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="w-full bg-black hover:bg-gray-900 text-white"
|
|
>
|
|
{loading ? "Creating account..." : "Sign up"}
|
|
</Button>
|
|
</form>
|
|
|
|
{/* Terms and Privacy */}
|
|
<div className="mt-4 text-center text-xs text-gray-500">
|
|
By signing up, you agree to our{" "}
|
|
<Link
|
|
href="https://mikeoss.com/terms"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-blue-600 hover:underline"
|
|
>
|
|
Terms of Use
|
|
</Link>{" "}
|
|
and{" "}
|
|
<Link
|
|
href="https://mikeoss.com/privacy"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-blue-600 hover:underline"
|
|
>
|
|
Privacy Policy
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
<p className="text-center text-xs text-gray-500 leading-relaxed px-2">
|
|
Mike hosted on MikeOSS.com is currently a demo service.
|
|
Please do not upload, submit, or store sensitive,
|
|
confidential, privileged, client, or personally identifiable
|
|
documents.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|