// Self-contained Better Auth sign-in page for Cereal Milk.
// Served by the Better Auth server (same domain, so cookies work).
// The desktop app opens it in the SYSTEM browser with
// ?callback=http://127.0.0.1:/complete
// On success the page POSTs the session token + identity to that localhost
// callback. No secret key, no backend — the token is an identity marker for
// the app's sign-in gate.
//
// Extra params from the app's native buttons:
// strategy=oauth_google go straight to Google
// strategy=email show the email form directly
// switch=1 sign out of the browser session first, so
// "Sign out" in the app re-arms the gate
import { createAuthClient } from "better-auth/client";
const AUTH_URL = window.__BA_URL || window.location.origin;
const authClient = createAuthClient({
baseURL: AUTH_URL,
});
const msg = document.getElementById("msg");
const btnGoogle = document.getElementById("btn-google");
const btnEmail = document.getElementById("btn-email");
const emailForm = document.getElementById("email-form");
const emailInp = document.getElementById("email-inp");
const passInp = document.getElementById("pass-inp");
const authZone = document.getElementById("auth-zone");
function setMsg(text, isError) {
if (!msg) return;
msg.textContent = text;
msg.className = isError ? "error" : "";
msg.style.display = "block";
}
// Where to hand the token back. A ?callback is honored only if it points at
// localhost (prevents this page being abused to exfiltrate a token elsewhere).
function resolveCallback() {
const raw = new URLSearchParams(location.search).get("callback");
if (!raw) return "/complete";
try {
const u = new URL(raw);
if (u.hostname === "127.0.0.1" || u.hostname === "localhost") return u.href;
} catch (_e) { /* fall through */ }
return "/complete";
}
const callbackUrl = resolveCallback();
const qs = new URLSearchParams(location.search);
const strategy = qs.get("strategy");
const wantsSwitch = qs.get("switch") === "1";
// The URL to return to after the OAuth provider redirects back.
function returnUrl() {
const u = new URL(location.href);
u.searchParams.delete("strategy");
u.searchParams.delete("switch");
// Keep the callback param
return u.href;
}
function showLoading(v) {
btnGoogle.disabled = v;
btnEmail.disabled = v;
}
let done = false;
async function complete(session, user) {
if (done) return;
done = true;
const token = session ? session.token : null;
const email = user ? user.email : null;
const name = user ? user.name : null;
hideForm();
setMsg("Signed in. Returning to the app…", false);
try {
await fetch(callbackUrl, {
method: "POST",
headers: { "Content-Type": "text/plain" },
body: JSON.stringify({
token: token,
userId: user ? user.id : null,
email: email,
name: name,
}),
});
setMsg("Signed in. You can close this tab — Cereal Milk is ready.", false);
} catch (_e) {
setMsg(
"Signed in, but couldn't reach the app. Make sure Cereal Milk is open, then try again.",
true
);
}
}
function hideForm() {
emailForm.style.display = "none";
btnGoogle.style.display = "";
btnEmail.style.display = "";
}
function showEmailForm() {
btnGoogle.style.display = "none";
btnEmail.style.display = "none";
emailForm.style.display = "flex";
emailInp.focus();
}
// Sign in with email + password (falls back to sign-up if account doesn't exist)
async function signInEmail() {
const email = emailInp.value.trim();
const password = passInp.value;
const name = email.split("@")[0];
if (!email || !password) {
setMsg("Enter your email and password.", true);
return;
}
setMsg("Signing in…", false);
showLoading(true);
// Try sign-in first
let { data, error } = await authClient.signIn.email({ email, password });
// If sign-in fails, try sign-up
if (error) {
const signUpRes = await authClient.signUp.email({
email, password, name,
});
if (signUpRes.error) {
setMsg(signUpRes.error.message || "Sign-in failed.", true);
showLoading(false);
return;
}
// Sign-up succeeded (autoSignIn creates a session)
}
const sessionRes = await authClient.getSession();
if (sessionRes.data) {
await complete(sessionRes.data.session, sessionRes.data.user);
}
}
// Sign in with Google OAuth
async function signInGoogle() {
setMsg("Sending you to Google…", false);
showLoading(true);
try {
await authClient.signIn.social({
provider: "google",
callbackURL: returnUrl(),
});
} catch (_e) {
setMsg("Google sign-in couldn't start — try again or use email.", true);
showLoading(false);
}
}
// Check if we returned from Google OAuth
async function checkReturnFromOAuth() {
// Better Auth handles OAuth callbacks server-side, then redirects back
// to this page. The session should be available.
try {
const { data: session, error } = await authClient.getSession();
if (error) return;
if (session && session.session && session.user) {
await complete(session.session, session.user);
return true;
}
} catch (_e) {
/* not signed in yet */
}
return false;
}
// ---------- init ----------
btnGoogle.addEventListener("click", signInGoogle);
btnEmail.addEventListener("click", showEmailForm);
document.getElementById("email-back").addEventListener("click", hideForm);
document.getElementById("email-submit").addEventListener("click", signInEmail);
passInp.addEventListener("keydown", (e) => {
if (e.key === "Enter") signInEmail();
});
// If the user already has a session (returning from OAuth or previous sign-in),
// complete immediately.
(async function boot() {
// Handle sign-out switch: if the app requests a fresh sign-in, sign out the
// existing browser session first.
if (wantsSwitch) {
try {
const { data: session } = await authClient.getSession();
if (session) {
setMsg("Signing out previous session…", false);
await authClient.signOut();
}
} catch (_e) {
/* proceed to a fresh sign-in */
}
}
// Check for an existing session (e.g., returning from Google OAuth)
const returned = await checkReturnFromOAuth();
if (returned) return;
// Auto-trigger strategy if requested
if (strategy === "oauth_google") {
signInGoogle();
return;
}
if (strategy === "email") {
showEmailForm();
return;
}
// Session check: if already signed in and no switch requested, complete
if (!wantsSwitch) {
const returned2 = await checkReturnFromOAuth();
if (returned2) return;
}
})();