330 lines
11 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|