mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-22 08:33:13 -04:00
Make credential edit flow work (#900)
This commit is contained in:
committed by
Leendert de Borst
parent
34d00dc7d6
commit
0ccbeb683d
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Form input props.
|
||||
*/
|
||||
type FormInputProps = {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
type?: 'text' | 'password';
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
multiline?: boolean;
|
||||
rows?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form input component.
|
||||
*/
|
||||
export const FormInput: React.FC<FormInputProps> = ({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
type = 'text',
|
||||
placeholder,
|
||||
required = false,
|
||||
multiline = false,
|
||||
rows = 1,
|
||||
error
|
||||
}) => {
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
|
||||
const inputClasses = `mt-1 block w-full rounded-md ${
|
||||
error ? 'border-red-500' : 'border-gray-300 dark:border-gray-700'
|
||||
} text-gray-900 sm:text-sm rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400 p-3`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor={id} className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<div className="relative">
|
||||
{multiline ? (
|
||||
<textarea
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
rows={rows}
|
||||
placeholder={placeholder}
|
||||
className={inputClasses}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type={type === 'password' && !showPassword ? 'password' : 'text'}
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className={inputClasses}
|
||||
/>
|
||||
)}
|
||||
{type === 'password' && (
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2">
|
||||
<button
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-900 transition-colors duration-200"
|
||||
>
|
||||
{showPassword ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{error && (
|
||||
<p className="mt-1 text-sm text-red-500">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,7 +6,8 @@ export enum HeaderIconType {
|
||||
DELETE = 'delete',
|
||||
SETTINGS = 'settings',
|
||||
RELOAD = 'reload',
|
||||
EXTERNAL_LINK = 'external_link'
|
||||
EXTERNAL_LINK = 'external_link',
|
||||
SAVE = 'save'
|
||||
}
|
||||
|
||||
type HeaderIconProps = {
|
||||
@@ -115,7 +116,35 @@ export const HeaderIcon: React.FC<HeaderIconProps> = ({ type, className = 'w-5 h
|
||||
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
|
||||
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
|
||||
</svg>
|
||||
)
|
||||
),
|
||||
[HeaderIconType.SAVE]: (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M17 3H7a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V7l-4-4z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 3v5h10"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 12a2 2 0 100 4 2 2 0 000-4z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
return icons[type] || null;
|
||||
|
||||
@@ -27,7 +27,6 @@ const CredentialDetails: React.FC = (): React.ReactElement => {
|
||||
const [credential, setCredential] = useState<Credential | null>(null);
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const [headerButtonsConfigured, setHeaderButtonsConfigured] = useState(false);
|
||||
|
||||
/**
|
||||
* Check if the current page is an expanded popup.
|
||||
@@ -102,28 +101,23 @@ const CredentialDetails: React.FC = (): React.ReactElement => {
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
// Only set the header buttons once on mount.
|
||||
if (!headerButtonsConfigured) {
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
<HeaderButton
|
||||
onClick={openInNewPopup}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
<HeaderButton
|
||||
onClick={handleEdit}
|
||||
title="Edit credential"
|
||||
iconType={HeaderIconType.EDIT}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
setHeaderButtonsConfigured(true);
|
||||
}
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
<HeaderButton
|
||||
onClick={openInNewPopup}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
<HeaderButton
|
||||
onClick={handleEdit}
|
||||
title="Edit credential"
|
||||
iconType={HeaderIconType.EDIT}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
return () => {};
|
||||
}, [setHeaderButtons, headerButtonsConfigured, handleEdit, openInNewPopup]);
|
||||
}, [setHeaderButtons, handleEdit, openInNewPopup]);
|
||||
|
||||
// Clear header buttons on unmount
|
||||
useEffect((): (() => void) => {
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { FormInput } from '@/entrypoints/popup/components/FormInput';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/icons/HeaderIcons';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
|
||||
|
||||
import type { Credential } from '@/utils/shared/models/vault';
|
||||
|
||||
type CredentialMode = 'random' | 'manual';
|
||||
|
||||
/**
|
||||
* Credential edit page.
|
||||
* Add or edit credential page.
|
||||
*/
|
||||
const CredentialEdit: React.FC = () => {
|
||||
const CredentialAddEdit: React.FC = () => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const dbContext = useDb();
|
||||
const [credential, setCredential] = useState<Credential | null>(null);
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const { executeVaultMutation, isLoading, syncStatus } = useVaultMutate();
|
||||
const [mode, setMode] = useState<CredentialMode>('random');
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
|
||||
// If we received an ID, we're in edit mode
|
||||
const isEditMode = id !== undefined && id.length > 0;
|
||||
|
||||
/**
|
||||
* Load an existing credential from the database in edit mode.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!dbContext?.sqliteClient || !id) {
|
||||
return;
|
||||
@@ -28,6 +42,10 @@ const CredentialEdit: React.FC = () => {
|
||||
if (result) {
|
||||
setCredential(result);
|
||||
setIsInitialLoading(false);
|
||||
// If credential has alias data, switch to manual mode
|
||||
if (result.Alias?.FirstName || result.Alias?.LastName) {
|
||||
setMode('manual');
|
||||
}
|
||||
} else {
|
||||
console.error('Credential not found');
|
||||
navigate('/credentials');
|
||||
@@ -39,115 +57,241 @@ const CredentialEdit: React.FC = () => {
|
||||
|
||||
/**
|
||||
* Handle the delete button click.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const handleDelete = async (): Promise<void> => {
|
||||
const handleDelete = useCallback(async (): Promise<void> => {
|
||||
if (!id || !window.confirm('Are you sure you want to delete this credential? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the vault mutation to delete the credential.
|
||||
*/
|
||||
executeVaultMutation(async () => {
|
||||
/**
|
||||
* Delete the credential from the database.
|
||||
*/
|
||||
dbContext.sqliteClient!.deleteCredentialById(id);
|
||||
}, {
|
||||
/**
|
||||
* Navigate back to the credentials list on success.
|
||||
* Navigate to the credentials list page on success.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
navigate('/credentials');
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [id, executeVaultMutation, dbContext.sqliteClient, navigate]);
|
||||
|
||||
if (!credential) {
|
||||
/**
|
||||
* Handle form submission.
|
||||
*/
|
||||
const handleSubmit = useCallback(async (): Promise<void> => {
|
||||
if (!credential) {
|
||||
return;
|
||||
}
|
||||
|
||||
executeVaultMutation(async () => {
|
||||
if (isEditMode) {
|
||||
await dbContext.sqliteClient!.updateCredentialById(credential);
|
||||
} else {
|
||||
const credentialId = await dbContext.sqliteClient!.createCredential(credential);
|
||||
credential.Id = credentialId.toString();
|
||||
}
|
||||
}, {
|
||||
/**
|
||||
* Navigate to the credential details page on success.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
// Pop the current page from the history stack
|
||||
navigate(-1);
|
||||
}
|
||||
});
|
||||
}, [credential, isEditMode, dbContext.sqliteClient, executeVaultMutation, navigate]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
// Only set the header buttons once on mount.
|
||||
if (credential) {
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
{isEditMode && (
|
||||
<HeaderButton
|
||||
onClick={handleDelete}
|
||||
title="Delete credential"
|
||||
iconType={HeaderIconType.DELETE}
|
||||
variant="danger"
|
||||
/>
|
||||
)}
|
||||
<HeaderButton
|
||||
onClick={handleSubmit}
|
||||
title="Save credential"
|
||||
iconType={HeaderIconType.SAVE}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
}
|
||||
return () => {};
|
||||
}, [setHeaderButtons, handleSubmit, credential, isEditMode, handleDelete]);
|
||||
|
||||
// Clear header buttons on unmount
|
||||
useEffect((): (() => void) => {
|
||||
return () => setHeaderButtons(null);
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
if (!credential && isEditMode) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-xl font-bold">Edit Credential</h1>
|
||||
<button
|
||||
onClick={() => navigate(`/credentials/${id}`)}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{isLoading && (
|
||||
<div className="text-sm text-gray-500">
|
||||
{syncStatus}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Service Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={credential.ServiceName}
|
||||
onChange={(e) => setCredential({ ...credential, ServiceName: e.target.value })}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Service URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={credential.ServiceUrl}
|
||||
onChange={(e) => setCredential({ ...credential, ServiceUrl: e.target.value })}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
value={credential.Username}
|
||||
onChange={(e) => setCredential({ ...credential, Username: e.target.value })}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={credential.Password}
|
||||
onChange={(e) => setCredential({ ...credential, Password: e.target.value })}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Notes</label>
|
||||
<textarea
|
||||
value={credential.Notes}
|
||||
onChange={(e) => setCredential({ ...credential, Notes: e.target.value })}
|
||||
rows={4}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
{!isEditMode && (
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="w-full bg-red-500 text-white py-2 px-4 rounded hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
|
||||
onClick={() => setMode('random')}
|
||||
className={`flex-1 py-2 px-4 rounded ${
|
||||
mode === 'random' ? 'bg-blue-500 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
Delete Credential
|
||||
Random Alias
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('manual')}
|
||||
className={`flex-1 py-2 px-4 rounded ${
|
||||
mode === 'manual' ? 'bg-blue-500 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
Manual
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Service</h2>
|
||||
<div className="space-y-4">
|
||||
<FormInput
|
||||
id="serviceName"
|
||||
label="Service Name"
|
||||
value={credential?.ServiceName || ''}
|
||||
onChange={(value) => setCredential({ ...credential!, ServiceName: value })}
|
||||
required
|
||||
/>
|
||||
<FormInput
|
||||
id="serviceUrl"
|
||||
label="Service URL"
|
||||
value={credential?.ServiceUrl || ''}
|
||||
onChange={(value) => setCredential({ ...credential!, ServiceUrl: value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(mode === 'manual' || isEditMode) && (
|
||||
<>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Login Credentials</h2>
|
||||
<div className="space-y-4">
|
||||
<FormInput
|
||||
id="username"
|
||||
label="Username"
|
||||
value={credential?.Username || ''}
|
||||
onChange={(value) => setCredential({ ...credential!, Username: value })}
|
||||
/>
|
||||
<FormInput
|
||||
id="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
value={credential?.Password || ''}
|
||||
onChange={(value) => setCredential({ ...credential!, Password: value })}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {/* TODO: Implement generate random alias */}}
|
||||
className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
Generate Random Alias
|
||||
</button>
|
||||
<FormInput
|
||||
id="email"
|
||||
label="Email"
|
||||
value={credential?.Alias?.Email || ''}
|
||||
onChange={(value) => setCredential({
|
||||
...credential!,
|
||||
Alias: { ...credential!.Alias, Email: value }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Alias</h2>
|
||||
<div className="space-y-4">
|
||||
<FormInput
|
||||
id="firstName"
|
||||
label="First Name"
|
||||
value={credential?.Alias?.FirstName || ''}
|
||||
onChange={(value) => setCredential({
|
||||
...credential!,
|
||||
Alias: { ...credential!.Alias, FirstName: value }
|
||||
})}
|
||||
/>
|
||||
<FormInput
|
||||
id="lastName"
|
||||
label="Last Name"
|
||||
value={credential?.Alias?.LastName || ''}
|
||||
onChange={(value) => setCredential({
|
||||
...credential!,
|
||||
Alias: { ...credential!.Alias, LastName: value }
|
||||
})}
|
||||
/>
|
||||
<FormInput
|
||||
id="nickName"
|
||||
label="Nick Name"
|
||||
value={credential?.Alias?.NickName || ''}
|
||||
onChange={(value) => setCredential({
|
||||
...credential!,
|
||||
Alias: { ...credential!.Alias, NickName: value }
|
||||
})}
|
||||
/>
|
||||
<FormInput
|
||||
id="gender"
|
||||
label="Gender"
|
||||
value={credential?.Alias?.Gender || ''}
|
||||
onChange={(value) => setCredential({
|
||||
...credential!,
|
||||
Alias: { ...credential!.Alias, Gender: value }
|
||||
})}
|
||||
/>
|
||||
<FormInput
|
||||
id="birthDate"
|
||||
label="Birth Date"
|
||||
placeholder="YYYY-MM-DD"
|
||||
value={credential?.Alias?.BirthDate || ''}
|
||||
onChange={(value) => setCredential({
|
||||
...credential!,
|
||||
Alias: { ...credential!.Alias, BirthDate: value }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Metadata</h2>
|
||||
<div className="space-y-4">
|
||||
<FormInput
|
||||
id="notes"
|
||||
label="Notes"
|
||||
value={credential?.Notes || ''}
|
||||
onChange={(value) => setCredential({ ...credential!, Notes: value })}
|
||||
multiline
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CredentialEdit;
|
||||
export default CredentialAddEdit;
|
||||
@@ -622,6 +622,167 @@ export class SqliteClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing credential with associated entities
|
||||
* @param credential The credential object to update
|
||||
* @returns The number of rows modified
|
||||
*/
|
||||
public async updateCredentialById(credential: Credential): Promise<number> {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
this.beginTransaction();
|
||||
const currentDateTime = new Date().toISOString()
|
||||
.replace('T', ' ')
|
||||
.replace('Z', '')
|
||||
.substring(0, 23);
|
||||
|
||||
// Get existing credential to compare changes
|
||||
const existingCredential = this.getCredentialById(credential.Id);
|
||||
if (!existingCredential) {
|
||||
throw new Error('Credential not found');
|
||||
}
|
||||
|
||||
// 1. Update Service
|
||||
const serviceQuery = `
|
||||
UPDATE Services
|
||||
SET Name = ?,
|
||||
Url = ?,
|
||||
Logo = COALESCE(?, Logo),
|
||||
UpdatedAt = ?
|
||||
WHERE Id = (
|
||||
SELECT ServiceId
|
||||
FROM Credentials
|
||||
WHERE Id = ?
|
||||
)`;
|
||||
|
||||
let logoData = null;
|
||||
try {
|
||||
if (credential.Logo) {
|
||||
// Handle object-like array conversion
|
||||
if (typeof credential.Logo === 'object' && !ArrayBuffer.isView(credential.Logo)) {
|
||||
const values = Object.values(credential.Logo);
|
||||
logoData = new Uint8Array(values);
|
||||
// Handle existing array types
|
||||
} else if (Array.isArray(credential.Logo) || credential.Logo instanceof ArrayBuffer || credential.Logo instanceof Uint8Array) {
|
||||
logoData = new Uint8Array(credential.Logo);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to convert logo to Uint8Array:', error);
|
||||
logoData = null;
|
||||
}
|
||||
|
||||
this.executeUpdate(serviceQuery, [
|
||||
credential.ServiceName,
|
||||
credential.ServiceUrl ?? null,
|
||||
logoData,
|
||||
currentDateTime,
|
||||
credential.Id
|
||||
]);
|
||||
|
||||
// 2. Update Alias
|
||||
const aliasQuery = `
|
||||
UPDATE Aliases
|
||||
SET FirstName = ?,
|
||||
LastName = ?,
|
||||
NickName = ?,
|
||||
BirthDate = ?,
|
||||
Gender = ?,
|
||||
Email = ?,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = (
|
||||
SELECT AliasId
|
||||
FROM Credentials
|
||||
WHERE Id = ?
|
||||
)`;
|
||||
|
||||
// Only update BirthDate if it's actually different (accounting for format differences)
|
||||
let birthDate = credential.Alias.BirthDate;
|
||||
if (birthDate && existingCredential.Alias.BirthDate) {
|
||||
const newDate = new Date(birthDate);
|
||||
const existingDate = new Date(existingCredential.Alias.BirthDate);
|
||||
if (newDate.getTime() === existingDate.getTime()) {
|
||||
birthDate = existingCredential.Alias.BirthDate;
|
||||
}
|
||||
}
|
||||
|
||||
this.executeUpdate(aliasQuery, [
|
||||
credential.Alias.FirstName ?? null,
|
||||
credential.Alias.LastName ?? null,
|
||||
credential.Alias.NickName ?? null,
|
||||
birthDate ?? null,
|
||||
credential.Alias.Gender ?? null,
|
||||
credential.Alias.Email ?? null,
|
||||
currentDateTime,
|
||||
credential.Id
|
||||
]);
|
||||
|
||||
// 3. Update Credential
|
||||
const credentialQuery = `
|
||||
UPDATE Credentials
|
||||
SET Username = ?,
|
||||
Notes = ?,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = ?`;
|
||||
|
||||
this.executeUpdate(credentialQuery, [
|
||||
credential.Username ?? null,
|
||||
credential.Notes ?? null,
|
||||
currentDateTime,
|
||||
credential.Id
|
||||
]);
|
||||
|
||||
// 4. Update Password if changed
|
||||
if (credential.Password !== existingCredential.Password) {
|
||||
// Check if a password record already exists for this credential, if not, then create one.
|
||||
const passwordRecordExistsQuery = `
|
||||
SELECT Id
|
||||
FROM Passwords
|
||||
WHERE CredentialId = ?`;
|
||||
const passwordResults = this.executeQuery(passwordRecordExistsQuery, [credential.Id]);
|
||||
|
||||
if (passwordResults.length === 0) {
|
||||
// Create a new password record
|
||||
const passwordQuery = `
|
||||
INSERT INTO Passwords (Id, Value, CredentialId, CreatedAt, UpdatedAt, IsDeleted)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
this.executeUpdate(passwordQuery, [
|
||||
crypto.randomUUID().toUpperCase(),
|
||||
credential.Password,
|
||||
credential.Id,
|
||||
currentDateTime,
|
||||
currentDateTime,
|
||||
0
|
||||
]);
|
||||
} else {
|
||||
// Update the existing password record
|
||||
const passwordQuery = `
|
||||
UPDATE Passwords
|
||||
SET Value = ?, UpdatedAt = ?
|
||||
WHERE CredentialId = ?`;
|
||||
|
||||
this.executeUpdate(passwordQuery, [
|
||||
credential.Password,
|
||||
currentDateTime,
|
||||
credential.Id
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
await this.commitTransaction();
|
||||
return 1;
|
||||
|
||||
} catch (error) {
|
||||
this.rollbackTransaction();
|
||||
console.error('Error updating credential:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert binary data to a base64 encoded image source.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user