// Sherpas Labs — Login page (production)
// Google OAuth → /auth/login
// Partner email+password → /auth/partner/login
// Access request → POST /api/access-request
const LoginPage = () => {
const [requesting, setRequesting] = React.useState(false);
const [partnerEmail, setPartnerEmail] = React.useState("");
const [partnerPassword, setPartnerPassword] = React.useState("");
const [partnerLoading, setPartnerLoading] = React.useState(false);
const [partnerError, setPartnerError] = React.useState("");
const [changePasswordUser, setChangePasswordUser] = React.useState(null);
const params = new URLSearchParams(window.location.search);
const error = params.get("error");
const errorMessages = {
oauth_denied: "Sign-in was cancelled.",
invalid_callback: "Invalid OAuth callback. Please try again.",
invalid_state: "Session expired. Please try again.",
token_exchange: "Could not complete sign-in. Please try again.",
userinfo: "Could not retrieve your profile. Please try again.",
pending_approval: "Your request is pending review. We'll be in touch.",
access_denied: "Access denied.",
session_expired: "Your session expired. Please sign in again.",
};
const errorMsg = error ? (errorMessages[error] || "An error occurred. Please try again.") : null;
const onGoogle = () => { window.location.href = "/auth/login"; };
const onPartnerSubmit = async (e) => {
e.preventDefault();
if (!partnerEmail || !partnerPassword) return;
setPartnerLoading(true); setPartnerError("");
try {
const res = await fetch("/auth/partner/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: partnerEmail, password: partnerPassword }),
credentials: "include",
});
const data = await res.json();
if (!res.ok) { setPartnerError(data.message || "Incorrect email or password."); return; }
document.cookie = `sherpas_labs_session=${data.session_token}; path=/; SameSite=Lax`;
if (data.must_change_password) {
setChangePasswordUser({ email: partnerEmail, tempPassword: partnerPassword });
} else {
window.location.href = "/index";
}
} catch (err) {
setPartnerError("Something went wrong. Please try again.");
} finally {
setPartnerLoading(false);
}
};
// Responsive: detect mobile
const isMobile = window.innerWidth < 768;
return (
{/* LEFT — dark manifesto (hidden on mobile) */}
{!isMobile && (
)}
{/* RIGHT — form */}
{/* Mobile header */}
{isMobile && (
Labs
)}
{!isMobile && (
)}
Sign in
Open the file room.
Sherpas team uses Google. Design partners sign in with the credentials we issued.
{errorMsg && (
{errorMsg}
)}
{/* Google button */}
Sherpas team · @sherpaswealth.com only
{/* Divider */}
{/* Partner form */}
{/* Request access */}
Not on the list?
Request access — we read every one.
By signing in you agree to Sherpas Labs access terms.
{requesting &&
setRequesting(false)} />}
{changePasswordUser && { window.location.href = "/index"; }} />}
);
};
// --- Google G glyph ---
const GoogleGlyph = () => (
React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24" },
React.createElement("path", { fill: "#4285F4", d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" }),
React.createElement("path", { fill: "#34A853", d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" }),
React.createElement("path", { fill: "#FBBC05", d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" }),
React.createElement("path", { fill: "#EA4335", d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" })
)
);
// --- Underline field ---
const Field = ({ label, type, value, onChange, placeholder, autoComplete }) => {
const [focus, setFocus] = React.useState(false);
return (
onChange(e.target.value)}
placeholder={placeholder} autoComplete={autoComplete}
onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
style={{
width: "100%", border: "none", outline: "none",
borderBottom: `1px solid ${focus ? "var(--ink)" : "var(--line-strong)"}`,
padding: "10px 0", fontFamily: "var(--font-mono)", fontSize: 14, color: "var(--ink)",
background: "transparent", transition: "border-color 0.15s", boxSizing: "border-box",
}}
/>
);
};
// --- Change password modal ---
const ChangePasswordModal = ({ user, onDone }) => {
const [newPw, setNewPw] = React.useState("");
const [confirmPw, setConfirmPw] = React.useState("");
const [loading, setLoading] = React.useState(false);
const [err, setErr] = React.useState("");
const onSubmit = async (e) => {
e.preventDefault();
if (newPw.length < 8) { setErr("Password must be at least 8 characters."); return; }
if (newPw !== confirmPw) { setErr("Passwords don't match."); return; }
setLoading(true); setErr("");
try {
const res = await fetch("/auth/partner/change-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: user.email, current_password: user.tempPassword, new_password: newPw }),
credentials: "include",
});
const data = await res.json();
if (!res.ok) { setErr(data.detail || "Something went wrong."); return; }
document.cookie = `sherpas_labs_session=${data.session_token}; path=/; SameSite=Lax`;
onDone();
} catch (e) {
setErr("Something went wrong. Please try again.");
} finally {
setLoading(false);
}
};
return (
● First login
Set your password.
Choose a password you'll use going forward. At least 8 characters.
{err &&
{err}
}
);
};
// --- Request access modal ---
const RequestAccessModal = ({ onClose }) => {
const [sent, setSent] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const [form, setForm] = React.useState({ name: "", company: "", email: "", role: "", reason: "" });
const [err, setErr] = React.useState("");
const set = (k) => (v) => setForm((f) => ({ ...f, [k]: v }));
const onSubmit = async (e) => {
e.preventDefault();
if (!form.name || !form.email) { setErr("Name and email are required."); return; }
setLoading(true); setErr("");
try {
const res = await fetch("/api/access-request", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: form.name, email: form.email, company: form.company, role: form.role, reason: form.reason }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || "Request failed");
setSent(true);
} catch (e) {
setErr(e.message || "Something went wrong.");
} finally { setLoading(false); }
};
return (
e.stopPropagation()} style={{ background: "var(--bg)", maxWidth: 500, width: "100%", padding: 36, border: "1px solid var(--line-strong)", position: "relative" }}>
{!sent ? (
<>
Request access
Tell us who you are.
We read every request carefully, one at a time.
{err &&
{err}
}
>
) : (
● Filed
Request received.
Someone from the team will read it and reply personally.
)}
);
};