integratedhub/islands/UserProfileEdit.tsx
2026-01-20 09:02:26 -06:00

330 lines
11 KiB
TypeScript

import { useEffect, useRef, useState } from "preact/hooks";
import {
decryptDataWithStoredPassword,
encryptDataWithStoredPassword,
getEncryptionPassword,
} from "../helpers.ts";
import { User } from "../db/interfaces.ts";
interface UserProfileEditProps {
user: User;
isEncrypted: boolean;
}
export default function UserProfileEdit(
{ user, isEncrypted }: UserProfileEditProps,
) {
const [form, setForm] = useState<User>({ ...user });
const [errors, setErrors] = useState<{ name?: string; email?: string }>({});
const [isEncryptionEnabled, setIsEncryptionEnabled] = useState(isEncrypted);
const formRef = useRef<HTMLFormElement>(null);
// Check if encryption is available and decrypt data on component mount
useEffect(() => {
const initializeComponent = async () => {
const encryptionEnabled = getEncryptionPassword(user.id);
setIsEncryptionEnabled(encryptionEnabled !== null);
// If encryption is enabled and user has encrypted data, decrypt and populate fields
if (encryptionEnabled && user.encrypted_data) {
try {
const decryptedData = await decryptDataWithStoredPassword(
user.id,
user.encrypted_data,
);
if (decryptedData) {
const encryptedDataObj = JSON.parse(decryptedData);
// Only update form state with fields that have actual data from decrypted object
const updatedFields: Partial<User> = {};
if (encryptedDataObj.address_1) {
updatedFields.address_1 = encryptedDataObj.address_1;
}
if (encryptedDataObj.address_2) {
updatedFields.address_2 = encryptedDataObj.address_2;
}
if (encryptedDataObj.city) {
updatedFields.city = encryptedDataObj.city;
}
if (encryptedDataObj.state) {
updatedFields.state = encryptedDataObj.state;
}
if (encryptedDataObj.zip) updatedFields.zip = encryptedDataObj.zip;
if (encryptedDataObj.country) {
updatedFields.country = encryptedDataObj.country;
}
setForm((prevForm) => ({
...prevForm,
...updatedFields,
}));
}
} catch (error) {
console.error("Failed to decrypt user data:", error);
// Continue with original user data if decryption fails
}
}
};
initializeComponent();
}, [user.id, user.encrypted_data]);
function handleChange(e: Event) {
const target = e.target as HTMLInputElement;
setForm({ ...form, [target.name]: target.value });
setErrors({ ...errors, [target.name]: undefined });
}
async function handleSave(e: Event) {
e.preventDefault();
// Validate required fields
const newErrors: { [key: string]: string } = {};
if (!form.name?.trim()) newErrors.name = "Name is required";
if (!form.email?.trim()) newErrors.email = "Email is required";
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
setErrors({});
// Prepare data for submission
const submissionData = { ...form };
// Handle encryption if enabled
if (isEncryptionEnabled) {
const dataToEncrypt = {
user_id: user.id,
address_1: form.address_1 || "",
address_2: form.address_2 || "",
city: form.city || "",
state: form.state || "",
zip: form.zip || "",
country: form.country || "",
};
try {
const encryptedData = await encryptDataWithStoredPassword(
user.id,
JSON.stringify(dataToEncrypt),
);
if (encryptedData) {
submissionData.encrypted_data = encryptedData;
// Clear address fields when encryption is used
submissionData.address_1 = "";
submissionData.address_2 = "";
submissionData.city = "";
submissionData.state = "";
submissionData.zip = "";
submissionData.country = "";
}
} catch (error) {
console.error("Encryption failed:", error);
alert("Failed to encrypt data. Please try again.");
return;
}
}
// Get the form element using the ref
const formElement = formRef.current;
if (!formElement) return;
// Update form field values with submission data (including encrypted data)
Object.entries(submissionData).forEach(([key, value]) => {
const input = formElement.querySelector(
`[name="${key}"]`,
) as HTMLInputElement;
if (input && value !== null && value !== undefined) {
input.value = String(value);
}
});
// Submit the existing form
formElement.submit();
}
return (
<>
<button
type="button"
aria-label="Go back"
onClick={() => globalThis.location.href = "/"}
style={{
position: "fixed",
bottom: 300,
left: 4,
background: "none",
border: "none",
zIndex: 50,
cursor: "pointer",
}}
>
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="15 18 9 12 15 6" />
</svg>
</button>
<div class="w-full max-w-xl mx-auto mt-8 bg-white shadow-lg rounded-xl p-6 sm:p-8 flex flex-col items-center">
<form
ref={formRef}
class="flex flex-col gap-4 w-full pb-14"
onSubmit={handleSave}
method="POST"
action="/api/user/update"
>
<h2 class="text-lg font-semibold mb-2 mt-2">Profile</h2>
<label class="flex flex-col text-sm font-medium gap-1">
Name:
<input
name="name"
value={form.name || ""}
onInput={handleChange}
required
minLength={1}
class="input input-bordered rounded px-3 py-2 bg-slate-100 focus:bg-white transition-colors"
/>
{errors.name && (
<span class="text-red-600 text-xs mt-1">{errors.name}</span>
)}
</label>
<label class="flex flex-col text-sm font-medium gap-1">
Email:
<input
name="email"
value={form.email || ""}
onInput={handleChange}
required
type="email"
class="input input-bordered rounded px-3 py-2 bg-slate-100 focus:bg-white transition-colors"
/>
{errors.email && (
<span class="text-red-600 text-xs mt-1">{errors.email}</span>
)}
</label>
<label class="flex flex-col text-sm font-medium gap-1 mb-4">
Phone:
<input
name="phone"
value={form.phone || ""}
onInput={handleChange}
class="input input-bordered rounded px-3 py-2 bg-slate-100 focus:bg-white transition-colors"
/>
</label>
<input
type="hidden"
name="sender"
value={typeof location !== "undefined" ? location.pathname : ""}
/>
<input
type="hidden"
name="user_id"
value={user.id}
/>
{isEncryptionEnabled && (
<div class="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
<div class="flex items-center gap-2 mb-2">
<svg
class="w-5 h-5 text-green-600"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
clipRule="evenodd"
/>
</svg>
<span class="text-sm font-medium text-green-800">
Encryption Enabled
</span>
</div>
<p class="text-xs text-green-700">
Address information will be encrypted before saving.
</p>
</div>
)}
<input
type="hidden"
name="encrypted_data"
value={form.encrypted_data || ""}
/>
<h2 class="text-lg font-semibold mb-2 mt-4">Mailing Address</h2>
<label class="flex flex-col text-sm font-medium gap-1">
Address 1:
<input
name="address_1"
value={form.address_1 || ""}
onInput={handleChange}
class="input input-bordered rounded px-3 py-2 bg-slate-100 focus:bg-white transition-colors"
/>
</label>
<label class="flex flex-col text-sm font-medium gap-1">
Address 2:
<input
name="address_2"
value={form.address_2 || ""}
onInput={handleChange}
class="input input-bordered rounded px-3 py-2 bg-slate-100 focus:bg-white transition-colors"
/>
</label>
<label class="flex flex-col text-sm font-medium gap-1">
City:
<input
name="city"
value={form.city || ""}
onInput={handleChange}
class="input input-bordered rounded px-3 py-2 bg-slate-100 focus:bg-white transition-colors"
/>
</label>
<label class="flex flex-col text-sm font-medium gap-1">
State:
<input
name="state"
value={form.state || ""}
onInput={handleChange}
class="input input-bordered rounded px-3 py-2 bg-slate-100 focus:bg-white transition-colors"
/>
</label>
<label class="flex flex-col text-sm font-medium gap-1">
ZIP:
<input
name="zip"
value={form.zip || ""}
onInput={handleChange}
class="input input-bordered rounded px-3 py-2 bg-slate-100 focus:bg-white transition-colors"
/>
</label>
<label class="flex flex-col text-sm font-medium gap-1">
Country:
<input
name="country"
value={form.country || ""}
onInput={handleChange}
class="input input-bordered rounded px-3 py-2 bg-slate-100 focus:bg-white transition-colors"
/>
</label>
<div class="fixed bottom-4 inset-x-0 flex justify-center z-40 pointer-events-none">
<button
type="submit"
class="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-10 rounded-full shadow-lg pointer-events-auto w-11/12 max-w-md"
>
Save
</button>
</div>
</form>
</div>
</>
);
}