import { createSignal, createEffect, Show, onMount } from "solid-js"; import { Title, Meta } from "@solidjs/meta"; import { useNavigate, cache, redirect } 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 Dropzone from "~/components/blog/Dropzone"; import AddImageToS3 from "~/lib/s3upload"; import { validatePassword, isValidEmail } from "~/lib/validation"; type UserProfile = { id: string; email: string | null; emailVerified: boolean; displayName: string | null; image: string | null; provider: string; hasPassword: boolean; }; const checkAuth = cache(async () => { "use server"; const { checkAuthStatus } = await import("~/server/utils"); const event = getEvent()!; const { isAuthenticated } = await checkAuthStatus(event); if (!isAuthenticated) { throw redirect("/login"); } return { isAuthenticated }; }, "accountAuthCheck"); export const route = { load: () => checkAuth() }; export default function AccountPage() { const navigate = useNavigate(); // User data const [user, setUser] = createSignal(null); const [loading, setLoading] = createSignal(true); // Form loading states 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); // Password state 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); // Show/hide password toggles const [showOldPasswordInput, setShowOldPasswordInput] = createSignal(false); const [showPasswordInput, setShowPasswordInput] = createSignal(false); const [showPasswordConfInput, setShowPasswordConfInput] = createSignal(false); // Success messages const [showImageSuccess, setShowImageSuccess] = createSignal(false); const [showEmailSuccess, setShowEmailSuccess] = createSignal(false); const [showDisplayNameSuccess, setShowDisplayNameSuccess] = createSignal(false); const [showPasswordSuccess, setShowPasswordSuccess] = createSignal(false); // Profile image state const [profileImage, setProfileImage] = createSignal( undefined ); const [profileImageHolder, setProfileImageHolder] = createSignal< string | null >(null); const [profileImageStateChange, setProfileImageStateChange] = createSignal(false); const [preSetHolder, setPreSetHolder] = createSignal(null); // Form refs let oldPasswordRef: HTMLInputElement | undefined; let newPasswordRef: HTMLInputElement | undefined; let newPasswordConfRef: HTMLInputElement | undefined; let emailRef: HTMLInputElement | undefined; let displayNameRef: HTMLInputElement | undefined; let deleteAccountPasswordRef: HTMLInputElement | undefined; // Fetch user profile on mount onMount(async () => { try { const response = await fetch("/api/trpc/user.getProfile", { method: "GET" }); if (response.ok) { const result = await response.json(); if (result.result?.data) { setUser(result.result.data); // Set preset holder if user has existing image if (result.result.data.image) { setPreSetHolder(result.result.data.image); } } } } catch (err) { console.error("Failed to fetch user profile:", err); } finally { setLoading(false); } }); // 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 currentUser = user(); if (!currentUser) { setProfileImageSetLoading(false); return; } try { let imageUrl = ""; // Upload new image if one was selected if (profileImage()) { const imageKey = await AddImageToS3( profileImage()!, currentUser.id, "user" ); imageUrl = imageKey || ""; } // Update user profile image 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 currentUser = user(); if (!currentUser) return; if (currentUser.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 }) }); const result = await response.json(); if (response.ok && result.result?.data?.success) { setShowPasswordSuccess(true); setTimeout(() => setShowPasswordSuccess(false), 3000); // Clear form 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({ password: newPassword }) }); 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); // Clear form 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) { // Redirect to login 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 currentUser = user(); if (!currentUser?.email) return; try { await fetch("/api/trpc/auth.resendEmailVerification", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: currentUser.email }) }); alert("Verification email sent!"); } catch (err) { console.error("Email verification error:", err); } }; // Password validation helpers 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; 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); }; return ( <> Account | Michael Freno
Loading...
} > {(currentUser) => ( <>
Account Settings
{/* Profile Image Section */}
Profile Image
Profile image updated!

{/* Email Section */}
Current email:
{currentUser().email ? ( {currentUser().email} ) : ( None Set )}
Email updated!
{/* Display Name Section */}
Display Name:
{currentUser().displayName ? ( {currentUser().displayName} ) : ( None Set )}
Display name updated!
{/* Password Change/Set Section */}
{currentUser().hasPassword ? "Change Password" : "Set Password"}
Password too short! Min Length: 8
= 6 } >
Passwords do not match!
{currentUser().hasPassword ? "Password did not match record" : "Error setting password"}
Password {currentUser().hasPassword ? "changed" : "set"}{" "} successfully!

{/* Delete Account Section */}
Delete Account
Warning: This will delete all account information and is irreversible
Password did not match record
)}
); }