import { createSignal, createEffect, onCleanup, Show } from "solid-js"; import { A, useNavigate, useSearchParams, redirect, query } from "@solidjs/router"; import { Title, Meta } from "@solidjs/meta"; import { getEvent } from "vinxi/http"; import GoogleLogo from "~/components/icons/GoogleLogo"; import GitHub from "~/components/icons/GitHub"; import Eye from "~/components/icons/Eye"; import EyeSlash from "~/components/icons/EyeSlash"; import CountdownCircleTimer from "~/components/CountdownCircleTimer"; import { isValidEmail, validatePassword } from "~/lib/validation"; import { getClientCookie } from "~/lib/cookies.client"; import { env } from "~/env/client"; const checkAuth = query(async () => { "use server"; const { checkAuthStatus } = await import("~/server/utils"); const event = getEvent()!; const { isAuthenticated } = await checkAuthStatus(event); if (isAuthenticated) { throw redirect("/account"); } return { isAuthenticated }; }, "loginAuthCheck"); export const route = { load: () => checkAuth() }; export default function LoginPage() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); // State management const [register, setRegister] = createSignal(false); const [error, setError] = createSignal(""); const [loading, setLoading] = createSignal(false); const [usePassword, setUsePassword] = createSignal(false); const [countDown, setCountDown] = createSignal(0); const [emailSent, setEmailSent] = createSignal(false); const [showPasswordError, setShowPasswordError] = createSignal(false); const [showPasswordSuccess, setShowPasswordSuccess] = createSignal(false); const [showPasswordInput, setShowPasswordInput] = createSignal(false); const [showPasswordConfInput, setShowPasswordConfInput] = createSignal(false); const [passwordsMatch, setPasswordsMatch] = createSignal(false); const [showPasswordLengthWarning, setShowPasswordLengthWarning] = createSignal(false); const [passwordLengthSufficient, setPasswordLengthSufficient] = createSignal(false); const [passwordBlurred, setPasswordBlurred] = createSignal(false); // Form refs let emailRef: HTMLInputElement | undefined; let passwordRef: HTMLInputElement | undefined; let passwordConfRef: HTMLInputElement | undefined; let rememberMeRef: HTMLInputElement | undefined; let timerInterval: number | undefined; // Environment variables const googleClientId = env.VITE_GOOGLE_CLIENT_ID; const githubClientId = env.VITE_GITHUB_CLIENT_ID; const domain = env.VITE_DOMAIN || "https://www.freno.me"; // Calculate remaining time from cookie const calcRemainder = (timer: string) => { const expires = new Date(timer); const remaining = expires.getTime() - Date.now(); const remainingInSeconds = remaining / 1000; if (remainingInSeconds <= 0) { setCountDown(0); if (timerInterval) { clearInterval(timerInterval); } } else { setCountDown(remainingInSeconds); } }; // Check for existing timer on mount createEffect(() => { const timer = getClientCookie("emailLoginLinkRequested"); if (timer) { timerInterval = setInterval( () => calcRemainder(timer), 1000 ) as unknown as number; } onCleanup(() => { if (timerInterval) { clearInterval(timerInterval); } }); }); // Check for OAuth/callback errors in URL createEffect(() => { const errorParam = searchParams.error; if (errorParam) { const errorMessages: Record = { missing_code: "OAuth authorization failed - missing code", auth_failed: "Authentication failed - please try again", server_error: "Server error - please try again later", missing_params: "Invalid login link - missing parameters", link_expired: "Login link has expired - please request a new one", access_denied: "Access denied - you cancelled the login", email_in_use: "This email is already associated with another account. Please sign in with that account instead." }; setError(errorMessages[errorParam] || "An error occurred during login"); } }); // Form submission handler const formHandler = async (e: Event) => { e.preventDefault(); setLoading(true); setError(""); setShowPasswordError(false); setShowPasswordSuccess(false); try { if (register()) { // Registration flow if (!emailRef || !passwordRef || !passwordConfRef) { setError("Please fill in all fields"); setLoading(false); return; } const email = emailRef.value; const password = passwordRef.value; const passwordConf = passwordConfRef.value; // Validate inputs if (!isValidEmail(email)) { setError("Invalid email address"); setLoading(false); return; } const passwordValidation = validatePassword(password); if (!passwordValidation.isValid) { setError(passwordValidation.errors[0] || "Invalid password"); setLoading(false); return; } if (password !== passwordConf) { setError("passwordMismatch"); setLoading(false); return; } // Call registration endpoint const response = await fetch("/api/trpc/auth.emailRegistration", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password, passwordConfirmation: passwordConf }) }); const result = await response.json(); if (response.ok && result.result?.data) { navigate("/account"); } else { const errorMsg = result.error?.message || result.result?.data?.message || "Registration failed"; if ( errorMsg.includes("duplicate") || errorMsg.includes("already exists") ) { setError("duplicate"); } else { setError(errorMsg); } } } else if (usePassword()) { // Password login flow if (!emailRef || !passwordRef || !rememberMeRef) { setError("Please fill in all fields"); setLoading(false); return; } const email = emailRef.value; const password = passwordRef.value; const rememberMe = rememberMeRef.checked; const response = await fetch("/api/trpc/auth.emailPasswordLogin", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password, rememberMe }) }); const result = await response.json(); if (response.ok && result.result?.data?.success) { setShowPasswordSuccess(true); setTimeout(() => { navigate(-1); // Go back window.location.reload(); // Refresh to update session }, 500); } else { setShowPasswordError(true); } } else { // Email link login flow if (!emailRef || !rememberMeRef) { setError("Please enter your email"); setLoading(false); return; } const email = emailRef.value; const rememberMe = rememberMeRef.checked; if (!isValidEmail(email)) { setError("Invalid email address"); setLoading(false); return; } const response = await fetch("/api/trpc/auth.requestEmailLinkLogin", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, rememberMe }) }); const result = await response.json(); if (response.ok && result.result?.data?.success) { setEmailSent(true); const timer = getClientCookie("emailLoginLinkRequested"); if (timer) { if (timerInterval) { clearInterval(timerInterval); } timerInterval = setInterval( () => calcRemainder(timer), 1000 ) as unknown as number; } } else { const errorMsg = result.error?.message || result.result?.data?.message || "Failed to send email"; setError(errorMsg); } } } catch (err: any) { console.error("Login error:", err); setError(err.message || "An error occurred"); } finally { setLoading(false); } }; // Countdown timer render function const renderTime = () => { return (
{countDown().toFixed(0)}
); }; // Password validation helpers const checkForMatch = (newPassword: string, newPasswordConf: string) => { setPasswordsMatch(newPassword === newPasswordConf); }; const checkPasswordLength = (password: string) => { if (password.length >= 8) { setPasswordLengthSufficient(true); setShowPasswordLengthWarning(false); } else { setPasswordLengthSufficient(false); if (passwordBlurred()) { setShowPasswordLengthWarning(true); } } }; const passwordLengthBlurCheck = () => { if ( !passwordLengthSufficient() && passwordRef && passwordRef.value !== "" ) { setShowPasswordLengthWarning(true); } setPasswordBlurred(true); }; const handleNewPasswordChange = (e: Event) => { const target = e.target as HTMLInputElement; checkPasswordLength(target.value); }; const handlePasswordConfChange = (e: Event) => { const target = e.target as HTMLInputElement; if (passwordRef) { checkForMatch(passwordRef.value, target.value); } }; const handlePasswordBlur = () => { passwordLengthBlurCheck(); }; return ( <> Login | Michael Freno
{/* Main content */}
{/* Error message */}
Passwords did not match!
Email Already Exists!
{error()}
{/* Title */}
{register() ? "Register" : "Login"}
{/* Toggle Register/Login */} Already have an account?
} >
Don't have an account yet?
{/* Form */}
{/* Email input */}
{/* Password input - shown for login with password or registration */}
Password too short! Min Length: 8
{/* Password confirmation - shown only for registration */}
= 6 ? "" : "opacity-0 select-none" } text-center text-red-500 transition-opacity duration-200 ease-in-out`} > Passwords do not match!
{/* Remember Me checkbox */}
Remember Me
{/* Error/Success messages */}
Credentials did not match any record Login Success! Redirecting...
{/* Submit button or countdown timer */}
0} fallback={ } > {renderTime} {/* Toggle password/email link */}
{/* Password reset link */}
Trouble Logging In?{" "} Reset Password
{/* Email sent confirmation */}
Email Sent!
{/* Or divider */}
Or
{/* OAuth buttons */}
); }