diff --git a/README.md b/README.md index d35abdf..b912c68 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,10 @@ cp core-api/.env.example core-api/.env cp core-web/.env.example core-web/.env # Edit both .env files with your Supabase credentials + +# Optional: enable email/password sign-in (skips OAuth setup) +# In core-web/.env, set: +# VITE_ENABLE_EMAIL_AUTH=true ``` ### 2. Set up the database diff --git a/core-web/.env.example b/core-web/.env.example index a9fdcb3..4c232bb 100644 --- a/core-web/.env.example +++ b/core-web/.env.example @@ -17,6 +17,16 @@ VITE_API_URL=http://localhost:8000/api VITE_SUPABASE_URL=https://your-project.supabase.co VITE_SUPABASE_ANON_KEY=your-anon-key +# ------------------------------------------------------------------------------ +# [OPTIONAL] Email/Password Auth — Self-hosted +# ------------------------------------------------------------------------------ +# Enables email/password sign-in on the landing page. +# Disabled by default (hosted app uses OAuth only). +# Set to "true" for self-hosted deployments without OAuth configured. +# Tip: disable "Enable email confirmations" in Supabase (Auth > Settings) +# to skip the verification email step during local development. +VITE_ENABLE_EMAIL_AUTH= + # ------------------------------------------------------------------------------ # [OPTIONAL] Sentry — Error Tracking # ------------------------------------------------------------------------------ diff --git a/core-web/src/components/Landing/LandingPage.tsx b/core-web/src/components/Landing/LandingPage.tsx index f5d47e3..b3c01f0 100644 --- a/core-web/src/components/Landing/LandingPage.tsx +++ b/core-web/src/components/Landing/LandingPage.tsx @@ -6,6 +6,7 @@ import { useAuthStore } from "../../stores/authStore"; import { API_BASE } from "../../lib/apiBase"; const TURNSTILE_SITE_KEY = import.meta.env.VITE_TURNSTILE_SITE_KEY || ""; +const ENABLE_EMAIL_AUTH = import.meta.env.VITE_ENABLE_EMAIL_AUTH === "true"; /* ──────────────────── Styles ──────────────────── */ @@ -51,15 +52,28 @@ function MicrosoftIcon() { /* ──────────────────── Sign-in modal ──────────────────── */ function SignInModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) { - const { signInWithGoogle, signInWithMicrosoft } = useAuthStore(); + const { signInWithGoogle, signInWithMicrosoft, signInWithEmail, signUpWithEmail } = useAuthStore(); const [turnstileToken, setTurnstileToken] = useState(null); const [verifying, setVerifying] = useState(false); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [emailError, setEmailError] = useState(""); + const [emailLoading, setEmailLoading] = useState(false); + const [isSignUp, setIsSignUp] = useState(false); + const showEmailAuth = ENABLE_EMAIL_AUTH; const hasTurnstile = !!TURNSTILE_SITE_KEY; - // Reset token when modal closes + // Reset state when modal closes useEffect(() => { - if (!isOpen) setTurnstileToken(null); + if (!isOpen) { + setTurnstileToken(null); + setEmail(""); + setPassword(""); + setEmailError(""); + setEmailLoading(false); + setIsSignUp(false); + } }, [isOpen]); useEffect(() => { @@ -98,6 +112,7 @@ function SignInModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }, [hasTurnstile, turnstileToken, signInWithGoogle, signInWithMicrosoft]); const buttonsDisabled = hasTurnstile && (!turnstileToken || verifying); + const emailSubmitLabel = isSignUp ? "Sign up with Email" : "Sign in with Email"; return createPortal( @@ -144,6 +159,66 @@ function SignInModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void Continue with Microsoft + {showEmailAuth && ( + <> +
+
+
or
+
+
{ + e.preventDefault(); + setEmailError(""); + setEmailLoading(true); + try { + if (isSignUp) { + await signUpWithEmail(email, password); + } else { + await signInWithEmail(email, password); + } + } catch (err: unknown) { + setEmailError(err instanceof Error ? err.message : "Auth failed"); + } finally { + setEmailLoading(false); + } + }} + className="space-y-2" + > + setEmail(e.target.value)} + required + className="w-full px-4 py-3 border border-black/20 rounded-xl text-base focus:outline-none focus:ring-2 focus:ring-black/20" + /> + setPassword(e.target.value)} + required + minLength={6} + className="w-full px-4 py-3 border border-black/20 rounded-xl text-base focus:outline-none focus:ring-2 focus:ring-black/20" + /> + {emailError &&

{emailError}

} + + +
+ + )}
{hasTurnstile && (