import { createSignal, Show, createEffect, For } from "solid-js"; import { PageHead } from "~/components/PageHead"; import { useNavigate, redirect, query, createAsync } from "@solidjs/router"; import XCircle from "~/components/icons/XCircle"; import GoogleLogo from "~/components/icons/GoogleLogo"; import GitHub from "~/components/icons/GitHub"; import EmailIcon from "~/components/icons/EmailIcon"; import Dropzone from "~/components/blog/Dropzone"; import AddImageToS3 from "~/lib/s3upload"; import { validatePassword, isValidEmail } from "~/lib/validation"; import { TerminalSplash } from "~/components/TerminalSplash"; import { VALIDATION_CONFIG } from "~/config"; import { api } from "~/lib/api"; import Input from "~/components/ui/Input"; import PasswordInput from "~/components/ui/PasswordInput"; import Button from "~/components/ui/Button"; import FormFeedback from "~/components/ui/FormFeedback"; import type { UserProfile } from "~/types/user"; import { useAuth } from "~/context/auth"; const getUserProfile = query(async (): Promise => { "use server"; const { getUserState } = await import("~/lib/auth-query"); const { ConnectionFactory } = await import("~/server/utils"); const userState = await getUserState(); if (!userState.isAuthenticated || !userState.userId) { throw redirect("/login"); } const conn = ConnectionFactory(); try { const res = await conn.execute({ sql: "SELECT provider, image, password_hash FROM User WHERE id = ?", args: [userState.userId] }); if (res.rows.length === 0) { throw redirect("/login"); } const user = res.rows[0] as any; return { id: userState.userId, email: userState.email ?? undefined, emailVerified: userState.emailVerified, displayName: userState.displayName ?? undefined, provider: user.provider ?? undefined, image: user.image ?? undefined, hasPassword: !!user.password_hash }; } catch (err) { console.error("Failed to fetch user profile:", err); throw redirect("/login"); } }, "accountUserProfile"); export const route = { load: () => getUserProfile() }; export default function AccountPage() { const navigate = useNavigate(); const { refreshAuth } = useAuth(); const userData = createAsync(() => getUserProfile(), { deferStream: true }); const [user, setUser] = createSignal(null); const [emailButtonLoading, setEmailButtonLoading] = createSignal(false); const [displayNameButtonLoading, setDisplayNameButtonLoading] = createSignal(false); const [passwordChangeLoading, setPasswordChangeLoading] = createSignal(false); const [deleteAccountButtonLoading, setDeleteAccountButtonLoading] = createSignal(false); const [profileImageSetLoading, setProfileImageSetLoading] = createSignal(false); const [signOutLoading, setSignOutLoading] = createSignal(false); const [passwordsMatch, setPasswordsMatch] = createSignal(false); const [showPasswordLengthWarning, setShowPasswordLengthWarning] = createSignal(false); const [passwordLengthSufficient, setPasswordLengthSufficient] = createSignal(false); const [passwordBlurred, setPasswordBlurred] = createSignal(false); const [passwordError, setPasswordError] = createSignal(false); const [passwordDeletionError, setPasswordDeletionError] = createSignal(false); const [newPassword, setNewPassword] = createSignal(""); const [showImageSuccess, setShowImageSuccess] = createSignal(false); const [showEmailSuccess, setShowEmailSuccess] = createSignal(false); const [showDisplayNameSuccess, setShowDisplayNameSuccess] = createSignal(false); const [showPasswordSuccess, setShowPasswordSuccess] = createSignal(false); const [profileImage, setProfileImage] = createSignal( undefined ); const [profileImageHolder, setProfileImageHolder] = createSignal< string | null >(null); const [profileImageStateChange, setProfileImageStateChange] = createSignal(false); const [preSetHolder, setPreSetHolder] = createSignal(null); let oldPasswordRef: HTMLInputElement | undefined; let newPasswordRef: HTMLInputElement | undefined; let newPasswordConfRef: HTMLInputElement | undefined; let emailRef: HTMLInputElement | undefined; let displayNameRef: HTMLInputElement | undefined; let deleteAccountPasswordRef: HTMLInputElement | undefined; const currentUser = () => user() || userData(); createEffect(() => { const userProfile = userData(); if (userProfile?.image && !preSetHolder()) { setPreSetHolder(userProfile.image); } }); const handleImageDrop = (acceptedFiles: File[]) => { acceptedFiles.forEach((file: File) => { setProfileImage(file); const reader = new FileReader(); reader.onload = () => { const str = reader.result as string; setProfileImageHolder(str); setProfileImageStateChange(true); }; reader.readAsDataURL(file); }); }; const removeImage = () => { setProfileImage(undefined); setProfileImageHolder(null); if (preSetHolder()) { setProfileImageStateChange(true); setPreSetHolder(null); } else { setProfileImageStateChange(false); } }; const setUserImage = async (e: Event) => { e.preventDefault(); setProfileImageSetLoading(true); setShowImageSuccess(false); const userProfile = currentUser(); if (!userProfile) { setProfileImageSetLoading(false); return; } try { let imageUrl = ""; if (profileImage()) { const imageKey = await AddImageToS3( profileImage()!, userProfile.id, "user" ); imageUrl = imageKey || ""; } const response = await fetch("/api/trpc/user.updateProfileImage", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ imageUrl }) }); const result = await response.json(); if (response.ok && result.result?.data) { setUser(result.result.data); setShowImageSuccess(true); setProfileImageStateChange(false); setTimeout(() => setShowImageSuccess(false), 3000); setPreSetHolder(imageUrl || null); } else { alert("Error updating profile image!"); } } catch (err) { console.error("Profile image update error:", err); alert("Error updating profile image! Check console."); } finally { setProfileImageSetLoading(false); } }; const setEmailTrigger = async (e: Event) => { e.preventDefault(); if (!emailRef) return; const email = emailRef.value; if (!isValidEmail(email)) { alert("Invalid email address"); return; } setEmailButtonLoading(true); setShowEmailSuccess(false); try { const response = await fetch("/api/trpc/user.updateEmail", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email }) }); const result = await response.json(); if (response.ok && result.result?.data) { setUser(result.result.data); setShowEmailSuccess(true); setTimeout(() => setShowEmailSuccess(false), 3000); } } catch (err) { console.error("Email update error:", err); } finally { setEmailButtonLoading(false); } }; const setDisplayNameTrigger = async (e: Event) => { e.preventDefault(); if (!displayNameRef) return; const displayName = displayNameRef.value; setDisplayNameButtonLoading(true); setShowDisplayNameSuccess(false); try { const response = await fetch("/api/trpc/user.updateDisplayName", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ displayName }) }); const result = await response.json(); if (response.ok && result.result?.data) { setUser(result.result.data); setShowDisplayNameSuccess(true); setTimeout(() => setShowDisplayNameSuccess(false), 3000); } } catch (err) { console.error("Display name update error:", err); } finally { setDisplayNameButtonLoading(false); } }; const handlePasswordSubmit = async (e: Event) => { e.preventDefault(); const userProfile = currentUser(); if (!userProfile) return; if (userProfile.hasPassword) { if (!oldPasswordRef || !newPasswordRef || !newPasswordConfRef) return; const oldPassword = oldPasswordRef.value; const newPassword = newPasswordRef.value; const newPasswordConf = newPasswordConfRef.value; const passwordValidation = validatePassword(newPassword); if (!passwordValidation.isValid) { setPasswordError(true); return; } if (newPassword !== newPasswordConf) { setPasswordError(true); return; } setPasswordChangeLoading(true); setPasswordError(false); try { const response = await fetch("/api/trpc/user.changePassword", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ oldPassword, newPassword, newPasswordConfirmation: newPasswordConf }) }); const result = await response.json(); if (response.ok && result.result?.data?.success) { setShowPasswordSuccess(true); setTimeout(() => setShowPasswordSuccess(false), 3000); if (oldPasswordRef) oldPasswordRef.value = ""; if (newPasswordRef) newPasswordRef.value = ""; if (newPasswordConfRef) newPasswordConfRef.value = ""; } else { setPasswordError(true); } } catch (err) { console.error("Password change error:", err); setPasswordError(true); } finally { setPasswordChangeLoading(false); } } else { if (!newPasswordRef || !newPasswordConfRef) return; const newPassword = newPasswordRef.value; const newPasswordConf = newPasswordConfRef.value; const passwordValidation = validatePassword(newPassword); if (!passwordValidation.isValid) { setPasswordError(true); return; } if (newPassword !== newPasswordConf) { setPasswordError(true); return; } setPasswordChangeLoading(true); setPasswordError(false); try { const response = await fetch("/api/trpc/user.setPassword", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ newPassword, newPasswordConfirmation: newPasswordConf }) }); const result = await response.json(); if (response.ok && result.result?.data?.success) { const profileResponse = await fetch("/api/trpc/user.getProfile"); const profileResult = await profileResponse.json(); if (profileResult.result?.data) { setUser(profileResult.result.data); } setShowPasswordSuccess(true); setTimeout(() => setShowPasswordSuccess(false), 3000); if (newPasswordRef) newPasswordRef.value = ""; if (newPasswordConfRef) newPasswordConfRef.value = ""; } else { setPasswordError(true); } } catch (err) { console.error("Password set error:", err); setPasswordError(true); } finally { setPasswordChangeLoading(false); } } }; const deleteAccountTrigger = async (e: Event) => { e.preventDefault(); if (!deleteAccountPasswordRef) return; const password = deleteAccountPasswordRef.value; setDeleteAccountButtonLoading(true); setPasswordDeletionError(false); try { const response = await fetch("/api/trpc/user.deleteAccount", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password }) }); const result = await response.json(); if (response.ok && result.result?.data?.success) { navigate("/login"); } else { setPasswordDeletionError(true); } } catch (err) { console.error("Delete account error:", err); setPasswordDeletionError(true); } finally { setDeleteAccountButtonLoading(false); } }; const sendEmailVerification = async () => { const userProfile = currentUser(); if (!userProfile?.email) return; try { await fetch("/api/trpc/auth.resendEmailVerification", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: userProfile.email }) }); alert("Verification email sent!"); } catch (err) { console.error("Email verification error:", err); } }; const checkPasswordLength = (password: string) => { if (password.length >= 8) { setPasswordLengthSufficient(true); setShowPasswordLengthWarning(false); } else { setPasswordLengthSufficient(false); if (passwordBlurred()) { setShowPasswordLengthWarning(true); } } }; const checkForMatch = (newPassword: string, newPasswordConf: string) => { setPasswordsMatch(newPassword === newPasswordConf); }; const handleNewPasswordChange = (e: Event) => { const target = e.target as HTMLInputElement; setNewPassword(target.value); checkPasswordLength(target.value); if (newPasswordConfRef) { checkForMatch(target.value, newPasswordConfRef.value); } }; const handlePasswordConfChange = (e: Event) => { const target = e.target as HTMLInputElement; if (newPasswordRef) { checkForMatch(newPasswordRef.value, target.value); } }; const handlePasswordBlur = () => { if ( !passwordLengthSufficient() && newPasswordRef && newPasswordRef.value !== "" ) { setShowPasswordLengthWarning(true); } setPasswordBlurred(true); }; const handleSignOut = async () => { setSignOutLoading(true); try { await api.auth.signOut.mutate(); refreshAuth(); navigate("/"); } catch (error) { console.error("Sign out failed:", error); setSignOutLoading(false); } }; const getProviderName = (provider: UserProfile["provider"]) => { switch (provider) { case "google": return "Google"; case "github": return "GitHub"; case "email": return "Email"; default: return "Unknown"; } }; const getProviderColor = (provider: UserProfile["provider"]) => { switch (provider) { case "google": return "text-blue-500"; case "github": return "text-gray-700 dark:text-gray-300"; case "email": return "text-green-500"; default: return "text-gray-500"; } }; return ( <>
}> {(userProfile) => ( <>
Account Settings


{/* Email Section */}
(emailRef = el)} emailButtonLoading={emailButtonLoading} setEmailTrigger={setEmailTrigger} showEmailSuccess={showEmailSuccess} sendEmailVerification={sendEmailVerification} /> {/* Display Name Section */} (displayNameRef = el)} displayNameButtonLoading={displayNameButtonLoading} setDisplayNameTrigger={setDisplayNameTrigger} showDisplayNameSuccess={showDisplayNameSuccess} />
(oldPasswordRef = el)} newPasswordRef={(el) => (newPasswordRef = el)} newPasswordConfRef={(el) => el !== undefined ? (newPasswordConfRef = el) : newPasswordConfRef } handleNewPasswordChange={handleNewPasswordChange} handlePasswordConfChange={handlePasswordConfChange} handlePasswordBlur={handlePasswordBlur} sendEmailVerification={sendEmailVerification} getProviderName={getProviderName} passwordChangeLoading={passwordChangeLoading} newPassword={newPassword} passwordsMatch={passwordsMatch} passwordLengthSufficient={passwordLengthSufficient} passwordError={passwordError} showPasswordSuccess={showPasswordSuccess} />


(deleteAccountPasswordRef = el) } deleteAccountButtonLoading={deleteAccountButtonLoading} deleteAccountTrigger={deleteAccountTrigger} passwordDeletionError={passwordDeletionError} /> )}
); } function AccountTypeSection(props: { profile: () => UserProfile; getProviderColor: (provider: UserProfile["provider"]) => string; getProviderName: (provider: UserProfile["provider"]) => string; }) { const { profile, getProviderColor, getProviderName } = props; return (
Account Type
{getProviderName(profile().provider)} Account
⚠️ Add an email address for account recovery
{!profile().email ? "💡 Add and verify an email to enable email/password login" : !profile().emailVerified ? "💡 Verify your email to enable password setup" : "💡 Add a password to enable email/password login"}
); } function ProfileImageSection(props: { profile: () => UserProfile; handleImageDrop: (acceptedFiles: File[]) => void; profileImageHolder: () => string | null; preSetHolder: () => string | null; removeImage: () => void; setUserImage: (e: Event) => void; profileImageSetLoading: () => boolean; profileImageStateChange: () => boolean; showImageSuccess: () => boolean; }) { const { profile, handleImageDrop, profileImageHolder, preSetHolder, removeImage, setUserImage, profileImageSetLoading, profileImageStateChange, showImageSuccess } = props; return (
Profile Image
); } function EmailSection(props: { profile: () => UserProfile; emailRef: (el: HTMLInputElement) => void; emailButtonLoading: () => boolean; setEmailTrigger: (e: Event) => void; showEmailSuccess: () => boolean; sendEmailVerification: () => void; }) { const { profile, emailRef, emailButtonLoading, setEmailTrigger, showEmailSuccess, sendEmailVerification } = props; return ( <>
{profile().provider === "email" ? "Email:" : "Linked Email:"}
{profile().email ? ( {profile().email} ) : ( {profile().provider === "email" ? "None Set" : "Not Linked"} )}
Add an email for account recovery and notifications
); } function DisplayNameSection(props: { profile: () => UserProfile; displayNameRef: (el: HTMLInputElement) => void; displayNameButtonLoading: () => boolean; setDisplayNameTrigger: (e: Event) => void; showDisplayNameSuccess: () => boolean; }) { const { profile, displayNameRef, displayNameButtonLoading, setDisplayNameTrigger, showDisplayNameSuccess } = props; return ( <>
Display Name:
{profile().displayName ? ( {profile().displayName} ) : ( None Set )}
); } function PasswordSection(props: { profile: () => UserProfile; handlePasswordSubmit: (e: Event) => void; oldPasswordRef: (el: HTMLInputElement) => void; newPasswordRef: (el: HTMLInputElement) => void; newPasswordConfRef: ( el?: HTMLInputElement ) => HTMLInputElement | undefined; handleNewPasswordChange: (e: Event) => void; handlePasswordConfChange: (e: Event) => void; handlePasswordBlur: () => void; sendEmailVerification: () => void; getProviderName: (provider: UserProfile["provider"]) => string; passwordChangeLoading: () => boolean; newPassword: () => string; passwordsMatch: () => boolean; passwordLengthSufficient: () => boolean; passwordError: () => boolean; showPasswordSuccess: () => boolean; }) { const { profile, handlePasswordSubmit, oldPasswordRef, newPasswordRef, newPasswordConfRef, handleNewPasswordChange, handlePasswordConfChange, handlePasswordBlur, sendEmailVerification, getProviderName, passwordChangeLoading, newPassword, passwordsMatch, passwordLengthSufficient, passwordError, showPasswordSuccess } = props; return (
{profile().hasPassword ? "Change Password" : "Add Password"}
⚠️ Email Verification Required
{!profile().email ? "Please add and verify an email address before setting a password." : "Please verify your email address before setting a password."}
{profile().provider === "email" ? "Set a password to enable password login" : "Add a password to enable email/password login alongside your " + getProviderName(profile().provider) + " login"}
= 6 } >
); } function LinkedProvidersSection(props: { profile: () => UserProfile }) { const { profile } = props; return (
Linked Authentication Methods
); } function SignOutSection(props: { handleSignOut: () => void; signOutLoading: () => boolean; }) { const { handleSignOut, signOutLoading } = props; return (
); } function DeleteAccountSection(props: { profile: () => UserProfile; getProviderName: (provider: UserProfile["provider"]) => string; deleteAccountPasswordRef: (el: HTMLInputElement) => void; deleteAccountButtonLoading: () => boolean; deleteAccountTrigger: (e: Event) => void; passwordDeletionError: () => boolean; }) { const { profile, getProviderName, deleteAccountPasswordRef, deleteAccountButtonLoading, deleteAccountTrigger, passwordDeletionError } = props; return (
Delete Account
Warning: This will delete all account information and is irreversible
Your {getProviderName(profile().provider)}{" "} account doesn't have a password. To delete your account, please set a password first, then return here to proceed with deletion.
} >
); } function LinkedProviders(props: { userId: string }) { const [providers, setProviders] = createSignal([]); const [loading, setLoading] = createSignal(true); const [unlinkLoading, setUnlinkLoading] = createSignal(null); const loadProviders = async () => { try { const response = await fetch("/api/trpc/user.getProviders"); const result = await response.json(); if (response.ok && result.result?.data) { setProviders(result.result.data); } } catch (err) { console.error("Failed to load providers:", err); } finally { setLoading(false); } }; createEffect(() => { loadProviders(); }); const handleUnlink = async (provider: string) => { if (!confirm(`Are you sure you want to unlink your ${provider} account?`)) { return; } setUnlinkLoading(provider); try { const response = await fetch("/api/trpc/user.unlinkProvider", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider }) }); const result = await response.json(); if (response.ok && result.result?.data?.success) { await loadProviders(); alert(`${provider} account unlinked successfully`); } else { alert(result.error?.message || "Failed to unlink provider"); } } catch (err) { console.error("Failed to unlink provider:", err); alert("Failed to unlink provider"); } finally { setUnlinkLoading(null); } }; const getProviderIcon = (provider: string) => { switch (provider) { case "google": return ; case "github": return ; case "email": return ; default: return null; } }; const formatDate = (dateStr: string) => { return new Date(dateStr).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); }; return (
Loading providers...
No linked providers found
{(provider) => (
{getProviderIcon(provider.provider)}
{provider.provider}
{provider.email}
Last used: {formatDate(provider.lastUsedAt)}
1}>
Primary method
)}
); }