mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-20 23:52:31 -04:00
Add credential add page (#900)
This commit is contained in:
committed by
Leendert de Borst
parent
3da40f42c9
commit
c688764831
@@ -226,48 +226,16 @@ export async function getEmailAddressesForVault(
|
||||
/**
|
||||
* Get default email domain for a vault.
|
||||
*/
|
||||
export function handleGetDefaultEmailDomain(
|
||||
) : Promise<stringResponse> {
|
||||
return (async () : Promise<stringResponse> => {
|
||||
export function handleGetDefaultEmailDomain(): Promise<stringResponse> {
|
||||
return (async (): Promise<stringResponse> => {
|
||||
try {
|
||||
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[];
|
||||
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[];
|
||||
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const defaultEmailDomain = sqliteClient.getDefaultEmailDomain();
|
||||
const defaultEmailDomain = sqliteClient.getDefaultEmailDomain(privateEmailDomains, publicEmailDomains);
|
||||
|
||||
/**
|
||||
* Check if a domain is valid.
|
||||
*/
|
||||
const isValidDomain = (domain: string) : boolean => {
|
||||
const isValid = (domain &&
|
||||
domain !== 'DISABLED.TLD' &&
|
||||
(privateEmailDomains.includes(domain) || publicEmailDomains.includes(domain))) as boolean;
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
// First check if the default domain that is configured in the vault is still valid.
|
||||
if (defaultEmailDomain && isValidDomain(defaultEmailDomain)) {
|
||||
return { success: true, value: defaultEmailDomain };
|
||||
}
|
||||
|
||||
// If default domain is not valid, fall back to first available private domain.
|
||||
const firstPrivate = privateEmailDomains.find(isValidDomain);
|
||||
|
||||
if (firstPrivate) {
|
||||
return { success: true, value: firstPrivate };
|
||||
}
|
||||
|
||||
// Return first valid public domain if no private domains are available.
|
||||
const firstPublic = publicEmailDomains.find(isValidDomain);
|
||||
|
||||
if (firstPublic) {
|
||||
return { success: true, value: firstPublic };
|
||||
}
|
||||
|
||||
// Return null if no valid domains are found
|
||||
return { success: true };
|
||||
return { success: true, value: defaultEmailDomain ?? undefined };
|
||||
} catch (error) {
|
||||
console.error('Error getting default email domain:', error);
|
||||
return { success: false, error: 'Failed to get default email domain' };
|
||||
|
||||
@@ -9,8 +9,8 @@ import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import AuthSettings from '@/entrypoints/popup/pages/AuthSettings';
|
||||
import CredentialAddEdit from '@/entrypoints/popup/pages/CredentialAddEdit';
|
||||
import CredentialDetails from '@/entrypoints/popup/pages/CredentialDetails';
|
||||
import CredentialEdit from '@/entrypoints/popup/pages/CredentialEdit';
|
||||
import CredentialsList from '@/entrypoints/popup/pages/CredentialsList';
|
||||
import EmailDetails from '@/entrypoints/popup/pages/EmailDetails';
|
||||
import EmailsList from '@/entrypoints/popup/pages/EmailsList';
|
||||
@@ -46,8 +46,9 @@ const App: React.FC = () => {
|
||||
{ path: '/', element: <Home />, showBackButton: false },
|
||||
{ path: '/auth-settings', element: <AuthSettings />, showBackButton: true, title: 'Settings' },
|
||||
{ path: '/credentials', element: <CredentialsList />, showBackButton: false },
|
||||
{ path: '/credentials/add', element: <CredentialAddEdit />, showBackButton: true, title: 'Add credential' },
|
||||
{ path: '/credentials/:id', element: <CredentialDetails />, showBackButton: true, title: 'Credential details' },
|
||||
{ path: '/credentials/:id/edit', element: <CredentialEdit />, showBackButton: true, title: 'Edit credential' },
|
||||
{ path: '/credentials/:id/edit', element: <CredentialAddEdit />, showBackButton: true, title: 'Edit credential' },
|
||||
{ path: '/emails', element: <EmailsList />, showBackButton: false },
|
||||
{ path: '/emails/:id', element: <EmailDetails />, showBackButton: true, title: 'Email details' },
|
||||
{ path: '/settings', element: <Settings />, showBackButton: false },
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import React from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/icons/HeaderIcons';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
/**
|
||||
* Header props.
|
||||
*/
|
||||
@@ -32,19 +26,6 @@ const Header: React.FC<HeaderProps> = ({
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
/**
|
||||
* Open the client tab.
|
||||
*/
|
||||
const openClientTab = async () : Promise<void> => {
|
||||
const settingClientUrl = await storage.getItem('local:clientUrl') as string;
|
||||
let clientUrl = AppInfo.DEFAULT_CLIENT_URL;
|
||||
if (settingClientUrl && settingClientUrl.length > 0) {
|
||||
clientUrl = settingClientUrl;
|
||||
}
|
||||
|
||||
window.open(clientUrl, '_blank');
|
||||
};
|
||||
|
||||
// Updated route matching logic to handle URL parameters
|
||||
const currentRoute = routes?.find(route => {
|
||||
// Convert route pattern to regex
|
||||
@@ -112,13 +93,6 @@ const Header: React.FC<HeaderProps> = ({
|
||||
<div className="flex-grow" />
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{!currentRoute?.showBackButton && (
|
||||
<HeaderButton
|
||||
onClick={openClientTab}
|
||||
title="Open Client"
|
||||
iconType={HeaderIconType.EXTERNAL_LINK}
|
||||
/>
|
||||
)}
|
||||
{!authContext.isLoggedIn ? (
|
||||
<button
|
||||
id="settings"
|
||||
|
||||
@@ -7,7 +7,8 @@ export enum HeaderIconType {
|
||||
SETTINGS = 'settings',
|
||||
RELOAD = 'reload',
|
||||
EXTERNAL_LINK = 'external_link',
|
||||
SAVE = 'save'
|
||||
SAVE = 'save',
|
||||
PLUS = 'plus'
|
||||
}
|
||||
|
||||
type HeaderIconProps = {
|
||||
@@ -145,6 +146,17 @@ export const HeaderIcon: React.FC<HeaderIconProps> = ({ type, className = 'w-5 h
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
[HeaderIconType.PLUS]: (
|
||||
<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="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
)
|
||||
};
|
||||
|
||||
return icons[type] || null;
|
||||
|
||||
@@ -11,8 +11,9 @@ import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
|
||||
|
||||
import { IdentityHelperUtils } from '@/utils/shared/identity-generator';
|
||||
import { IdentityHelperUtils, CreateIdentityGenerator } from '@/utils/shared/identity-generator';
|
||||
import type { Credential } from '@/utils/shared/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/shared/password-generator';
|
||||
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
@@ -137,6 +138,69 @@ const CredentialAddEdit: React.FC = () => {
|
||||
});
|
||||
}, [id, executeVaultMutation, dbContext.sqliteClient, navigate]);
|
||||
|
||||
/**
|
||||
* Initialize the identity and password generators with settings from user's vault.
|
||||
*/
|
||||
const initializeGenerators = useCallback(async () => {
|
||||
// Get default identity language from database
|
||||
const identityLanguage = dbContext.sqliteClient!.getDefaultIdentityLanguage();
|
||||
|
||||
// Initialize identity generator based on language
|
||||
const identityGenerator = CreateIdentityGenerator(identityLanguage);
|
||||
|
||||
// Initialize password generator with settings from vault
|
||||
const passwordSettings = dbContext.sqliteClient!.getPasswordSettings();
|
||||
const passwordGenerator = CreatePasswordGenerator(passwordSettings);
|
||||
|
||||
return { identityGenerator, passwordGenerator };
|
||||
}, [dbContext.sqliteClient]);
|
||||
|
||||
/**
|
||||
* Generate a random alias and password.
|
||||
*/
|
||||
const generateRandomAlias = useCallback(async () => {
|
||||
const { identityGenerator, passwordGenerator } = await initializeGenerators();
|
||||
|
||||
const identity = identityGenerator.generateRandomIdentity();
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
|
||||
const metadata = await dbContext!.getVaultMetadata();
|
||||
|
||||
const privateEmailDomains = metadata?.privateEmailDomains ?? [];
|
||||
const publicEmailDomains = metadata?.publicEmailDomains ?? [];
|
||||
const defaultEmailDomain = dbContext.sqliteClient!.getDefaultEmailDomain(privateEmailDomains, publicEmailDomains);
|
||||
const email = defaultEmailDomain ? `${identity.emailPrefix}@${defaultEmailDomain}` : identity.emailPrefix;
|
||||
|
||||
setValue('Alias.Email', email);
|
||||
setValue('Alias.FirstName', identity.firstName);
|
||||
setValue('Alias.LastName', identity.lastName);
|
||||
setValue('Alias.NickName', identity.nickName);
|
||||
setValue('Alias.Gender', identity.gender);
|
||||
setValue('Alias.BirthDate', IdentityHelperUtils.normalizeBirthDateForDisplay(identity.birthDate.toISOString()));
|
||||
|
||||
// In edit mode, preserve existing username and password if they exist
|
||||
if (isEditMode && watch('Username')) {
|
||||
// Keep the existing username in edit mode, so don't do anything here.
|
||||
} else {
|
||||
// Use the newly generated username
|
||||
setValue('Username', identity.nickName);
|
||||
}
|
||||
|
||||
if (isEditMode && watch('Password')) {
|
||||
// Keep the existing password in edit mode, so don't do anything here.
|
||||
} else {
|
||||
// Use the newly generated password
|
||||
setValue('Password', password);
|
||||
}
|
||||
}, [isEditMode, watch, setValue, initializeGenerators, dbContext]);
|
||||
|
||||
/**
|
||||
* Handle the generate random alias button press.
|
||||
*/
|
||||
const handleGenerateRandomAlias = useCallback(() => {
|
||||
void generateRandomAlias();
|
||||
}, [generateRandomAlias]);
|
||||
|
||||
/**
|
||||
* Handle form submission.
|
||||
*/
|
||||
@@ -146,6 +210,20 @@ const CredentialAddEdit: React.FC = () => {
|
||||
data.Alias.BirthDate = IdentityHelperUtils.normalizeBirthDateForDb(data.Alias.BirthDate);
|
||||
}
|
||||
|
||||
// If we're creating a new credential and mode is random, generate random values here
|
||||
if (!isEditMode && mode === 'random') {
|
||||
// Generate random values now and then read them from the form fields to manually assign to the credentialToSave object
|
||||
await generateRandomAlias();
|
||||
data.Username = watch('Username');
|
||||
data.Password = watch('Password');
|
||||
data.Alias.FirstName = watch('Alias.FirstName');
|
||||
data.Alias.LastName = watch('Alias.LastName');
|
||||
data.Alias.NickName = watch('Alias.NickName');
|
||||
data.Alias.BirthDate = watch('Alias.BirthDate');
|
||||
data.Alias.Gender = watch('Alias.Gender');
|
||||
data.Alias.Email = watch('Alias.Email');
|
||||
}
|
||||
|
||||
executeVaultMutation(async () => {
|
||||
if (isEditMode) {
|
||||
await dbContext.sqliteClient!.updateCredentialById(data);
|
||||
@@ -158,11 +236,17 @@ const CredentialAddEdit: React.FC = () => {
|
||||
* Navigate to the credential details page on success.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
// Pop the current page from the history stack
|
||||
navigate(-1);
|
||||
// If in add mode, navigate to the credential details page.
|
||||
if (!isEditMode) {
|
||||
// Navigate to the credential details page.
|
||||
navigate(`/credentials/${data.Id}`, { replace: true });
|
||||
} else {
|
||||
// If in edit mode, pop the current page from the history stack to end up on details page as well.
|
||||
navigate(-1);
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [isEditMode, dbContext.sqliteClient, executeVaultMutation, navigate]);
|
||||
}, [isEditMode, dbContext.sqliteClient, executeVaultMutation, navigate, mode, watch, generateRandomAlias]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
@@ -194,7 +278,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
return () => setHeaderButtons(null);
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
if (!isEditMode && !watch('ServiceName')) {
|
||||
if (isEditMode && !watch('ServiceName')) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
@@ -214,7 +298,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<button
|
||||
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'
|
||||
mode === 'random' ? 'bg-primary-500 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
Random Alias
|
||||
@@ -222,7 +306,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<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'
|
||||
mode === 'manual' ? 'bg-primary-500 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
Manual
|
||||
@@ -237,7 +321,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<FormInput
|
||||
id="serviceName"
|
||||
label="Service Name"
|
||||
value={watch('ServiceName')}
|
||||
value={watch('ServiceName') ?? ''}
|
||||
onChange={(value) => setValue('ServiceName', value)}
|
||||
required
|
||||
error={errors.ServiceName?.message}
|
||||
@@ -245,7 +329,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<FormInput
|
||||
id="serviceUrl"
|
||||
label="Service URL"
|
||||
value={watch('ServiceUrl')}
|
||||
value={watch('ServiceUrl') ?? ''}
|
||||
onChange={(value) => setValue('ServiceUrl', value)}
|
||||
error={errors.ServiceUrl?.message}
|
||||
/>
|
||||
@@ -260,7 +344,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<FormInput
|
||||
id="username"
|
||||
label="Username"
|
||||
value={watch('Username')}
|
||||
value={watch('Username') ?? ''}
|
||||
onChange={(value) => setValue('Username', value)}
|
||||
error={errors.Username?.message}
|
||||
/>
|
||||
@@ -268,20 +352,20 @@ const CredentialAddEdit: React.FC = () => {
|
||||
id="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
value={watch('Password')}
|
||||
value={watch('Password') ?? ''}
|
||||
onChange={(value) => setValue('Password', value)}
|
||||
error={errors.Password?.message}
|
||||
/>
|
||||
<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"
|
||||
onClick={handleGenerateRandomAlias}
|
||||
className="w-full bg-primary-500 text-white py-2 px-4 rounded hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||||
>
|
||||
Generate Random Alias
|
||||
</button>
|
||||
<FormInput
|
||||
id="email"
|
||||
label="Email"
|
||||
value={watch('Alias.Email')}
|
||||
value={watch('Alias.Email') ?? ''}
|
||||
onChange={(value) => setValue('Alias.Email', value)}
|
||||
error={errors.Alias?.Email?.message}
|
||||
/>
|
||||
@@ -294,28 +378,28 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<FormInput
|
||||
id="firstName"
|
||||
label="First Name"
|
||||
value={watch('Alias.FirstName')}
|
||||
value={watch('Alias.FirstName') ?? ''}
|
||||
onChange={(value) => setValue('Alias.FirstName', value)}
|
||||
error={errors.Alias?.FirstName?.message}
|
||||
/>
|
||||
<FormInput
|
||||
id="lastName"
|
||||
label="Last Name"
|
||||
value={watch('Alias.LastName')}
|
||||
value={watch('Alias.LastName') ?? ''}
|
||||
onChange={(value) => setValue('Alias.LastName', value)}
|
||||
error={errors.Alias?.LastName?.message}
|
||||
/>
|
||||
<FormInput
|
||||
id="nickName"
|
||||
label="Nick Name"
|
||||
value={watch('Alias.NickName')}
|
||||
value={watch('Alias.NickName') ?? ''}
|
||||
onChange={(value) => setValue('Alias.NickName', value)}
|
||||
error={errors.Alias?.NickName?.message}
|
||||
/>
|
||||
<FormInput
|
||||
id="gender"
|
||||
label="Gender"
|
||||
value={watch('Alias.Gender')}
|
||||
value={watch('Alias.Gender') ?? ''}
|
||||
onChange={(value) => setValue('Alias.Gender', value)}
|
||||
error={errors.Alias?.Gender?.message}
|
||||
/>
|
||||
@@ -323,7 +407,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
id="birthDate"
|
||||
label="Birth Date"
|
||||
placeholder="YYYY-MM-DD"
|
||||
value={watch('Alias.BirthDate')}
|
||||
value={watch('Alias.BirthDate') ?? ''}
|
||||
onChange={(value) => setValue('Alias.BirthDate', value)}
|
||||
error={errors.Alias?.BirthDate?.message}
|
||||
/>
|
||||
@@ -336,7 +420,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<FormInput
|
||||
id="notes"
|
||||
label="Notes"
|
||||
value={watch('Notes')}
|
||||
value={watch('Notes') ?? ''}
|
||||
onChange={(value) => setValue('Notes', value)}
|
||||
multiline
|
||||
rows={4}
|
||||
@@ -67,7 +67,7 @@ const CredentialDetails: React.FC = (): React.ReactElement => {
|
||||
|
||||
window.open(
|
||||
`popup.html?expanded=true#/credentials/${id}/edit`,
|
||||
'CredentialEdit',
|
||||
'CredentialAddEdit',
|
||||
`width=${width},height=${height},left=${left},top=${top},popup=true`
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import CredentialCard from '@/entrypoints/popup/components/CredentialCard';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/icons/HeaderIcons';
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import ReloadButton from '@/entrypoints/popup/components/ReloadButton';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import { useVaultSync } from '@/entrypoints/popup/hooks/useVaultSync';
|
||||
@@ -18,7 +22,9 @@ import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
const CredentialsList: React.FC = () => {
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
const navigate = useNavigate();
|
||||
const { syncVault } = useVaultSync();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const [credentials, setCredentials] = useState<Credential[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
|
||||
@@ -28,6 +34,13 @@ const CredentialsList: React.FC = () => {
|
||||
*/
|
||||
const [isLoading, setIsLoading] = useMinDurationLoading(true, 100);
|
||||
|
||||
/**
|
||||
* Handle add new credential.
|
||||
*/
|
||||
const handleAddCredential = useCallback(() : void => {
|
||||
navigate('/credentials/add');
|
||||
}, [navigate]);
|
||||
|
||||
/**
|
||||
* Retrieve latest vault and refresh the credentials list.
|
||||
*/
|
||||
@@ -43,14 +56,12 @@ const CredentialsList: React.FC = () => {
|
||||
* On success.
|
||||
*/
|
||||
onSuccess: async (_hasNewVault) => {
|
||||
// Refresh credentials list, whether there is a new vault or not.
|
||||
const results = dbContext.sqliteClient?.getAllCredentials() ?? [];
|
||||
setCredentials(results);
|
||||
// Credentials list is refreshed automatically when the (new) sqlite client is available via useEffect hook below.
|
||||
},
|
||||
/**
|
||||
* On offline.
|
||||
*/
|
||||
onOffline: () => {
|
||||
_onOffline: () => {
|
||||
// Not implemented for browser extension yet.
|
||||
},
|
||||
/**
|
||||
@@ -76,6 +87,22 @@ const CredentialsList: React.FC = () => {
|
||||
hideLoading();
|
||||
};
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
<HeaderButton
|
||||
onClick={handleAddCredential}
|
||||
title="Add new credential"
|
||||
iconType={HeaderIconType.PLUS}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
return () => setHeaderButtons(null);
|
||||
}, [setHeaderButtons, handleAddCredential]);
|
||||
|
||||
/**
|
||||
* Load credentials list on mount and on sqlite client change.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/icons/HeaderIcons';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useTheme } from '@/entrypoints/popup/context/ThemeContext';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
@@ -28,6 +31,7 @@ type PopupSettings = {
|
||||
const Settings: React.FC = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const authContext = useAuth();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const [settings, setSettings] = useState<PopupSettings>({
|
||||
disabledUrls: [],
|
||||
temporaryDisabledUrls: {},
|
||||
@@ -46,6 +50,35 @@ const Settings: React.FC = () => {
|
||||
return tab;
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the client tab.
|
||||
*/
|
||||
const openClientTab = async () : Promise<void> => {
|
||||
const settingClientUrl = await storage.getItem('local:clientUrl') as string;
|
||||
let clientUrl = AppInfo.DEFAULT_CLIENT_URL;
|
||||
if (settingClientUrl && settingClientUrl.length > 0) {
|
||||
clientUrl = settingClientUrl;
|
||||
}
|
||||
|
||||
window.open(clientUrl, '_blank');
|
||||
};
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
<HeaderButton
|
||||
onClick={openClientTab}
|
||||
title="Open Client"
|
||||
iconType={HeaderIconType.EXTERNAL_LINK}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
return () => setHeaderButtons(null);
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
/**
|
||||
* Load settings.
|
||||
*/
|
||||
|
||||
@@ -341,9 +341,41 @@ export class SqliteClient {
|
||||
|
||||
/**
|
||||
* Get the default email domain from the database.
|
||||
* @param privateEmailDomains - Array of private email domains
|
||||
* @param publicEmailDomains - Array of public email domains
|
||||
* @returns The default email domain or null if no valid domain is found
|
||||
*/
|
||||
public getDefaultEmailDomain(): string {
|
||||
return this.getSetting('DefaultEmailDomain');
|
||||
public getDefaultEmailDomain(privateEmailDomains: string[], publicEmailDomains: string[]): string | null {
|
||||
const defaultEmailDomain = this.getSetting('DefaultEmailDomain');
|
||||
|
||||
/**
|
||||
* Check if a domain is valid.
|
||||
*/
|
||||
const isValidDomain = (domain: string): boolean => {
|
||||
return Boolean(domain &&
|
||||
domain !== 'DISABLED.TLD' &&
|
||||
(privateEmailDomains.includes(domain) || publicEmailDomains.includes(domain)));
|
||||
};
|
||||
|
||||
// First check if the default domain that is configured in the vault is still valid.
|
||||
if (defaultEmailDomain && isValidDomain(defaultEmailDomain)) {
|
||||
return defaultEmailDomain;
|
||||
}
|
||||
|
||||
// If default domain is not valid, fall back to first available private domain.
|
||||
const firstPrivate = privateEmailDomains.find(isValidDomain);
|
||||
if (firstPrivate) {
|
||||
return firstPrivate;
|
||||
}
|
||||
|
||||
// Return first valid public domain if no private domains are available.
|
||||
const firstPublic = publicEmailDomains.find(isValidDomain);
|
||||
if (firstPublic) {
|
||||
return firstPublic;
|
||||
}
|
||||
|
||||
// Return null if no valid domains are found
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -383,9 +415,9 @@ export class SqliteClient {
|
||||
/**
|
||||
* Create a new credential with associated entities
|
||||
* @param credential The credential object to insert
|
||||
* @returns The number of rows modified
|
||||
* @returns The ID of the created credential
|
||||
*/
|
||||
public async createCredential(credential: Credential): Promise<number> {
|
||||
public async createCredential(credential: Credential): Promise<string> {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
@@ -480,7 +512,7 @@ export class SqliteClient {
|
||||
}
|
||||
|
||||
await this.commitTransaction();
|
||||
return 1;
|
||||
return credentialId;
|
||||
|
||||
} catch (error) {
|
||||
this.rollbackTransaction();
|
||||
|
||||
Reference in New Issue
Block a user