import { createSignal, Show, createEffect } from "solid-js"; import { PageHead } from "~/components/PageHead"; import { useNavigate, redirect, query, createAsync } from "@solidjs/router"; import { getEvent } from "vinxi/http"; 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 PasswordStrengthMeter from "~/components/PasswordStrengthMeter"; const getUserProfile = query(async (): Promise => { "use server"; const { getUserID, ConnectionFactory } = await import("~/server/utils"); const event = getEvent()!; const userId = await getUserID(event); if (!userId) { throw redirect("/login"); } const conn = ConnectionFactory(); try { const res = await conn.execute({ sql: "SELECT * FROM User WHERE id = ?", args: [userId] }); if (res.rows.length === 0) { throw redirect("/login"); } const user = res.rows[0] as any; return { id: user.id, email: user.email ?? undefined, emailVerified: user.email_verified === 1, displayName: user.display_name ?? 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 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(); 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
{/* Account Type Section */}
Account Type
{getProviderName(userProfile().provider)} Account
⚠️ Add an email address for account recovery
💡 Add a password to enable email/password login

{/* Profile Image Section */}
Profile Image

{/* Email Section */}
{userProfile().provider === "email" ? "Email:" : "Linked Email:"}
{userProfile().email ? ( {userProfile().email} ) : ( {userProfile().provider === "email" ? "None Set" : "Not Linked"} )}
Add an email for account recovery and notifications
{/* Display Name Section */}
Display Name:
{userProfile().displayName ? ( {userProfile().displayName} ) : ( None Set )}
{/* Password Change/Set Section */}
{userProfile().hasPassword ? "Change Password" : "Add Password"}
{userProfile().provider === "email" ? "Set a password to enable password login" : "Add a password to enable email/password login alongside your " + getProviderName(userProfile().provider) + " login"}
= 6 } >

{/* Sign Out Section */}

{/* Delete Account Section */}
Delete Account
Warning: This will delete all account information and is irreversible
Your {getProviderName(userProfile().provider)}{" "} account doesn't have a password. To delete your account, please set a password first, then return here to proceed with deletion.
} >
)}
); }