Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions core-web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ------------------------------------------------------------------------------
Expand Down
81 changes: 78 additions & 3 deletions core-web/src/components/Landing/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ──────────────────── */

Expand Down Expand Up @@ -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<string | null>(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;

Comment thread
coderabbitai[bot] marked this conversation as resolved.
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(() => {
Expand Down Expand Up @@ -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(
<AnimatePresence>
Expand Down Expand Up @@ -144,6 +159,66 @@ function SignInModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void
<MicrosoftIcon />
Continue with Microsoft
</button>
{showEmailAuth && (
<>
<div className="relative my-2">
<div className="absolute inset-0 flex items-center"><div className="w-full border-t border-black/10" /></div>
<div className="relative flex justify-center text-xs"><span className="bg-white px-2 text-text-tertiary">or</span></div>
</div>
<form
onSubmit={async (e) => {
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"
>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => 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"
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => 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 && <p className="text-red-500 text-xs">{emailError}</p>}
<button
type="submit"
disabled={emailLoading}
className="w-full px-4 py-3 bg-text-body text-white rounded-xl text-base font-medium hover:bg-black/80 transition-all disabled:opacity-40"
>
{emailLoading ? "..." : emailSubmitLabel}
</button>
<button
type="button"
onClick={() => { setIsSignUp(!isSignUp); setEmailError(""); }}
className="w-full text-xs text-text-tertiary hover:text-text-body transition-colors"
>
{isSignUp ? "Already have an account? Sign in" : "No account? Sign up"}
</button>
</form>
</>
)}
</div>
{hasTurnstile && (
<div className="mt-4 flex justify-center">
Expand Down
Loading