import { createSignal, Show, createEffect } from "solid-js"; import { Title, Meta } from "@solidjs/meta"; import { useNavigate, redirect, query, createAsync } from "@solidjs/router"; import { getEvent } from "vinxi/http"; import Eye from "~/components/icons/Eye"; import EyeSlash from "~/components/icons/EyeSlash"; 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 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 [showOldPasswordInput, setShowOldPasswordInput] = createSignal(false); const [showPasswordInput, setShowPasswordInput] = createSignal(false); const [showPasswordConfInput, setShowPasswordConfInput] = createSignal(false); 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); } }); // Profile image handlers 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); // Update preSetHolder with new image 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); } }; // Email update handler 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); } }; // Display name update handler 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); } }; // Password change/set handler const handlePasswordSubmit = async (e: Event) => { e.preventDefault(); const userProfile = currentUser(); if (!userProfile) return; if (userProfile.hasPassword) { // Change password (requires old password) 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 { // Set password (first time for OAuth users) 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) { // Refresh user data to show hasPassword = true 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); } } }; // Delete account handler 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); } }; // Resend email verification 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); }; // Sign out handler const handleSignOut = async () => { setSignOutLoading(true); try { await api.auth.signOut.mutate(); navigate("/"); } catch (error) { console.error("Sign out failed:", error); setSignOutLoading(false); } }; // Helper to get provider display name const getProviderName = (provider: UserProfile["provider"]) => { switch (provider) { case "google": return "Google"; case "github": return "GitHub"; case "email": return "Email"; default: return "Unknown"; } }; // Helper to get provider icon color 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 ( <> Account | Michael Freno
}> {(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
Profile image updated!

{/* 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
Email updated!
{/* Display Name Section */}
Display Name:
{userProfile().displayName ? ( {userProfile().displayName} ) : ( None Set )}
Display name updated!
{/* 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 } >
Passwords do not match!
{userProfile().hasPassword ? "Password did not match record" : "Error setting password"}
Password {userProfile().hasPassword ? "changed" : "set"}{" "} successfully!

{/* 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.
} >
Password did not match record
)}
); }