Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48414dcae4 | ||
|
|
151548f6f7 | ||
|
|
fd5c8096ad | ||
|
|
09cfee2888 | ||
|
|
74cb2eae7d | ||
|
|
35b8f0abae | ||
|
|
08517e3469 | ||
|
|
f3dabc3a39 | ||
|
|
d98f047963 | ||
|
|
599966996e | ||
|
|
952cfd9a28 | ||
|
|
81a5155734 | ||
|
|
3a953ec7c8 | ||
|
|
392dbd626c | ||
|
|
b6d3f9e70f | ||
|
|
c2f2511f6a | ||
|
|
ce2e21900f | ||
|
|
660b286ee9 | ||
|
|
133037dcd8 | ||
|
|
03b65a63ba | ||
|
|
f7a8189b86 | ||
|
|
38973de6f1 | ||
|
|
9ddd00bfa4 | ||
|
|
88013161d1 | ||
|
|
b0da0d8590 | ||
|
|
7dcfd6bfd1 | ||
|
|
586b0a3495 | ||
|
|
30a009c5c4 | ||
|
|
7d73222ee1 | ||
|
|
6d191a1bd5 | ||
|
|
e5c68c6c6e | ||
|
|
58c39815e4 | ||
|
|
4b706f466f | ||
|
|
19f72b1386 |
2
.vscode/tasks.json
vendored
@@ -199,7 +199,7 @@
|
||||
{
|
||||
"label": "Build and watch Docs",
|
||||
"type": "shell",
|
||||
"command": "docker compose build && docker compose up",
|
||||
"command": "docker compose -f docker-compose.dev.yml build && docker compose -f docker-compose.dev.yml up",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
|
||||
17
SECURITY.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
Contact: mailto:security@support.aliasvault.net
|
||||
Expires: 2026-09-16T12:00:00.000Z
|
||||
Preferred-Languages: en
|
||||
Canonical: https://raw.githubusercontent.com/aliasvault/aliasvault/main/SECURITY.txt
|
||||
|
||||
# Security Policy for AliasVault
|
||||
#
|
||||
# We take security seriously and appreciate responsible disclosure of vulnerabilities.
|
||||
# Please report security issues to the email above rather than opening public issues.
|
||||
#
|
||||
# Include the following information in your report:
|
||||
# - Description of the vulnerability
|
||||
# - Steps to reproduce
|
||||
# - Potential impact
|
||||
# - Suggested remediation (if any)
|
||||
#
|
||||
# We will acknowledge receipt within 48 hours and provide updates as we investigate.
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "aliasvault-browser-extension",
|
||||
"description": "AliasVault Browser Extension",
|
||||
"private": true,
|
||||
"version": "0.23.0",
|
||||
"version": "0.23.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:chrome": "wxt -b chrome",
|
||||
|
||||
@@ -447,7 +447,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -460,7 +460,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.23.0;
|
||||
MARKETING_VERSION = 0.23.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -479,7 +479,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -492,7 +492,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.23.0;
|
||||
MARKETING_VERSION = 0.23.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -515,7 +515,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -530,7 +530,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.23.0;
|
||||
MARKETING_VERSION = 0.23.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -554,7 +554,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -569,7 +569,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.23.0;
|
||||
MARKETING_VERSION = 0.23.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { onMessage, sendMessage } from "webext-bridge/background";
|
||||
import { handleResetAutoLockTimer, handlePopupHeartbeat, handleSetAutoLockTimeout } from '@/entrypoints/background/AutolockTimeoutHandler';
|
||||
import { handleClipboardCopied, handleCancelClipboardClear, handleGetClipboardClearTimeout, handleSetClipboardClearTimeout, handleGetClipboardCountdownState } from '@/entrypoints/background/ClipboardClearHandler';
|
||||
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
|
||||
import { handleOpenPopup, handlePopupWithCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler';
|
||||
import { handleOpenPopup, handlePopupWithCredential, handleOpenPopupCreateCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler';
|
||||
import { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentitySettings, handleGetEncryptionKey, handleGetEncryptionKeyDerivationParams, handleGetPasswordSettings, handleGetPersistedFormValues, handleGetVault, handlePersistFormValues, handleStoreEncryptionKey, handleStoreEncryptionKeyDerivationParams, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler';
|
||||
|
||||
import { GLOBAL_CONTEXT_MENU_ENABLED_KEY } from '@/utils/Constants';
|
||||
@@ -40,6 +40,7 @@ export default defineBackground({
|
||||
|
||||
onMessage('OPEN_POPUP', () => handleOpenPopup());
|
||||
onMessage('OPEN_POPUP_WITH_CREDENTIAL', ({ data }) => handlePopupWithCredential(data));
|
||||
onMessage('OPEN_POPUP_CREATE_CREDENTIAL', ({ data }) => handleOpenPopupCreateCredential(data));
|
||||
onMessage('TOGGLE_CONTEXT_MENU', ({ data }) => handleToggleContextMenu(data));
|
||||
|
||||
onMessage('PERSIST_FORM_VALUES', ({ data }) => handlePersistFormValues(data));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
|
||||
|
||||
import { SKIP_FORM_RESTORE_KEY } from '@/utils/Constants';
|
||||
import { BoolResponse } from '@/utils/types/messaging/BoolResponse';
|
||||
|
||||
import { browser } from '#imports';
|
||||
@@ -37,6 +38,53 @@ export function handlePopupWithCredential(message: any) : Promise<BoolResponse>
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle opening the popup on create credential page with prefilled service name.
|
||||
*/
|
||||
export function handleOpenPopupCreateCredential(message: any) : Promise<BoolResponse> {
|
||||
return (async () : Promise<BoolResponse> => {
|
||||
const serviceName = encodeURIComponent(message.serviceName || '');
|
||||
|
||||
// Use the URL passed from the content script (current page URL)
|
||||
let serviceUrl = '';
|
||||
if (message.currentUrl) {
|
||||
try {
|
||||
const url = new URL(message.currentUrl);
|
||||
// Only include http/https URLs
|
||||
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
||||
serviceUrl = encodeURIComponent(url.origin + url.pathname);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing current URL:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Set a localStorage flag to skip restoring previously persisted form values as we want to start fresh with this explicit create credential request.
|
||||
await browser.storage.local.set({ [SKIP_FORM_RESTORE_KEY]: true });
|
||||
|
||||
const urlParams = new URLSearchParams();
|
||||
urlParams.set('expanded', 'true');
|
||||
if (serviceName) {
|
||||
urlParams.set('serviceName', serviceName);
|
||||
}
|
||||
if (serviceUrl) {
|
||||
urlParams.set('serviceUrl', serviceUrl);
|
||||
}
|
||||
if (message.currentUrl) {
|
||||
urlParams.set('currentUrl', message.currentUrl);
|
||||
}
|
||||
|
||||
browser.windows.create({
|
||||
url: browser.runtime.getURL(`/popup.html?${urlParams.toString()}#/credentials/add`),
|
||||
type: 'popup',
|
||||
width: 400,
|
||||
height: 600,
|
||||
focused: true
|
||||
});
|
||||
return { success: true };
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle toggling the context menu.
|
||||
*/
|
||||
|
||||
@@ -3,12 +3,12 @@ import { sendMessage } from 'webext-bridge/content-script';
|
||||
import { filterCredentials, AutofillMatchingMode } from '@/entrypoints/contentScript/Filter';
|
||||
import { fillCredential } from '@/entrypoints/contentScript/Form';
|
||||
|
||||
import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY, LAST_CUSTOM_EMAIL_KEY, LAST_CUSTOM_USERNAME_KEY, AUTOFILL_MATCHING_MODE_KEY } from '@/utils/Constants';
|
||||
import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY, AUTOFILL_MATCHING_MODE_KEY, CUSTOM_EMAIL_HISTORY_KEY, CUSTOM_USERNAME_HISTORY_KEY } from '@/utils/Constants';
|
||||
import { CreateIdentityGenerator } from '@/utils/dist/shared/identity-generator';
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import { CreatePasswordGenerator, PasswordGenerator, PasswordSettings } from '@/utils/dist/shared/password-generator';
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { ClickValidator } from '@/utils/security/ClickValidator';
|
||||
import { ServiceDetectionUtility } from '@/utils/serviceDetection/ServiceDetectionUtility';
|
||||
import { SqliteClient } from '@/utils/SqliteClient';
|
||||
import { CredentialsResponse } from '@/utils/types/messaging/CredentialsResponse';
|
||||
import { IdentitySettingsResponse } from '@/utils/types/messaging/IdentitySettingsResponse';
|
||||
@@ -227,8 +227,8 @@ export async function createAutofillPopup(input: HTMLInputElement, credentials:
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
const suggestedNames = FormDetector.getSuggestedServiceName(document, window.location);
|
||||
const result = await createAliasCreationPopup(suggestedNames, rootContainer);
|
||||
const serviceInfo = ServiceDetectionUtility.getServiceInfo(document, window.location);
|
||||
const result = await createAliasCreationPopup(serviceInfo.suggestedNames, rootContainer);
|
||||
|
||||
if (!result) {
|
||||
// User cancelled
|
||||
@@ -762,9 +762,9 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
// Close existing popup
|
||||
removeExistingPopup(rootContainer);
|
||||
|
||||
// Load last used values
|
||||
const lastEmail = await storage.getItem(LAST_CUSTOM_EMAIL_KEY) as string ?? '';
|
||||
const lastUsername = await storage.getItem(LAST_CUSTOM_USERNAME_KEY) as string ?? '';
|
||||
// Load history
|
||||
const emailHistory = await storage.getItem(CUSTOM_EMAIL_HISTORY_KEY) as string[] ?? [];
|
||||
const usernameHistory = await storage.getItem(CUSTOM_USERNAME_HISTORY_KEY) as string[] ?? [];
|
||||
|
||||
return new Promise((resolve) => {
|
||||
(async (): Promise<void> => {
|
||||
@@ -829,11 +829,20 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
${randomIdentityIcon}
|
||||
<h3 class="av-create-popup-title">${randomIdentityTitle}</h3>
|
||||
</div>
|
||||
<button class="av-create-popup-mode-dropdown">
|
||||
<svg class="av-icon" viewBox="0 0 24 24">
|
||||
<path d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="av-create-popup-header-buttons">
|
||||
<button class="av-create-popup-mode-dropdown">
|
||||
<svg class="av-icon" viewBox="0 0 24 24">
|
||||
<path d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="av-create-popup-popout" title="Open in main popup">
|
||||
<svg class="av-icon" viewBox="0 0 24 24">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
||||
<polyline points="15 3 21 3 21 9"></polyline>
|
||||
<line x1="10" y1="14" x2="21" y2="3"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -888,8 +897,8 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
id="custom-email"
|
||||
class="av-create-popup-input"
|
||||
placeholder="${enterEmailAddressText}"
|
||||
data-default-value="${lastEmail}"
|
||||
>
|
||||
<div class="av-field-suggestions" id="email-suggestions"></div>
|
||||
</div>
|
||||
<div class="av-create-popup-field-group">
|
||||
<label for="custom-username">${usernameText}</label>
|
||||
@@ -898,8 +907,8 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
id="custom-username"
|
||||
class="av-create-popup-input"
|
||||
placeholder="${enterUsernameText}"
|
||||
data-default-value="${lastUsername}"
|
||||
>
|
||||
<div class="av-field-suggestions" id="username-suggestions"></div>
|
||||
</div>
|
||||
<div class="av-create-popup-field-group">
|
||||
<label>${passwordText}</label>
|
||||
@@ -960,6 +969,7 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
const customMode = popup.querySelector('.av-create-popup-custom-mode') as HTMLElement;
|
||||
const dropdownMenu = popup.querySelector('.av-create-popup-mode-dropdown-menu') as HTMLElement;
|
||||
const titleContainer = popup.querySelector('.av-create-popup-title-container') as HTMLElement;
|
||||
const popoutBtn = popup.querySelector('.av-create-popup-popout') as HTMLButtonElement;
|
||||
const cancelBtn = popup.querySelector('#cancel-btn') as HTMLButtonElement;
|
||||
const customCancelBtn = popup.querySelector('#custom-cancel-btn') as HTMLButtonElement;
|
||||
const saveBtn = popup.querySelector('#save-btn') as HTMLButtonElement;
|
||||
@@ -970,41 +980,154 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
const passwordPreview = popup.querySelector('#password-preview') as HTMLInputElement;
|
||||
const regenerateBtn = popup.querySelector('#regenerate-password') as HTMLButtonElement;
|
||||
const toggleVisibilityBtn = popup.querySelector('#toggle-password-visibility') as HTMLButtonElement;
|
||||
const emailSuggestions = popup.querySelector('#email-suggestions') as HTMLElement;
|
||||
const usernameSuggestions = popup.querySelector('#username-suggestions') as HTMLElement;
|
||||
|
||||
/**
|
||||
* Setup default value for input with placeholder styling.
|
||||
* Update history with new value (max 2 unique entries)
|
||||
*/
|
||||
const setupDefaultValue = (input: HTMLInputElement) : void => {
|
||||
const defaultValue = input.dataset.defaultValue;
|
||||
if (defaultValue) {
|
||||
input.value = defaultValue;
|
||||
input.classList.add('av-create-popup-input-default');
|
||||
const updateHistory = async (value: string, historyKey: typeof CUSTOM_EMAIL_HISTORY_KEY | typeof CUSTOM_USERNAME_HISTORY_KEY, maxItems: number = 2): Promise<string[]> => {
|
||||
const history = await storage.getItem(historyKey) as string[] ?? [];
|
||||
|
||||
// Remove the value if it already exists
|
||||
const filteredHistory = history.filter((item: string) => item !== value);
|
||||
|
||||
// Add the new value at the beginning
|
||||
if (value.trim()) {
|
||||
filteredHistory.unshift(value);
|
||||
}
|
||||
|
||||
// Keep only the first maxItems
|
||||
const updatedHistory = filteredHistory.slice(0, maxItems);
|
||||
|
||||
// Save the updated history
|
||||
await storage.setItem(historyKey, updatedHistory);
|
||||
|
||||
return updatedHistory;
|
||||
};
|
||||
|
||||
setupDefaultValue(customEmail);
|
||||
setupDefaultValue(customUsername);
|
||||
/**
|
||||
* Remove item from history
|
||||
*/
|
||||
const removeFromHistory = async (value: string, historyKey: typeof CUSTOM_EMAIL_HISTORY_KEY | typeof CUSTOM_USERNAME_HISTORY_KEY): Promise<string[]> => {
|
||||
const history = await storage.getItem(historyKey) as string[] ?? [];
|
||||
const updatedHistory = history.filter((item: string) => item !== value);
|
||||
await storage.setItem(historyKey, updatedHistory);
|
||||
return updatedHistory;
|
||||
};
|
||||
|
||||
// Handle input changes
|
||||
customEmail.addEventListener('input', () => {
|
||||
const value = customEmail.value.trim();
|
||||
if (value || value === '') {
|
||||
customEmail.classList.remove('av-create-popup-input-default');
|
||||
storage.setItem(LAST_CUSTOM_EMAIL_KEY, value);
|
||||
/**
|
||||
* Format suggestions HTML as pill-style buttons
|
||||
*/
|
||||
const formatSuggestionsHtml = async (history: string[], currentValue: string): Promise<string> => {
|
||||
// Filter out the current value from history and limit to 2 items
|
||||
const filteredHistory = history
|
||||
.filter(item => item.toLowerCase() !== currentValue.toLowerCase())
|
||||
.slice(0, 2);
|
||||
|
||||
if (filteredHistory.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Build HTML with pill-style buttons
|
||||
return filteredHistory.map(item =>
|
||||
`<span class="av-suggestion-pill">
|
||||
<span class="av-suggestion-pill-text" data-value="${item}">${item}</span>
|
||||
<span class="av-suggestion-pill-delete" data-value="${item}" title="Remove">×</span>
|
||||
</span>`
|
||||
).join(' ');
|
||||
};
|
||||
|
||||
/**
|
||||
* Update suggestions display
|
||||
*/
|
||||
const updateSuggestions = async (input: HTMLInputElement, suggestionsContainer: HTMLElement, history: string[]): Promise<void> => {
|
||||
const currentValue = input.value.trim();
|
||||
const html = await formatSuggestionsHtml(history, currentValue);
|
||||
suggestionsContainer.innerHTML = html;
|
||||
suggestionsContainer.style.display = html ? 'flex' : 'none';
|
||||
};
|
||||
|
||||
// Initial display of suggestions
|
||||
await updateSuggestions(customEmail, emailSuggestions, emailHistory);
|
||||
await updateSuggestions(customUsername, usernameSuggestions, usernameHistory);
|
||||
|
||||
// Handle popout button click
|
||||
popoutBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const serviceName = inputServiceName.value.trim();
|
||||
const encodedServiceInfo = ServiceDetectionUtility.getEncodedServiceInfo(document, window.location);
|
||||
sendMessage('OPEN_POPUP_CREATE_CREDENTIAL', {
|
||||
serviceName: serviceName || encodedServiceInfo.serviceName,
|
||||
currentUrl: encodedServiceInfo.currentUrl
|
||||
}, 'background');
|
||||
closePopup(null);
|
||||
});
|
||||
|
||||
// Handle email input
|
||||
customEmail.addEventListener('input', async () => {
|
||||
await updateSuggestions(customEmail, emailSuggestions, emailHistory);
|
||||
});
|
||||
|
||||
// Handle username input
|
||||
customUsername.addEventListener('input', async () => {
|
||||
await updateSuggestions(customUsername, usernameSuggestions, usernameHistory);
|
||||
});
|
||||
|
||||
// Handle suggestion clicks for email
|
||||
emailSuggestions.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Check if delete button was clicked
|
||||
if (target.classList.contains('av-suggestion-pill-delete')) {
|
||||
const value = target.dataset.value;
|
||||
if (value) {
|
||||
const updatedHistory = await removeFromHistory(value, CUSTOM_EMAIL_HISTORY_KEY);
|
||||
emailHistory.splice(0, emailHistory.length, ...updatedHistory);
|
||||
await updateSuggestions(customEmail, emailSuggestions, emailHistory);
|
||||
}
|
||||
} else {
|
||||
customEmail.classList.add('av-create-popup-input-default');
|
||||
storage.setItem(LAST_CUSTOM_EMAIL_KEY, '');
|
||||
// Check if pill or pill text was clicked
|
||||
let pillElement = target.closest('.av-suggestion-pill') as HTMLElement;
|
||||
if (pillElement) {
|
||||
const textElement = pillElement.querySelector('.av-suggestion-pill-text') as HTMLElement;
|
||||
const value = textElement?.dataset.value;
|
||||
if (value) {
|
||||
customEmail.value = value;
|
||||
await updateSuggestions(customEmail, emailSuggestions, emailHistory);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
customUsername.addEventListener('input', () => {
|
||||
const value = customUsername.value.trim();
|
||||
if (value || value === '') {
|
||||
customUsername.classList.remove('av-create-popup-input-default');
|
||||
storage.setItem(LAST_CUSTOM_USERNAME_KEY, value);
|
||||
// Handle suggestion clicks for username
|
||||
usernameSuggestions.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Check if delete button was clicked
|
||||
if (target.classList.contains('av-suggestion-pill-delete')) {
|
||||
const value = target.dataset.value;
|
||||
if (value) {
|
||||
const updatedHistory = await removeFromHistory(value, CUSTOM_USERNAME_HISTORY_KEY);
|
||||
usernameHistory.splice(0, usernameHistory.length, ...updatedHistory);
|
||||
await updateSuggestions(customUsername, usernameSuggestions, usernameHistory);
|
||||
}
|
||||
} else {
|
||||
customUsername.classList.add('av-create-popup-input-default');
|
||||
storage.setItem(LAST_CUSTOM_USERNAME_KEY, '');
|
||||
// Check if pill or pill text was clicked
|
||||
let pillElement = target.closest('.av-suggestion-pill') as HTMLElement;
|
||||
if (pillElement) {
|
||||
const textElement = pillElement.querySelector('.av-suggestion-pill-text') as HTMLElement;
|
||||
const value = textElement?.dataset.value;
|
||||
if (value) {
|
||||
customUsername.value = value;
|
||||
await updateSuggestions(customUsername, usernameSuggestions, usernameHistory);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1372,12 +1495,8 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
if (serviceName) {
|
||||
const email = customEmail.value.trim();
|
||||
const username = customUsername.value.trim();
|
||||
const hasDefaultEmail = customEmail.classList.contains('av-create-popup-input-default');
|
||||
const hasDefaultUsername = customUsername.classList.contains('av-create-popup-input-default');
|
||||
|
||||
// If using default values, use the dataset values
|
||||
const finalEmail = hasDefaultEmail ? customEmail.dataset.defaultValue : email;
|
||||
const finalUsername = hasDefaultUsername ? customUsername.dataset.defaultValue : username;
|
||||
const finalEmail = email;
|
||||
const finalUsername = username;
|
||||
|
||||
if (!finalEmail && !finalUsername) {
|
||||
// Add error styling to fields
|
||||
@@ -1424,6 +1543,14 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
return;
|
||||
}
|
||||
|
||||
// Update history when saving
|
||||
if (finalEmail) {
|
||||
await updateHistory(finalEmail, CUSTOM_EMAIL_HISTORY_KEY);
|
||||
}
|
||||
if (finalUsername) {
|
||||
await updateHistory(finalUsername, CUSTOM_USERNAME_HISTORY_KEY);
|
||||
}
|
||||
|
||||
closePopup({
|
||||
serviceName,
|
||||
isCustomCredential: true,
|
||||
|
||||
@@ -539,6 +539,62 @@ body {
|
||||
box-shadow: 0 0 0 1px #ef4444 !important;
|
||||
}
|
||||
|
||||
/* Field Suggestions - Pill Style */
|
||||
.av-field-suggestions {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.av-suggestion-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: #4b5563;
|
||||
border: 1px solid #6b7280;
|
||||
border-radius: 16px;
|
||||
padding: 4px 8px 4px 12px;
|
||||
font-size: 13px;
|
||||
color: #e5e7eb;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.av-suggestion-pill:hover {
|
||||
background: #6b7280;
|
||||
border-color: #9ca3af;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.av-suggestion-pill-text {
|
||||
display: inline-block;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.av-suggestion-pill-delete {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 6px;
|
||||
padding: 0 2px;
|
||||
color: #9ca3af;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
border-left: 1px solid #6b7280;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.av-suggestion-pill-delete:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.av-create-popup-error-text {
|
||||
color: #ef4444;
|
||||
font-size: 0.875rem;
|
||||
@@ -728,28 +784,41 @@ body {
|
||||
.av-create-popup-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.av-create-popup-title-wrapper {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: #d68338;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.av-create-popup-header-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
|
||||
.av-create-popup-title-wrapper .av-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke-width: 1.5;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.av-create-popup-title-wrapper .av-create-popup-title {
|
||||
@@ -757,6 +826,7 @@ body {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #f8f9fa;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.av-create-popup-title-container:hover {
|
||||
@@ -785,6 +855,34 @@ body {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.av-create-popup-popout {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
color: #9ca3af;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.av-create-popup-popout:hover {
|
||||
background-color: #4b5563;
|
||||
color: #d68338;
|
||||
}
|
||||
|
||||
.av-create-popup-popout .av-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-dropdown-menu {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
|
||||
@@ -90,7 +90,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
setIsCustomDomain(!isKnownDomain);
|
||||
} else {
|
||||
setLocalPart(value);
|
||||
setIsCustomDomain(false);
|
||||
// Don't reset isCustomDomain here - preserve the current mode
|
||||
|
||||
// Set default domain if not already set
|
||||
if (!selectedDomain && !value.includes('@')) {
|
||||
@@ -107,6 +107,13 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
const handleLocalPartChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newLocalPart = e.target.value;
|
||||
|
||||
// If in custom domain mode, always pass through the full value
|
||||
if (isCustomDomain) {
|
||||
onChange(newLocalPart);
|
||||
// Stay in custom domain mode - don't auto-switch back
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if new value contains '@' symbol, if so, switch to custom domain mode
|
||||
if (newLocalPart.includes('@')) {
|
||||
setIsCustomDomain(true);
|
||||
@@ -115,10 +122,11 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
}
|
||||
|
||||
setLocalPart(newLocalPart);
|
||||
if (!isCustomDomain && selectedDomain) {
|
||||
// If the local part is empty, treat the whole field as empty
|
||||
if (!newLocalPart || newLocalPart.trim() === '') {
|
||||
onChange('');
|
||||
} else if (selectedDomain) {
|
||||
onChange(`${newLocalPart}@${selectedDomain}`);
|
||||
} else {
|
||||
onChange(newLocalPart);
|
||||
}
|
||||
}, [isCustomDomain, selectedDomain, onChange]);
|
||||
|
||||
@@ -126,7 +134,12 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
const selectDomain = useCallback((domain: string) => {
|
||||
setSelectedDomain(domain);
|
||||
const cleanLocalPart = localPart.includes('@') ? localPart.split('@')[0] : localPart;
|
||||
onChange(`${cleanLocalPart}@${domain}`);
|
||||
// If the local part is empty, treat the whole field as empty
|
||||
if (!cleanLocalPart || cleanLocalPart.trim() === '') {
|
||||
onChange('');
|
||||
} else {
|
||||
onChange(`${cleanLocalPart}@${domain}`);
|
||||
}
|
||||
setIsCustomDomain(false);
|
||||
setIsPopupVisible(false);
|
||||
}, [localPart, onChange]);
|
||||
@@ -136,13 +149,30 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
const newIsCustom = !isCustomDomain;
|
||||
setIsCustomDomain(newIsCustom);
|
||||
|
||||
if (!newIsCustom && !value.includes('@')) {
|
||||
// Switching to domain chooser mode, add default domain
|
||||
if (newIsCustom) {
|
||||
/*
|
||||
* Switching to custom domain mode
|
||||
* If we have a domain-based value, extract just the local part
|
||||
*/
|
||||
if (value && value.includes('@')) {
|
||||
const [local] = value.split('@');
|
||||
onChange(local);
|
||||
setLocalPart(local);
|
||||
}
|
||||
} else {
|
||||
// Switching to domain chooser mode
|
||||
const defaultDomain = showPrivateDomains && privateEmailDomains[0]
|
||||
? privateEmailDomains[0]
|
||||
: PUBLIC_EMAIL_DOMAINS[0];
|
||||
onChange(`${localPart}@${defaultDomain}`);
|
||||
setSelectedDomain(defaultDomain);
|
||||
|
||||
// Only add domain if we have a local part
|
||||
if (localPart && localPart.trim()) {
|
||||
onChange(`${localPart}@${defaultDomain}`);
|
||||
} else if (value && !value.includes('@')) {
|
||||
// If we have a value without @, add the domain
|
||||
onChange(`${value}@${defaultDomain}`);
|
||||
}
|
||||
}
|
||||
}, [isCustomDomain, value, localPart, showPrivateDomains, privateEmailDomains, onChange]);
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
import type { PasswordSettings } from '@/utils/dist/shared/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
|
||||
@@ -15,7 +17,6 @@ interface IPasswordFieldProps {
|
||||
error?: string;
|
||||
showPassword?: boolean;
|
||||
onShowPasswordChange?: (show: boolean) => void;
|
||||
initialSettings: PasswordSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,13 +30,14 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
|
||||
placeholder,
|
||||
error,
|
||||
showPassword: controlledShowPassword,
|
||||
onShowPasswordChange,
|
||||
initialSettings
|
||||
onShowPasswordChange
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dbContext = useDb();
|
||||
const [internalShowPassword, setInternalShowPassword] = useState(false);
|
||||
const [showConfigDialog, setShowConfigDialog] = useState(false);
|
||||
const [currentSettings, setCurrentSettings] = useState<PasswordSettings>(initialSettings);
|
||||
const [currentSettings, setCurrentSettings] = useState<PasswordSettings | null>(null);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
// Use controlled or uncontrolled showPassword state
|
||||
const showPassword = controlledShowPassword !== undefined ? controlledShowPassword : internalShowPassword;
|
||||
@@ -51,11 +53,24 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
|
||||
}
|
||||
}, [controlledShowPassword, onShowPasswordChange]);
|
||||
|
||||
// Initialize settings only once when component mounts
|
||||
// Load password settings from database
|
||||
useEffect(() => {
|
||||
setCurrentSettings({ ...initialSettings });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); // Only run on mount to avoid resetting user changes
|
||||
/**
|
||||
* Load password settings from the database.
|
||||
*/
|
||||
const loadSettings = async (): Promise<void> => {
|
||||
try {
|
||||
if (dbContext.sqliteClient) {
|
||||
const settings = dbContext.sqliteClient.getPasswordSettings();
|
||||
setCurrentSettings(settings);
|
||||
setIsLoaded(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading password settings:', error);
|
||||
}
|
||||
};
|
||||
void loadSettings();
|
||||
}, [dbContext.sqliteClient]);
|
||||
|
||||
const generatePassword = useCallback((settings: PasswordSettings) => {
|
||||
try {
|
||||
@@ -69,6 +84,9 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
|
||||
}, [onChange, setShowPassword]);
|
||||
|
||||
const handleLengthChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!currentSettings) {
|
||||
return;
|
||||
}
|
||||
const length = parseInt(e.target.value, 10);
|
||||
const newSettings = { ...currentSettings, Length: length };
|
||||
setCurrentSettings(newSettings);
|
||||
@@ -78,6 +96,9 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
|
||||
}, [currentSettings, generatePassword]);
|
||||
|
||||
const handleRegeneratePassword = useCallback(() => {
|
||||
if (!currentSettings) {
|
||||
return;
|
||||
}
|
||||
generatePassword(currentSettings);
|
||||
}, [generatePassword, currentSettings]);
|
||||
|
||||
@@ -98,6 +119,18 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
|
||||
setShowConfigDialog(true);
|
||||
}, []);
|
||||
|
||||
// Don't render until settings are loaded
|
||||
if (!currentSettings || !isLoaded) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label htmlFor={id} className="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
{label}
|
||||
</label>
|
||||
<div className="animate-pulse bg-gray-200 dark:bg-gray-700 h-10 rounded-lg"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Label */}
|
||||
|
||||
@@ -23,9 +23,13 @@ import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
|
||||
|
||||
import { SKIP_FORM_RESTORE_KEY } from '@/utils/Constants';
|
||||
import { IdentityHelperUtils, CreateIdentityGenerator, CreateUsernameEmailGenerator, Identity, Gender } from '@/utils/dist/shared/identity-generator';
|
||||
import type { Attachment, Credential } from '@/utils/dist/shared/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
import { ServiceDetectionUtility } from '@/utils/serviceDetection/ServiceDetectionUtility';
|
||||
|
||||
import { browser } from '#imports';
|
||||
|
||||
type CredentialMode = 'random' | 'manual';
|
||||
|
||||
@@ -90,6 +94,13 @@ const CredentialAddEdit: React.FC = () => {
|
||||
const [originalAttachmentIds, setOriginalAttachmentIds] = useState<string[]>([]);
|
||||
const webApi = useWebApi();
|
||||
|
||||
// Track last generated values to avoid overwriting manual entries
|
||||
const [lastGeneratedValues, setLastGeneratedValues] = useState<{
|
||||
username: string | null;
|
||||
password: string | null;
|
||||
email: string | null;
|
||||
}>({ username: null, password: null, email: null });
|
||||
|
||||
const serviceNameRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { handleSubmit, setValue, watch, formState: { errors } } = useForm<Credential>({
|
||||
@@ -223,20 +234,80 @@ const CredentialAddEdit: React.FC = () => {
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
// On create mode, focus the service name field after a short delay to ensure the component is mounted.
|
||||
// On create mode, check for URL parameters first, then fallback to tab detection
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const serviceName = urlParams.get('serviceName');
|
||||
const serviceUrl = urlParams.get('serviceUrl');
|
||||
const currentUrl = urlParams.get('currentUrl');
|
||||
|
||||
/**
|
||||
* Initialize service detection from URL parameters or current tab
|
||||
*/
|
||||
const initializeServiceDetection = async (): Promise<void> => {
|
||||
try {
|
||||
// If URL parameters are present (e.g., from content script popout), use them
|
||||
if (serviceName || serviceUrl || currentUrl) {
|
||||
if (serviceName) {
|
||||
setValue('ServiceName', decodeURIComponent(serviceName));
|
||||
}
|
||||
if (serviceUrl) {
|
||||
setValue('ServiceUrl', decodeURIComponent(serviceUrl));
|
||||
}
|
||||
|
||||
// If we have currentUrl but missing serviceName or serviceUrl, derive them
|
||||
if (currentUrl && (!serviceName || !serviceUrl)) {
|
||||
const decodedCurrentUrl = decodeURIComponent(currentUrl);
|
||||
const serviceInfo = ServiceDetectionUtility.getServiceInfoFromTab(decodedCurrentUrl);
|
||||
|
||||
if (!serviceName && serviceInfo.suggestedNames.length > 0) {
|
||||
setValue('ServiceName', serviceInfo.suggestedNames[0]);
|
||||
}
|
||||
if (!serviceUrl && serviceInfo.serviceUrl) {
|
||||
setValue('ServiceUrl', serviceInfo.serviceUrl);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, detect from current active tab (for dashboard case)
|
||||
const [activeTab] = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
if (activeTab?.url) {
|
||||
const serviceInfo = ServiceDetectionUtility.getServiceInfoFromTab(
|
||||
activeTab.url,
|
||||
activeTab.title
|
||||
);
|
||||
|
||||
if (serviceInfo.suggestedNames.length > 0) {
|
||||
setValue('ServiceName', serviceInfo.suggestedNames[0]);
|
||||
}
|
||||
if (serviceInfo.serviceUrl) {
|
||||
setValue('ServiceUrl', serviceInfo.serviceUrl);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error detecting service information:', error);
|
||||
}
|
||||
};
|
||||
|
||||
initializeServiceDetection();
|
||||
|
||||
// Focus the service name field after a short delay to ensure the component is mounted.
|
||||
setTimeout(() => {
|
||||
serviceNameRef.current?.focus();
|
||||
}, 100);
|
||||
setIsInitialLoading(false);
|
||||
|
||||
// Load persisted form values if they exist.
|
||||
loadPersistedValues().then(() => {
|
||||
// Generate default password if no persisted password exists
|
||||
if (!watch('Password')) {
|
||||
const passwordSettings = dbContext.sqliteClient!.getPasswordSettings();
|
||||
const passwordGenerator = CreatePasswordGenerator(passwordSettings);
|
||||
const defaultPassword = passwordGenerator.generateRandomPassword();
|
||||
setValue('Password', defaultPassword);
|
||||
// Check if we should skip form restoration (e.g., when opened from popout button)
|
||||
browser.storage.local.get([SKIP_FORM_RESTORE_KEY]).then((result) => {
|
||||
if (result[SKIP_FORM_RESTORE_KEY]) {
|
||||
// Clear the flag after using it
|
||||
browser.storage.local.remove([SKIP_FORM_RESTORE_KEY]);
|
||||
// Don't load persisted values, but set local loading to false
|
||||
setLocalLoading(false);
|
||||
} else {
|
||||
// Load persisted form values normally
|
||||
loadPersistedValues();
|
||||
}
|
||||
});
|
||||
return;
|
||||
@@ -271,7 +342,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
console.error('Error loading credential:', err);
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue, loadPersistedValues, watch]);
|
||||
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue, loadPersistedValues, clearPersistedValues]);
|
||||
|
||||
/**
|
||||
* Handle the delete button click.
|
||||
@@ -331,35 +402,63 @@ const CredentialAddEdit: React.FC = () => {
|
||||
const defaultEmailDomain = dbContext.sqliteClient!.getDefaultEmailDomain(privateEmailDomains, publicEmailDomains);
|
||||
const email = defaultEmailDomain ? `${identity.emailPrefix}@${defaultEmailDomain}` : identity.emailPrefix;
|
||||
|
||||
setValue('Alias.Email', email);
|
||||
// Check current values
|
||||
const currentUsername = watch('Username') ?? '';
|
||||
const currentPassword = watch('Password') ?? '';
|
||||
const currentEmail = watch('Alias.Email') ?? '';
|
||||
|
||||
// Only overwrite email if it's empty or matches the last generated value
|
||||
if (!currentEmail || currentEmail === lastGeneratedValues.email) {
|
||||
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
|
||||
// Only overwrite username if it's empty or matches the last generated value
|
||||
if (!currentUsername || currentUsername === lastGeneratedValues.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
|
||||
// Only overwrite password if it's empty or matches the last generated value
|
||||
if (!currentPassword || currentPassword === lastGeneratedValues.password) {
|
||||
setValue('Password', password);
|
||||
}
|
||||
}, [isEditMode, watch, setValue, initializeGenerators, dbContext]);
|
||||
|
||||
// Update tracking with new generated values
|
||||
setLastGeneratedValues({
|
||||
username: identity.nickName,
|
||||
password: password,
|
||||
email: email
|
||||
});
|
||||
}, [watch, setValue, initializeGenerators, dbContext, lastGeneratedValues, setLastGeneratedValues]);
|
||||
|
||||
/**
|
||||
* Clear all alias fields.
|
||||
*/
|
||||
const clearAliasFields = useCallback(() => {
|
||||
setValue('Alias.FirstName', '');
|
||||
setValue('Alias.LastName', '');
|
||||
setValue('Alias.NickName', '');
|
||||
setValue('Alias.Gender', '');
|
||||
setValue('Alias.BirthDate', '');
|
||||
}, [setValue]);
|
||||
|
||||
// Check if any alias fields have values.
|
||||
const hasAliasValues = !!(watch('Alias.FirstName') || watch('Alias.LastName') || watch('Alias.NickName') || watch('Alias.Gender') || watch('Alias.BirthDate'));
|
||||
|
||||
/**
|
||||
* Handle the generate random alias button press.
|
||||
*/
|
||||
const handleGenerateRandomAlias = useCallback(() => {
|
||||
void generateRandomAlias();
|
||||
}, [generateRandomAlias]);
|
||||
if (hasAliasValues) {
|
||||
clearAliasFields();
|
||||
} else {
|
||||
void generateRandomAlias();
|
||||
}
|
||||
}, [generateRandomAlias, clearAliasFields, hasAliasValues]);
|
||||
|
||||
const generateRandomUsername = useCallback(async () => {
|
||||
try {
|
||||
@@ -382,15 +481,17 @@ const CredentialAddEdit: React.FC = () => {
|
||||
};
|
||||
|
||||
const username = usernameEmailGenerator.generateUsername(identity);
|
||||
setValue('Username', username);
|
||||
const currentUsername = watch('Username') ?? '';
|
||||
// Only overwrite username if it's empty or matches the last generated value
|
||||
if (!currentUsername || currentUsername === lastGeneratedValues.username) {
|
||||
setValue('Username', username);
|
||||
// Update the tracking for username
|
||||
setLastGeneratedValues(prev => ({ ...prev, username: username }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating random username:', error);
|
||||
}
|
||||
}, [setValue, watch]);
|
||||
|
||||
const initialPasswordSettings = useMemo(() => {
|
||||
return dbContext.sqliteClient?.getPasswordSettings();
|
||||
}, [dbContext.sqliteClient]);
|
||||
}, [setValue, watch, lastGeneratedValues, setLastGeneratedValues]);
|
||||
|
||||
/**
|
||||
* Handle form submission.
|
||||
@@ -609,18 +710,15 @@ const CredentialAddEdit: React.FC = () => {
|
||||
error={errors.Username?.message}
|
||||
onRegenerate={generateRandomUsername}
|
||||
/>
|
||||
{initialPasswordSettings && (
|
||||
<PasswordField
|
||||
id="password"
|
||||
label={t('common.password')}
|
||||
value={watch('Password') ?? ''}
|
||||
onChange={(value) => setValue('Password', value)}
|
||||
error={errors.Password?.message}
|
||||
showPassword={showPassword}
|
||||
onShowPasswordChange={setShowPassword}
|
||||
initialSettings={initialPasswordSettings}
|
||||
/>
|
||||
)}
|
||||
<PasswordField
|
||||
id="password"
|
||||
label={t('common.password')}
|
||||
value={watch('Password') ?? ''}
|
||||
onChange={(value) => setValue('Password', value)}
|
||||
error={errors.Password?.message}
|
||||
showPassword={showPassword}
|
||||
onShowPasswordChange={setShowPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -630,17 +728,33 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerateRandomAlias}
|
||||
className="w-full text-sm 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 flex items-center justify-center gap-2"
|
||||
className={`w-full text-sm py-2 px-4 rounded focus:outline-none focus:ring-2 focus:ring-offset-2 flex items-center justify-center gap-2 ${
|
||||
hasAliasValues
|
||||
? 'bg-gray-500 text-white hover:bg-gray-600 focus:ring-gray-500'
|
||||
: 'bg-primary-500 text-white hover:bg-primary-600 focus:ring-primary-500'
|
||||
}`}
|
||||
>
|
||||
<svg className='w-5 h-5 inline-block' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
<circle cx="8" cy="8" r="1"/>
|
||||
<circle cx="16" cy="8" r="1"/>
|
||||
<circle cx="12" cy="12" r="1"/>
|
||||
<circle cx="8" cy="16" r="1"/>
|
||||
<circle cx="16" cy="16" r="1"/>
|
||||
</svg>
|
||||
<span>{t('credentials.generateRandomAlias')}</span>
|
||||
{hasAliasValues ? (
|
||||
<>
|
||||
<svg className='w-5 h-5 inline-block' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
<span>{t('credentials.clearAliasFields')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className='w-5 h-5 inline-block' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
<circle cx="8" cy="8" r="1"/>
|
||||
<circle cx="16" cy="8" r="1"/>
|
||||
<circle cx="12" cy="12" r="1"/>
|
||||
<circle cx="8" cy="16" r="1"/>
|
||||
<circle cx="16" cy="16" r="1"/>
|
||||
</svg>
|
||||
<span>{t('credentials.generateRandomAlias')}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<FormInput
|
||||
id="firstName"
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
import deTranslations from './locales/de.json';
|
||||
import enTranslations from './locales/en.json';
|
||||
import fiTranslations from './locales/fi.json';
|
||||
import heTranslations from './locales/he.json';
|
||||
import itTranslations from './locales/it.json';
|
||||
import nlTranslations from './locales/nl.json';
|
||||
import ukTranslations from './locales/uk.json';
|
||||
import zhTranslations from './locales/zh.json';
|
||||
|
||||
/**
|
||||
@@ -24,12 +26,18 @@ export const LANGUAGE_RESOURCES = {
|
||||
fi: {
|
||||
translation: fiTranslations
|
||||
},
|
||||
he: {
|
||||
translation: heTranslations
|
||||
},
|
||||
it: {
|
||||
translation: itTranslations
|
||||
},
|
||||
nl: {
|
||||
translation: nlTranslations
|
||||
},
|
||||
uk: {
|
||||
translation: ukTranslations
|
||||
},
|
||||
zh: {
|
||||
translation: zhTranslations
|
||||
},
|
||||
@@ -58,6 +66,12 @@ export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
|
||||
nativeName: 'Suomi',
|
||||
flag: '🇫🇮'
|
||||
},
|
||||
{
|
||||
code: 'he',
|
||||
name: 'Hebrew',
|
||||
nativeName: 'עברית',
|
||||
flag: '🇮🇱'
|
||||
},
|
||||
{
|
||||
code: 'it',
|
||||
name: 'Italian',
|
||||
@@ -70,6 +84,12 @@ export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
|
||||
nativeName: 'Nederlands',
|
||||
flag: '🇳🇱'
|
||||
},
|
||||
{
|
||||
code: 'uk',
|
||||
name: 'Ukrainian',
|
||||
nativeName: 'Українська',
|
||||
flag: '🇺🇦'
|
||||
},
|
||||
{
|
||||
code: 'zh',
|
||||
name: 'Chinese',
|
||||
@@ -77,12 +97,6 @@ export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
|
||||
flag: '🇨🇳'
|
||||
},
|
||||
/*
|
||||
* {
|
||||
* code: 'de',
|
||||
* name: 'German',
|
||||
* nativeName: 'Deutsch',
|
||||
* flag: '🇩🇪'
|
||||
* },
|
||||
* {
|
||||
* code: 'es',
|
||||
* name: 'Spanish',
|
||||
@@ -95,12 +109,6 @@ export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
|
||||
* nativeName: 'Français',
|
||||
* flag: '🇫🇷'
|
||||
* },
|
||||
* {
|
||||
* code: 'uk',
|
||||
* name: 'Ukrainian',
|
||||
* nativeName: 'Українська',
|
||||
* flag: '🇺🇦'
|
||||
* }
|
||||
*/
|
||||
];
|
||||
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "Mehrdeutige Zeichen (1, l, I, 0, O, etc.) vermeiden",
|
||||
"generateNewPreview": "Neue Vorschau erstellen",
|
||||
"generateRandomAlias": "Zufälligen Alias generieren",
|
||||
"clearAliasFields": "Alias-Felder löschen",
|
||||
"alias": "Alias",
|
||||
"firstName": "Vorname",
|
||||
"lastName": "Nachname",
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "Vältä epäselviä merkkejä (o, 0 jne.)",
|
||||
"generateNewPreview": "Luo uusi esikatselu",
|
||||
"generateRandomAlias": "Luo sattumanvarainen alias",
|
||||
"clearAliasFields": "Tyhjennä aliaksen kentät",
|
||||
"alias": "Alias",
|
||||
"firstName": "Etunimi",
|
||||
"lastName": "Sukunimi",
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "Éviter les caractères ambigus (o, 0, etc.)",
|
||||
"generateNewPreview": "Générer un nouvel aperçu",
|
||||
"generateRandomAlias": "Créer un alias aléatoire",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "Alias",
|
||||
"firstName": "Prénom",
|
||||
"lastName": "Nom",
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "עדיף להימנע מאותיות וספרות שדומים זה לזה (o, 0 וכו׳)",
|
||||
"generateNewPreview": "יצירת תצוגה מקדימה חדשה",
|
||||
"generateRandomAlias": "יצירת כינוי אקראי",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "כינוי",
|
||||
"firstName": "שם פרטי",
|
||||
"lastName": "שם משפחה",
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "Evita caratteri ambigui (o, 0, ecc.)",
|
||||
"generateNewPreview": "Genera nuova anteprima",
|
||||
"generateRandomAlias": "Genera alias casuale",
|
||||
"clearAliasFields": "Cancella Campi Alias",
|
||||
"alias": "Alias",
|
||||
"firstName": "Nome",
|
||||
"lastName": "Cognome",
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "Onduidelijke tekens vermijden (o, 0, etc.)",
|
||||
"generateNewPreview": "Genereer nieuw voorbeeld",
|
||||
"generateRandomAlias": "Alias genereren",
|
||||
"clearAliasFields": "Leeg alias velden",
|
||||
"alias": "Alias",
|
||||
"firstName": "Voornaam",
|
||||
"lastName": "Achternaam",
|
||||
|
||||
393
apps/browser-extension/src/i18n/locales/pt.json
Normal file
@@ -0,0 +1,393 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "Log in to AliasVault",
|
||||
"username": "Username or email",
|
||||
"usernamePlaceholder": "name / name@company.com",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"rememberMe": "Remember me",
|
||||
"loginButton": "Login",
|
||||
"noAccount": "No account yet?",
|
||||
"createVault": "Create new vault",
|
||||
"twoFactorTitle": "Please enter the authentication code from your authenticator app.",
|
||||
"authCode": "Authentication Code",
|
||||
"authCodePlaceholder": "Enter 6-digit code",
|
||||
"verify": "Verify",
|
||||
"cancel": "Cancel",
|
||||
"twoFactorNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.",
|
||||
"masterPassword": "Master Password",
|
||||
"unlockVault": "Unlock Vault",
|
||||
"unlockTitle": "Unlock Your Vault",
|
||||
"unlockDescription": "Enter your master password to unlock your vault.",
|
||||
"logout": "Logout",
|
||||
"logoutConfirm": "Are you sure you want to logout?",
|
||||
"sessionExpired": "Your session has expired. Please log in again.",
|
||||
"unlockSuccess": "Vault unlocked successfully!",
|
||||
"unlockSuccessTitle": "Your vault is successfully unlocked",
|
||||
"unlockSuccessDescription": "You can now use autofill in login forms in your browser.",
|
||||
"closePopup": "Close this popup",
|
||||
"browseVault": "Browse vault contents",
|
||||
"connectingTo": "Connecting to",
|
||||
"switchAccounts": "Switch accounts?",
|
||||
"loggedIn": "Logged in",
|
||||
"errors": {
|
||||
"invalidCode": "Please enter a valid 6-digit authentication code.",
|
||||
"serverError": "Could not reach AliasVault server. Please try again later or contact support if the problem persists.",
|
||||
"noToken": "Login failed -- no token returned",
|
||||
"migrationError": "An error occurred while checking for pending migrations.",
|
||||
"wrongPassword": "Incorrect password. Please try again.",
|
||||
"accountLocked": "Account temporarily locked due to too many failed attempts.",
|
||||
"networkError": "Network error. Please check your connection and try again.",
|
||||
"loginDataMissing": "Login session expired. Please try again."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "Credentials",
|
||||
"emails": "Emails",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"cancel": "Cancel",
|
||||
"use": "Use",
|
||||
"delete": "Delete",
|
||||
"close": "Close",
|
||||
"copied": "Copied!",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"language": "Language",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"loadingEmails": "Loading emails...",
|
||||
"loadingTotpCodes": "Loading TOTP codes...",
|
||||
"attachments": "Attachments",
|
||||
"loadingAttachments": "Loading attachments...",
|
||||
"settings": "Settings",
|
||||
"recentEmails": "Recent emails",
|
||||
"loginCredentials": "Login credentials",
|
||||
"twoFactorAuthentication": "Two-factor authentication",
|
||||
"alias": "Alias",
|
||||
"notes": "Notes",
|
||||
"fullName": "Full Name",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"birthDate": "Birth Date",
|
||||
"nickname": "Nickname",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"syncingVault": "Syncing vault",
|
||||
"savingChangesToVault": "Saving changes to vault",
|
||||
"uploadingVaultToServer": "Uploading vault to server",
|
||||
"checkingVaultUpdates": "Checking for vault updates",
|
||||
"syncingUpdatedVault": "Syncing updated vault",
|
||||
"executingOperation": "Executing operation...",
|
||||
"loadMore": "Load more",
|
||||
"errors": {
|
||||
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
|
||||
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
|
||||
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
|
||||
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
|
||||
"unknownError": "An unknown error occurred",
|
||||
"failedToStoreVault": "Failed to store vault",
|
||||
"vaultNotAvailable": "Vault not available",
|
||||
"failedToRetrieveData": "Failed to retrieve data",
|
||||
"vaultIsLocked": "Vault is locked",
|
||||
"failedToUploadVault": "Failed to upload vault",
|
||||
"passwordChanged": "Your password has changed since the last time you logged in. Please login again for security reasons."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",
|
||||
"ACCOUNT_LOCKED": "Account temporarily locked due to too many failed attempts. Please try again later.",
|
||||
"ACCOUNT_BLOCKED": "Your account has been disabled. If you believe this is a mistake, please contact support.",
|
||||
"USER_NOT_FOUND": "Invalid username or password. Please try again.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Invalid authenticator code. Please try again.",
|
||||
"INVALID_RECOVERY_CODE": "Invalid recovery code. Please try again.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Refresh token is required.",
|
||||
"INVALID_REFRESH_TOKEN": "Invalid refresh token.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token revoked successfully.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "New account registration is currently disabled on this server. Please contact the administrator.",
|
||||
"USERNAME_REQUIRED": "Username is required.",
|
||||
"USERNAME_ALREADY_IN_USE": "Username is already in use.",
|
||||
"USERNAME_AVAILABLE": "Username is available.",
|
||||
"USERNAME_MISMATCH": "Username does not match the current user.",
|
||||
"PASSWORD_MISMATCH": "The provided password does not match your current password.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Account successfully deleted.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Username cannot be empty or whitespace.",
|
||||
"USERNAME_TOO_SHORT": "Username too short: must be at least 3 characters long.",
|
||||
"USERNAME_TOO_LONG": "Username too long: cannot be longer than 40 characters.",
|
||||
"USERNAME_INVALID_EMAIL": "Invalid email address.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Username is invalid, can only contain letters or digits.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Your vault is not up-to-date. Please synchronize your vault and try again.",
|
||||
"INTERNAL_SERVER_ERROR": "Internal server error.",
|
||||
"VAULT_ERROR": "The local vault is not up-to-date. Please synchronize your vault by refreshing the page and try again."
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"or": "or",
|
||||
"new": "New",
|
||||
"cancel": "Cancel",
|
||||
"search": "Search",
|
||||
"vaultLocked": "AliasVault is locked.",
|
||||
"creatingNewAlias": "Creating new alias...",
|
||||
"noMatchesFound": "No matches found",
|
||||
"searchVault": "Search vault...",
|
||||
"serviceName": "Service name",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"enterServiceName": "Enter service name",
|
||||
"enterEmailAddress": "Enter email address",
|
||||
"enterUsername": "Enter username",
|
||||
"hideFor1Hour": "Hide for 1 hour (current site)",
|
||||
"hidePermanently": "Hide permanently (current site)",
|
||||
"createRandomAlias": "Create random alias",
|
||||
"createUsernamePassword": "Create username/password",
|
||||
"randomAlias": "Random alias",
|
||||
"usernamePassword": "Username/password",
|
||||
"createAndSaveAlias": "Create and save alias",
|
||||
"createAndSaveCredential": "Create and save credential",
|
||||
"randomIdentityDescription": "Generate a random identity with a random email address accessible in AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Random identity with random email",
|
||||
"manualCredentialDescription": "Specify your own email address and username.",
|
||||
"manualCredentialDescriptionDropdown": "Manual username and password",
|
||||
"failedToCreateIdentity": "Failed to create identity. Please try again.",
|
||||
"enterEmailAndOrUsername": "Enter email and/or username",
|
||||
"autofillWithAliasVault": "Autofill with AliasVault",
|
||||
"generateRandomPassword": "Generate random password (copy to clipboard)",
|
||||
"generateNewPassword": "Generate new password",
|
||||
"togglePasswordVisibility": "Toggle password visibility",
|
||||
"passwordCopiedToClipboard": "Password copied to clipboard",
|
||||
"enterEmailAndOrUsernameError": "Enter email and/or username",
|
||||
"openAliasVaultToUpgrade": "Open AliasVault to upgrade",
|
||||
"vaultUpgradeRequired": "Vault upgrade required.",
|
||||
"dismissPopup": "Dismiss popup"
|
||||
},
|
||||
"credentials": {
|
||||
"title": "Credentials",
|
||||
"addCredential": "Add Credential",
|
||||
"editCredential": "Edit Credential",
|
||||
"deleteCredential": "Delete Credential",
|
||||
"credentialDetails": "Credential Details",
|
||||
"serviceName": "Service Name",
|
||||
"serviceNamePlaceholder": "e.g., Gmail, Facebook, Bank",
|
||||
"website": "Website",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "Enter username",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"generatePassword": "Generate Password",
|
||||
"copyPassword": "Copy Password",
|
||||
"showPassword": "Show Password",
|
||||
"hidePassword": "Hide Password",
|
||||
"notes": "Notes",
|
||||
"notesPlaceholder": "Additional notes...",
|
||||
"totp": "Two-Factor Authentication",
|
||||
"totpCode": "TOTP Code",
|
||||
"copyTotp": "Copy TOTP",
|
||||
"totpSecret": "TOTP Secret",
|
||||
"totpSecretPlaceholder": "Enter TOTP secret key",
|
||||
"noCredentials": "No credentials found",
|
||||
"noCredentialsDescription": "Add your first credential to get started",
|
||||
"searchPlaceholder": "Search credentials...",
|
||||
"welcomeTitle": "Welcome to AliasVault!",
|
||||
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Fill Form",
|
||||
"deleteConfirm": "Are you sure you want to delete this credential?",
|
||||
"saveSuccess": "Credential saved successfully",
|
||||
"tags": "Tags",
|
||||
"addTag": "Add Tag",
|
||||
"removeTag": "Remove Tag",
|
||||
"folder": "Folder",
|
||||
"selectFolder": "Select Folder",
|
||||
"createFolder": "Create Folder",
|
||||
"saveCredential": "Save credential",
|
||||
"deleteCredentialTitle": "Delete Credential",
|
||||
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
|
||||
"randomAlias": "Random Alias",
|
||||
"manual": "Manual",
|
||||
"service": "Service",
|
||||
"serviceUrl": "Service URL",
|
||||
"loginCredentials": "Login Credentials",
|
||||
"generateRandomUsername": "Generate random username",
|
||||
"generateRandomPassword": "Generate random password",
|
||||
"changePasswordComplexity": "Change password complexity",
|
||||
"passwordLength": "Password length",
|
||||
"includeLowercase": "Include lowercase letters",
|
||||
"includeUppercase": "Include uppercase letters",
|
||||
"includeNumbers": "Include numbers",
|
||||
"includeSpecialChars": "Include special characters",
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"nickName": "Nick Name",
|
||||
"gender": "Gender",
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidEmail": "Invalid email format",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
|
||||
},
|
||||
"privateEmailTitle": "Private Email",
|
||||
"privateEmailAliasVaultServer": "AliasVault server",
|
||||
"privateEmailDescription": "E2E encrypted, fully private.",
|
||||
"publicEmailTitle": "Public Temp Email Providers",
|
||||
"publicEmailDescription": "Anonymous but limited privacy. Email content is readable by anyone that knows the address.",
|
||||
"useDomainChooser": "Use domain chooser",
|
||||
"enterCustomDomain": "Enter custom domain",
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
"deleteEmailTitle": "Delete Email",
|
||||
"deleteEmailConfirm": "Are you sure you want to permanently delete this email?",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"date": "Date",
|
||||
"emailContent": "Email content",
|
||||
"attachments": "Attachments",
|
||||
"emailNotFound": "Email not found",
|
||||
"noEmails": "No emails found",
|
||||
"noEmailsDescription": "You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.",
|
||||
"dateFormat": {
|
||||
"justNow": "just now",
|
||||
"minutesAgo_single": "{{count}} min ago",
|
||||
"minutesAgo_plural": "{{count}} mins ago",
|
||||
"hoursAgo_single": "{{count}} hr ago",
|
||||
"hoursAgo_plural": "{{count}} hrs ago",
|
||||
"yesterday": "yesterday"
|
||||
},
|
||||
"errors": {
|
||||
"emailLoadError": "An error occurred while loading emails. Please try again later.",
|
||||
"emailUnexpectedError": "An unexpected error occurred while loading emails. Please try again later."
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "The current chosen email address is already in use. Please change the email address by editing this credential.",
|
||||
"CLAIM_DOES_NOT_EXIST": "An error occurred while trying to load the emails. Please try to edit and save the credential entry to synchronize the database, then try again."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"serverUrl": "Server URL",
|
||||
"language": "Language",
|
||||
"autofillEnabled": "Enable Autofill",
|
||||
"version": "Version",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"openWebApp": "Open web app",
|
||||
"loggedIn": "Logged in",
|
||||
"logout": "Logout",
|
||||
"globalSettings": "Global Settings",
|
||||
"autofillPopup": "Autofill popup",
|
||||
"activeOnAllSites": "Active on all sites (unless disabled below)",
|
||||
"disabledOnAllSites": "Disabled on all sites",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"rightClickContextMenu": "Right-click context menu",
|
||||
"autofillMatching": "Autofill Matching",
|
||||
"autofillMatchingMode": "Autofill matching mode",
|
||||
"autofillMatchingModeDescription": "Determines which credentials are considered a match and shown as suggestions in the autofill popup for a given website.",
|
||||
"autofillMatchingDefault": "URL + subdomain + name wildcard",
|
||||
"autofillMatchingUrlSubdomain": "URL + subdomain",
|
||||
"autofillMatchingUrlExact": "Exact URL domain only",
|
||||
"siteSpecificSettings": "Site-Specific Settings",
|
||||
"autofillPopupOn": "Autofill popup on: ",
|
||||
"enabledForThisSite": "Enabled for this site",
|
||||
"disabledForThisSite": "Disabled for this site",
|
||||
"temporarilyDisabledUntil": "Temporarily disabled until ",
|
||||
"resetAllSiteSettings": "Reset all site-specific settings",
|
||||
"appearance": "Appearance",
|
||||
"theme": "Theme",
|
||||
"useDefault": "Use default",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||
"configureKeyboardShortcuts": "Configure keyboard shortcuts",
|
||||
"configure": "Configure",
|
||||
"security": "Security",
|
||||
"clipboardClearTimeout": "Clear clipboard after copying",
|
||||
"clipboardClearTimeoutDescription": "Automatically clear the clipboard after copying sensitive data",
|
||||
"clipboardClearDisabled": "Never clear",
|
||||
"clipboardClear5Seconds": "Clear after 5 seconds",
|
||||
"clipboardClear10Seconds": "Clear after 10 seconds",
|
||||
"clipboardClear15Seconds": "Clear after 15 seconds",
|
||||
"autoLockTimeout": "Auto-lock Timeout",
|
||||
"autoLockTimeoutDescription": "Automatically lock the vault after a period of inactivity",
|
||||
"autoLockTimeoutHelp": "The vault will only lock after the specified period of inactivity (no autofill usage or extension popup opened). The vault will always lock when the browser is closed, regardless of this setting.",
|
||||
"autoLockNever": "Never",
|
||||
"autoLock15Seconds": "15 seconds",
|
||||
"autoLock1Minute": "1 minute",
|
||||
"autoLock5Minutes": "5 minutes",
|
||||
"autoLock15Minutes": "15 minutes",
|
||||
"autoLock30Minutes": "30 minutes",
|
||||
"autoLock1Hour": "1 hour",
|
||||
"autoLock4Hours": "4 hours",
|
||||
"autoLock8Hours": "8 hours",
|
||||
"autoLock24Hours": "24 hours",
|
||||
"versionPrefix": "Version ",
|
||||
"preferences": "Preferences",
|
||||
"autofillSettings": "Autofill Settings",
|
||||
"clipboardSettings": "Clipboard Settings",
|
||||
"contextMenuSettings": "Context Menu Settings",
|
||||
"contextMenu": "Context Menu",
|
||||
"contextMenuEnabled": "Context menu is enabled",
|
||||
"contextMenuDisabled": "Context menu is disabled",
|
||||
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
|
||||
"selectLanguage": "Select Language",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL is required",
|
||||
"apiUrlInvalid": "Please enter a valid API URL",
|
||||
"clientUrlRequired": "Client URL is required",
|
||||
"clientUrlInvalid": "Please enter a valid client URL"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "Upgrade Vault",
|
||||
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
|
||||
"versionInformation": "Version Information",
|
||||
"yourVault": "Your vault:",
|
||||
"newVersion": "New version:",
|
||||
"upgrade": "Upgrade Vault",
|
||||
"upgrading": "Upgrading...",
|
||||
"logout": "Logout",
|
||||
"whatsNew": "What's New",
|
||||
"whatsNewDescription": "An upgrade is required to support the following changes:",
|
||||
"noDescriptionAvailable": "No description available for this version.",
|
||||
"okay": "Ok",
|
||||
"status": {
|
||||
"preparingUpgrade": "Preparing upgrade...",
|
||||
"vaultAlreadyUpToDate": "Vault is already up to date",
|
||||
"startingDatabaseTransaction": "Starting database transaction...",
|
||||
"applyingDatabaseMigrations": "Applying database migrations...",
|
||||
"applyingMigration": "Applying migration {{current}} of {{total}}...",
|
||||
"committingChanges": "Committing changes..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Error",
|
||||
"unableToGetVersionInfo": "Unable to get version information. Please try again.",
|
||||
"selfHostedServer": "Self-Hosted Server",
|
||||
"selfHostedWarning": "If you're using a self-hosted server, make sure to also update your self-hosted instance as otherwise logging in to the web client will stop working.",
|
||||
"cancel": "Cancel",
|
||||
"continueUpgrade": "Continue Upgrade",
|
||||
"upgradeFailed": "Upgrade Failed",
|
||||
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
|
||||
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "Избегать двусмысленных символов (o, 0 и т.д.).",
|
||||
"generateNewPreview": "Создать новый предварительный просмотр",
|
||||
"generateRandomAlias": "Сгенерировать случайный псевдоним",
|
||||
"clearAliasFields": "Очистить поля псевдонимов",
|
||||
"alias": "Псевдоним",
|
||||
"firstName": "Имя",
|
||||
"lastName": "Фамилия",
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "Уникайте неоднозначних символів (o, 0 тощо)",
|
||||
"generateNewPreview": "Згенерувати новий попередній перегляд",
|
||||
"generateRandomAlias": "Генерувати випадковий псевдонім",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "Псевдонім",
|
||||
"firstName": "Ім’я",
|
||||
"lastName": "Прізвище",
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "避免易混淆字符(o、0 等)",
|
||||
"generateNewPreview": "生成新预览",
|
||||
"generateRandomAlias": "生成随机别名",
|
||||
"clearAliasFields": "清除别名字段",
|
||||
"alias": "别名",
|
||||
"firstName": "名",
|
||||
"lastName": "姓",
|
||||
|
||||
@@ -6,7 +6,7 @@ export class AppInfo {
|
||||
/**
|
||||
* The current extension version. This should be updated with each release of the extension.
|
||||
*/
|
||||
public static readonly VERSION = '0.23.0';
|
||||
public static readonly VERSION = '0.23.2';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault server (API) version. If the server version is below this, the
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// TODO: store generic setting constants somewhere else.
|
||||
export const DISABLED_SITES_KEY = 'local:aliasvault_disabled_sites';
|
||||
export const GLOBAL_AUTOFILL_POPUP_ENABLED_KEY = 'local:aliasvault_global_autofill_popup_enabled';
|
||||
export const GLOBAL_CONTEXT_MENU_ENABLED_KEY = 'local:aliasvault_global_context_menu_enabled';
|
||||
@@ -9,5 +8,6 @@ export const AUTO_LOCK_TIMEOUT_KEY = 'local:aliasvault_auto_lock_timeout';
|
||||
export const AUTOFILL_MATCHING_MODE_KEY = 'local:aliasvault_autofill_matching_mode';
|
||||
|
||||
// TODO: store these settings in the actual vault when updating the datamodel for roadmap v1.0.
|
||||
export const LAST_CUSTOM_EMAIL_KEY = 'local:aliasvault_last_custom_email';
|
||||
export const LAST_CUSTOM_USERNAME_KEY = 'local:aliasvault_last_custom_username';
|
||||
export const CUSTOM_EMAIL_HISTORY_KEY = 'local:aliasvault_custom_email_history';
|
||||
export const CUSTOM_USERNAME_HISTORY_KEY = 'local:aliasvault_custom_username_history';
|
||||
export const SKIP_FORM_RESTORE_KEY = 'local:aliasvault_skip_form_restore';
|
||||
|
||||
@@ -479,14 +479,18 @@ export class FormDetector {
|
||||
excludeElements: HTMLInputElement[] = []
|
||||
): HTMLInputElement | null {
|
||||
const all = this.findAllInputFields(form, patterns, types, excludeElements);
|
||||
|
||||
// Filter out parent-child duplicates
|
||||
const filtered = this.filterOutNestedDuplicates(all);
|
||||
|
||||
// if email type explicitly requested, prefer actual <input type="email">
|
||||
if (types.includes('email')) {
|
||||
const emailMatch = all.find(i => (i.type || '').toLowerCase() === 'email');
|
||||
const emailMatch = filtered.find(i => (i.type || '').toLowerCase() === 'email');
|
||||
if (emailMatch) {
|
||||
return emailMatch;
|
||||
}
|
||||
}
|
||||
return all.length > 0 ? all[0] : null;
|
||||
return filtered.length > 0 ? filtered[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -496,25 +500,32 @@ export class FormDetector {
|
||||
primary: HTMLInputElement | null,
|
||||
confirm: HTMLInputElement | null
|
||||
} {
|
||||
// Find primary email field
|
||||
const primaryEmail = this.findInputField(
|
||||
// Find all email fields first
|
||||
const emailFields = this.findAllInputFields(
|
||||
form,
|
||||
CombinedFieldPatterns.email,
|
||||
['text', 'email']
|
||||
);
|
||||
|
||||
// Filter out parent-child relationships
|
||||
const filteredEmailFields = this.filterOutNestedDuplicates(emailFields);
|
||||
const primaryEmail = filteredEmailFields[0] ?? null;
|
||||
|
||||
/*
|
||||
* Find confirmation email field if primary exists
|
||||
* and ensure it's not the same as the primary email field.
|
||||
*/
|
||||
const confirmEmail = primaryEmail
|
||||
? this.findInputField(
|
||||
const confirmEmailFields = primaryEmail
|
||||
? this.findAllInputFields(
|
||||
form,
|
||||
CombinedFieldPatterns.emailConfirm,
|
||||
['text', 'email'],
|
||||
[primaryEmail]
|
||||
)
|
||||
: null;
|
||||
: [];
|
||||
|
||||
const filteredConfirmFields = this.filterOutNestedDuplicates(confirmEmailFields);
|
||||
const confirmEmail = filteredConfirmFields[0] ?? null;
|
||||
|
||||
return {
|
||||
primary: primaryEmail,
|
||||
@@ -667,6 +678,56 @@ export class FormDetector {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out nested duplicates where a parent element and its child are both detected.
|
||||
* This happens with custom elements that contain actual input elements.
|
||||
* We prefer the innermost actual input element over the parent custom element.
|
||||
*/
|
||||
private filterOutNestedDuplicates(fields: HTMLInputElement[]): HTMLInputElement[] {
|
||||
if (fields.length <= 1) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
const filtered: HTMLInputElement[] = [];
|
||||
|
||||
for (const field of fields) {
|
||||
let shouldInclude = true;
|
||||
|
||||
// Check if this field is a parent of any other field in the list
|
||||
for (const otherField of fields) {
|
||||
if (field !== otherField) {
|
||||
// Check if field contains otherField (field is parent)
|
||||
if (field.contains(otherField)) {
|
||||
shouldInclude = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if field's shadow DOM contains otherField
|
||||
const fieldWithShadow = field as HTMLElement & { shadowRoot?: ShadowRoot };
|
||||
if (fieldWithShadow.shadowRoot && fieldWithShadow.shadowRoot.contains(otherField)) {
|
||||
shouldInclude = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldInclude) {
|
||||
// Also check if this field is not already represented by its actual input
|
||||
const actualInput = this.getActualInputElement(field);
|
||||
if (actualInput !== field) {
|
||||
// If the actual input is also in the list, skip the parent
|
||||
if (fields.includes(actualInput as HTMLInputElement)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
filtered.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the password field in a form.
|
||||
*/
|
||||
@@ -676,9 +737,12 @@ export class FormDetector {
|
||||
} {
|
||||
const passwordFields = this.findAllInputFields(form, CombinedFieldPatterns.password, ['password']);
|
||||
|
||||
// Filter out parent-child relationships to avoid detecting the same field twice
|
||||
const filteredFields = this.filterOutNestedDuplicates(passwordFields);
|
||||
|
||||
return {
|
||||
primary: passwordFields[0] ?? null,
|
||||
confirm: passwordFields[1] ?? null
|
||||
primary: filteredFields[0] ?? null,
|
||||
confirm: filteredFields[1] ?? null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,12 @@ export class FormFiller {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fillBasicFields(credential);
|
||||
// Fill basic fields and password fields in parallel
|
||||
await Promise.all([
|
||||
this.fillBasicFields(credential),
|
||||
this.fillPasswordFields(credential)
|
||||
]);
|
||||
|
||||
this.fillBirthdateFields(credential);
|
||||
this.fillGenderFields(credential);
|
||||
}
|
||||
@@ -61,7 +66,7 @@ export class FormFiller {
|
||||
clientY: window.innerHeight / 2
|
||||
});
|
||||
// Note: isTrusted is read-only and set by the browser
|
||||
|
||||
|
||||
if (!await this.clickValidator.validateClick(dummyEvent)) {
|
||||
console.warn('[AliasVault Security] Form autofill blocked: Page-wide attack detected');
|
||||
return false;
|
||||
@@ -94,7 +99,7 @@ export class FormFiller {
|
||||
*/
|
||||
private getAllFormFields(): HTMLElement[] {
|
||||
const fields: HTMLElement[] = [];
|
||||
|
||||
|
||||
if (this.form.usernameField) {
|
||||
fields.push(this.form.usernameField);
|
||||
}
|
||||
@@ -110,7 +115,7 @@ export class FormFiller {
|
||||
if (this.form.emailConfirmField) {
|
||||
fields.push(this.form.emailConfirmField);
|
||||
}
|
||||
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
@@ -132,8 +137,8 @@ export class FormFiller {
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
|
||||
// Check if field is within viewport
|
||||
if (rect.width === 0 || rect.height === 0 ||
|
||||
centerX < 0 || centerY < 0 ||
|
||||
if (rect.width === 0 || rect.height === 0 ||
|
||||
centerX < 0 || centerY < 0 ||
|
||||
centerX > window.innerWidth || centerY > window.innerHeight) {
|
||||
console.warn('[AliasVault Security] Field outside viewport or zero-sized:', rect);
|
||||
return false;
|
||||
@@ -142,16 +147,16 @@ export class FormFiller {
|
||||
// Use elementsFromPoint to check what's actually at the field center
|
||||
try {
|
||||
const elementsAtPoint = document.elementsFromPoint(centerX, centerY);
|
||||
|
||||
|
||||
if (elementsAtPoint.length === 0) {
|
||||
console.warn('[AliasVault Security] No elements found at field center');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if our field is in the element stack (or its parents/children)
|
||||
const fieldFound = elementsAtPoint.some(element =>
|
||||
element === field ||
|
||||
field.contains(element) ||
|
||||
const fieldFound = elementsAtPoint.some(element =>
|
||||
element === field ||
|
||||
field.contains(element) ||
|
||||
element.contains(field)
|
||||
);
|
||||
|
||||
@@ -167,7 +172,7 @@ export class FormFiller {
|
||||
}
|
||||
|
||||
const style = getComputedStyle(element);
|
||||
|
||||
|
||||
// Check for nearly transparent overlays
|
||||
const opacity = parseFloat(style.opacity);
|
||||
if (opacity > 0 && opacity < 0.1) {
|
||||
@@ -184,7 +189,7 @@ export class FormFiller {
|
||||
|
||||
// Check for elements covering large areas (potential clickjacking overlays)
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
if (elementRect.width >= window.innerWidth * 0.8 &&
|
||||
if (elementRect.width >= window.innerWidth * 0.8 &&
|
||||
elementRect.height >= window.innerHeight * 0.8) {
|
||||
console.warn('[AliasVault Security] Large covering element detected:', element);
|
||||
return true;
|
||||
@@ -207,35 +212,35 @@ export class FormFiller {
|
||||
try {
|
||||
// Find all forms on the page
|
||||
const allForms = Array.from(document.querySelectorAll('form'));
|
||||
|
||||
|
||||
if (allForms.length <= 1) {
|
||||
return false; // Only one form, no decoy risk
|
||||
}
|
||||
|
||||
let suspiciousFormCount = 0;
|
||||
|
||||
|
||||
for (const form of allForms) {
|
||||
const hasPasswordField = form.querySelector('input[type="password"]');
|
||||
const hasEmailField = form.querySelector('input[type="email"], input[name*="email" i], input[placeholder*="email" i]');
|
||||
const hasUsernameField = form.querySelector('input[type="text"], input[name*="user" i], input[placeholder*="user" i]');
|
||||
|
||||
|
||||
// Count forms with login-like patterns
|
||||
if (hasPasswordField && (hasEmailField || hasUsernameField)) {
|
||||
const formRect = form.getBoundingClientRect();
|
||||
const isVisible = formRect.width > 0 && formRect.height > 0;
|
||||
|
||||
|
||||
if (isVisible) {
|
||||
suspiciousFormCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If more than 2 visible login forms, it's suspicious
|
||||
if (suspiciousFormCount > 2) {
|
||||
console.warn('[AliasVault Security] Multiple login forms detected:', suspiciousFormCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.warn('[AliasVault Security] Decoy form detection error:', error);
|
||||
@@ -251,7 +256,7 @@ export class FormFiller {
|
||||
private setElementValue(element: HTMLInputElement | HTMLSelectElement, value: string): void {
|
||||
// Try to set value directly on the element
|
||||
element.value = value;
|
||||
|
||||
|
||||
// If it's a custom element with shadow DOM, try to find and fill the actual input
|
||||
if (element.shadowRoot) {
|
||||
const shadowInput = element.shadowRoot.querySelector('input, textarea') as HTMLInputElement;
|
||||
@@ -261,7 +266,7 @@ export class FormFiller {
|
||||
this.triggerInputEvents(shadowInput, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Also check if the element contains a regular child input (non-shadow DOM)
|
||||
const childInput = element.querySelector('input, textarea') as HTMLInputElement;
|
||||
if (childInput && childInput !== element) {
|
||||
@@ -274,18 +279,9 @@ export class FormFiller {
|
||||
* Fill the basic fields of the form.
|
||||
* @param credential The credential to fill the form with.
|
||||
*/
|
||||
private fillBasicFields(credential: Credential): void {
|
||||
private async fillBasicFields(credential: Credential): Promise<void> {
|
||||
if (this.form.usernameField && credential.Username) {
|
||||
this.setElementValue(this.form.usernameField, credential.Username);
|
||||
this.triggerInputEvents(this.form.usernameField);
|
||||
}
|
||||
|
||||
if (this.form.passwordField && credential.Password) {
|
||||
this.fillPasswordField(this.form.passwordField, credential.Password);
|
||||
}
|
||||
|
||||
if (this.form.passwordConfirmField && credential.Password) {
|
||||
this.fillPasswordField(this.form.passwordConfirmField, credential.Password);
|
||||
await this.fillTextFieldWithTyping(this.form.usernameField, credential.Username);
|
||||
}
|
||||
|
||||
if (this.form.emailField && (credential.Alias?.Email !== undefined || credential.Username !== undefined)) {
|
||||
@@ -329,6 +325,70 @@ export class FormFiller {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill a text field with character-by-character typing to better simulate human input.
|
||||
* This method is similar to fillPasswordField but optimized for regular text fields.
|
||||
*
|
||||
* @param field The text field to fill.
|
||||
* @param text The text to fill the field with.
|
||||
*/
|
||||
private async fillTextFieldWithTyping(field: HTMLInputElement, text: string): Promise<void> {
|
||||
// Find the actual input element (could be in shadow DOM)
|
||||
let actualInput = field;
|
||||
|
||||
// Check for shadow DOM input
|
||||
if (field.shadowRoot) {
|
||||
const shadowInput = field.shadowRoot.querySelector('input, textarea') as HTMLInputElement;
|
||||
if (shadowInput) {
|
||||
actualInput = shadowInput;
|
||||
}
|
||||
} else if (field.tagName.toLowerCase() !== 'input' && field.tagName.toLowerCase() !== 'textarea') {
|
||||
// Check for child input (non-shadow DOM) only if field is not already an input
|
||||
const childInput = field.querySelector('input, textarea') as HTMLInputElement;
|
||||
if (childInput) {
|
||||
actualInput = childInput;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the field first without triggering events
|
||||
actualInput.value = '';
|
||||
|
||||
// Type each character with a small delay
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
actualInput.value += text[i];
|
||||
|
||||
/*
|
||||
* Small delay between characters to simulate human typing
|
||||
* This helps with sites that have input event handlers
|
||||
*/
|
||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 10 + 10));
|
||||
}
|
||||
|
||||
// Trigger events once after all typing is complete
|
||||
this.triggerInputEvents(actualInput, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill password fields sequentially to avoid visual conflicts.
|
||||
* First fills the main password field, then the confirm field if present.
|
||||
* @param credential The credential containing the password.
|
||||
*/
|
||||
private async fillPasswordFields(credential: Credential): Promise<void> {
|
||||
if (!credential.Password) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill main password field first
|
||||
if (this.form.passwordField) {
|
||||
await this.fillPasswordField(this.form.passwordField, credential.Password);
|
||||
}
|
||||
|
||||
// Then fill password confirm field after main field is complete
|
||||
if (this.form.passwordConfirmField) {
|
||||
await this.fillPasswordField(this.form.passwordConfirmField, credential.Password);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the password field with the given password. This uses a small delay between each character to simulate human typing.
|
||||
* Simulates actual keystroke behavior by appending characters one by one.
|
||||
@@ -340,48 +400,37 @@ export class FormFiller {
|
||||
private async fillPasswordField(field: HTMLInputElement, password: string): Promise<void> {
|
||||
// Find the actual input element (could be in shadow DOM)
|
||||
let actualInput = field;
|
||||
let isCustomElement = false;
|
||||
|
||||
|
||||
// Check for shadow DOM input
|
||||
if (field.shadowRoot) {
|
||||
const shadowInput = field.shadowRoot.querySelector('input[type="password"], input') as HTMLInputElement;
|
||||
if (shadowInput) {
|
||||
actualInput = shadowInput;
|
||||
isCustomElement = true;
|
||||
}
|
||||
} else if (field.tagName.toLowerCase() !== 'input') {
|
||||
// Check for child input (non-shadow DOM) only if field is not already an input
|
||||
const childInput = field.querySelector('input[type="password"], input') as HTMLInputElement;
|
||||
if (childInput) {
|
||||
actualInput = childInput;
|
||||
isCustomElement = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the field first
|
||||
// Clear the field first without triggering events
|
||||
actualInput.value = '';
|
||||
if (isCustomElement) {
|
||||
field.value = '';
|
||||
}
|
||||
this.triggerInputEvents(actualInput, true);
|
||||
|
||||
// Type each character with a small delay
|
||||
for (const char of password) {
|
||||
// Append the character to the actual input
|
||||
actualInput.value += char;
|
||||
if (isCustomElement) {
|
||||
// Also update the custom element's value property for compatibility
|
||||
field.value += char;
|
||||
}
|
||||
// Small random delay between 5-15ms to simulate human typing
|
||||
this.triggerInputEvents(actualInput, false);
|
||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 10 + 5));
|
||||
for (let i = 0; i < password.length; i++) {
|
||||
actualInput.value += password[i];
|
||||
|
||||
/*
|
||||
* Small delay between characters to simulate human typing
|
||||
* This helps with sites that have input event handlers
|
||||
*/
|
||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 10 + 10));
|
||||
}
|
||||
|
||||
this.triggerInputEvents(actualInput, false);
|
||||
if (isCustomElement) {
|
||||
this.triggerInputEvents(field, false);
|
||||
}
|
||||
// Trigger events once after all typing is complete
|
||||
this.triggerInputEvents(actualInput, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -79,4 +79,75 @@ describe('FormDetector generic tests', () => {
|
||||
expect(form).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Nested custom elements (parent-child duplicate prevention)', () => {
|
||||
describe('TrueNAS-style nested custom elements', () => {
|
||||
const htmlFile = 'nested-custom-elements.html';
|
||||
|
||||
it('should not detect both parent custom element and child input as separate password fields', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
|
||||
// Click on the actual password input element
|
||||
const passwordInput = document.getElementById('password-field');
|
||||
const formDetector = new FormDetector(document, passwordInput as HTMLElement);
|
||||
|
||||
// Get the detected form
|
||||
const form = formDetector.getForm();
|
||||
expect(form).toBeTruthy();
|
||||
|
||||
// Should detect only ONE password field
|
||||
expect(form?.passwordField).toBeTruthy();
|
||||
expect(form?.passwordConfirmField).toBeFalsy();
|
||||
|
||||
// The detected password field should be the actual input element
|
||||
expect(form?.passwordField?.tagName.toLowerCase()).toBe('input');
|
||||
expect(form?.passwordField?.type).toBe('password');
|
||||
expect(form?.passwordField?.id).toBe('password-field');
|
||||
});
|
||||
|
||||
it('should detect username field correctly without duplication', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
|
||||
const usernameInput = document.getElementById('username-field');
|
||||
const formDetector = new FormDetector(document, usernameInput as HTMLElement);
|
||||
|
||||
const form = formDetector.getForm();
|
||||
expect(form).toBeTruthy();
|
||||
|
||||
// Should detect the username field
|
||||
expect(form?.usernameField).toBeTruthy();
|
||||
expect(form?.usernameField?.tagName.toLowerCase()).toBe('input');
|
||||
expect(form?.usernameField?.id).toBe('username-field');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Nested custom elements with actual password confirm field', () => {
|
||||
const htmlFile = 'nested-custom-elements-confirm.html';
|
||||
|
||||
it('should correctly identify actual password confirm fields vs parent-child duplicates', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
|
||||
const passwordElement = document.getElementById('password-field');
|
||||
const formDetector = new FormDetector(document, passwordElement as HTMLElement);
|
||||
|
||||
const form = formDetector.getForm();
|
||||
expect(form).toBeTruthy();
|
||||
|
||||
// Should correctly detect both password and confirm as separate fields
|
||||
expect(form?.passwordField).toBeTruthy();
|
||||
expect(form?.passwordConfirmField).toBeTruthy();
|
||||
|
||||
// Both should be actual input elements
|
||||
expect(form?.passwordField?.tagName.toLowerCase()).toBe('input');
|
||||
expect(form?.passwordConfirmField?.tagName.toLowerCase()).toBe('input');
|
||||
|
||||
// They should be different elements
|
||||
expect(form?.passwordField?.id).toBe('password-field');
|
||||
expect(form?.passwordConfirmField?.id).toBe('password-confirm-field');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Registration Form - Nested Custom Elements with Confirm</title>
|
||||
</head>
|
||||
<body>
|
||||
<form id="registration-form">
|
||||
<div class="field-group">
|
||||
<ix-input formcontrolname="username" type="text" ix-label="Username" name="username">
|
||||
<input id="username-field" type="text" aria-label="Username" name="username" class="mat-input-element">
|
||||
</ix-input>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<ix-input formcontrolname="password" type="password" ix-label="Password" name="password">
|
||||
<input id="password-field" type="password" aria-label="Password" name="password" class="mat-input-element">
|
||||
</ix-input>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<ix-input formcontrolname="passwordConfirm" type="password" ix-label="Confirm Password" name="passwordConfirm">
|
||||
<input id="password-confirm-field" type="password" aria-label="Confirm Password" name="passwordConfirm" class="mat-input-element">
|
||||
</ix-input>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>TrueNAS Login - Nested Custom Elements</title>
|
||||
</head>
|
||||
<body>
|
||||
<form id="login-form">
|
||||
<div class="field-group">
|
||||
<ix-input id="username-wrapper" formcontrolname="username" type="text" ix-label="Username" name="username">
|
||||
<ix-label><label><span>Username</span></label></ix-label>
|
||||
<div class="input-container">
|
||||
<input id="username-field" type="text" aria-label="Username" name="username" class="mat-input-element">
|
||||
</div>
|
||||
</ix-input>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<ix-input id="password-wrapper" formcontrolname="password" type="password" ix-label="Password" name="password">
|
||||
<ix-label><label><span>Password</span></label></ix-label>
|
||||
<div class="input-container">
|
||||
<input id="password-field" type="password" aria-label="Password" name="password" class="mat-input-element">
|
||||
</div>
|
||||
</ix-input>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,148 @@
|
||||
import { FormDetector } from '../formDetector/FormDetector';
|
||||
|
||||
/**
|
||||
* Utility for detecting service name and URL information
|
||||
* Shared between content script and popup dashboard
|
||||
*/
|
||||
export class ServiceDetectionUtility {
|
||||
/**
|
||||
* Get service information from the current page
|
||||
*/
|
||||
public static getServiceInfo(document: Document, location: Location): ServiceInfo {
|
||||
// Get suggested service names using FormDetector
|
||||
const suggestedNames = FormDetector.getSuggestedServiceName(document, location);
|
||||
|
||||
// Get the current URL
|
||||
const currentUrl = location.href;
|
||||
|
||||
// Process the URL to extract service URL (origin + pathname)
|
||||
let serviceUrl = '';
|
||||
try {
|
||||
const url = new URL(currentUrl);
|
||||
// Only include http/https URLs
|
||||
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
||||
serviceUrl = url.origin + url.pathname;
|
||||
// Remove trailing slash
|
||||
if (serviceUrl.endsWith('/')) {
|
||||
serviceUrl = serviceUrl.slice(0, -1);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing current URL:', error);
|
||||
}
|
||||
|
||||
return {
|
||||
suggestedNames,
|
||||
currentUrl,
|
||||
serviceUrl,
|
||||
domain: location.hostname.replace(/^www\./, '')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service information from tab data (for use in popup dashboard)
|
||||
*/
|
||||
public static getServiceInfoFromTab(tabUrl: string, tabTitle?: string): ServiceInfo {
|
||||
try {
|
||||
const url = new URL(tabUrl);
|
||||
const location = {
|
||||
href: tabUrl,
|
||||
hostname: url.hostname,
|
||||
protocol: url.protocol,
|
||||
pathname: url.pathname,
|
||||
origin: url.origin
|
||||
} as Location;
|
||||
|
||||
// Create a minimal document object for service name detection
|
||||
const mockDocument = {
|
||||
title: tabTitle || url.hostname
|
||||
} as Document;
|
||||
|
||||
// Use FormDetector logic for service name detection
|
||||
const suggestedNames = FormDetector.getSuggestedServiceName(mockDocument, location);
|
||||
|
||||
// Get service URL (origin + pathname)
|
||||
let serviceUrl = '';
|
||||
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
||||
serviceUrl = url.origin + url.pathname;
|
||||
// Remove trailing slash
|
||||
if (serviceUrl.endsWith('/')) {
|
||||
serviceUrl = serviceUrl.slice(0, -1);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
suggestedNames,
|
||||
currentUrl: tabUrl,
|
||||
serviceUrl,
|
||||
domain: url.hostname.replace(/^www\./, '')
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error parsing tab URL:', error);
|
||||
// Fallback to basic hostname detection
|
||||
const domain = tabUrl.replace(/^https?:\/\/(www\.)?/, '').split('/')[0];
|
||||
return {
|
||||
suggestedNames: [domain],
|
||||
currentUrl: tabUrl,
|
||||
serviceUrl: tabUrl,
|
||||
domain
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encoded service information suitable for URL parameters
|
||||
*/
|
||||
public static getEncodedServiceInfo(document: Document, location: Location): EncodedServiceInfo {
|
||||
const serviceInfo = this.getServiceInfo(document, location);
|
||||
|
||||
return {
|
||||
serviceName: serviceInfo.suggestedNames.length > 0 ? encodeURIComponent(serviceInfo.suggestedNames[0]) : '',
|
||||
serviceUrl: serviceInfo.serviceUrl ? encodeURIComponent(serviceInfo.serviceUrl) : '',
|
||||
currentUrl: encodeURIComponent(serviceInfo.currentUrl),
|
||||
domain: encodeURIComponent(serviceInfo.domain)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encoded service information from tab data
|
||||
*/
|
||||
public static getEncodedServiceInfoFromTab(tabUrl: string, tabTitle?: string): EncodedServiceInfo {
|
||||
const serviceInfo = this.getServiceInfoFromTab(tabUrl, tabTitle);
|
||||
|
||||
return {
|
||||
serviceName: serviceInfo.suggestedNames.length > 0 ? encodeURIComponent(serviceInfo.suggestedNames[0]) : '',
|
||||
serviceUrl: serviceInfo.serviceUrl ? encodeURIComponent(serviceInfo.serviceUrl) : '',
|
||||
currentUrl: encodeURIComponent(serviceInfo.currentUrl),
|
||||
domain: encodeURIComponent(serviceInfo.domain)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Service information interface
|
||||
*/
|
||||
export type ServiceInfo = {
|
||||
/** Array of suggested service names */
|
||||
suggestedNames: string[];
|
||||
/** Current page URL */
|
||||
currentUrl: string;
|
||||
/** Service URL (origin + pathname) */
|
||||
serviceUrl: string;
|
||||
/** Domain name without www prefix */
|
||||
domain: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encoded service information interface
|
||||
*/
|
||||
export type EncodedServiceInfo = {
|
||||
/** URL-encoded primary service name */
|
||||
serviceName: string;
|
||||
/** URL-encoded service URL */
|
||||
serviceUrl: string;
|
||||
/** URL-encoded current page URL */
|
||||
currentUrl: string;
|
||||
/** URL-encoded domain */
|
||||
domain: string;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export default defineConfig({
|
||||
return {
|
||||
name: "AliasVault",
|
||||
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
|
||||
version: "0.23.0",
|
||||
version: "0.23.2",
|
||||
content_security_policy: {
|
||||
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
|
||||
},
|
||||
|
||||
@@ -93,8 +93,8 @@ android {
|
||||
applicationId 'net.aliasvault.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 230000
|
||||
versionName "0.23.0"
|
||||
versionCode 230200
|
||||
versionName "0.23.2"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
@@ -186,6 +186,9 @@ dependencies {
|
||||
// Add vector drawable support for SVG
|
||||
implementation("com.caverock:androidsvg-aar:1.4")
|
||||
|
||||
// Add Argon2 library for password key derivation
|
||||
implementation("com.lambdapioneer.argon2kt:argon2kt:1.4.0")
|
||||
|
||||
// Test dependencies
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.mockito:mockito-core:4.0.0'
|
||||
|
||||
@@ -778,6 +778,27 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a key from a password using Argon2Id.
|
||||
* @param password The password to derive from
|
||||
* @param salt The salt to use
|
||||
* @param encryptionType The type of encryption (should be "Argon2Id")
|
||||
* @param encryptionSettings JSON string with encryption parameters
|
||||
* @param promise The promise to resolve
|
||||
*/
|
||||
@ReactMethod
|
||||
override fun deriveKeyFromPassword(password: String, salt: String, encryptionType: String, encryptionSettings: String, promise: Promise) {
|
||||
try {
|
||||
val derivedKey = vaultStore.deriveKeyFromPassword(password, salt, encryptionType, encryptionSettings)
|
||||
// Return as base64 string
|
||||
val base64Key = android.util.Base64.encodeToString(derivedKey, android.util.Base64.NO_WRAP)
|
||||
promise.resolve(base64Key)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error deriving key from password", e)
|
||||
promise.reject("ERR_DERIVE_KEY", "Failed to derive key from password: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the autofill settings page.
|
||||
* @param promise The promise to resolve
|
||||
|
||||
@@ -6,6 +6,9 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.lambdapioneer.argon2kt.Argon2Kt
|
||||
import com.lambdapioneer.argon2kt.Argon2Mode
|
||||
import com.lambdapioneer.argon2kt.Argon2Version
|
||||
import net.aliasvault.app.vaultstore.interfaces.CredentialOperationCallback
|
||||
import net.aliasvault.app.vaultstore.interfaces.CryptoOperationCallback
|
||||
import net.aliasvault.app.vaultstore.keystoreprovider.KeystoreOperationCallback
|
||||
@@ -240,6 +243,43 @@ class VaultStore(
|
||||
return this.storageProvider.getKeyDerivationParams()
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a key from a password using Argon2Id.
|
||||
* @param password The password to derive from
|
||||
* @param salt The salt to use
|
||||
* @param encryptionType The type of encryption (should be "Argon2Id")
|
||||
* @param encryptionSettings JSON string with encryption parameters
|
||||
* @return The derived key as a ByteArray
|
||||
*/
|
||||
fun deriveKeyFromPassword(password: String, salt: String, encryptionType: String, encryptionSettings: String): ByteArray {
|
||||
if (encryptionType != "Argon2Id") {
|
||||
throw IllegalArgumentException("Unsupported encryption type: $encryptionType")
|
||||
}
|
||||
|
||||
// Parse encryption settings JSON
|
||||
val settings = JSONObject(encryptionSettings)
|
||||
val iterations = settings.getInt("Iterations")
|
||||
val memorySize = settings.getInt("MemorySize")
|
||||
val parallelism = settings.getInt("DegreeOfParallelism")
|
||||
|
||||
// Create Argon2 instance
|
||||
val argon2 = Argon2Kt()
|
||||
|
||||
// Hash the password using Argon2Id
|
||||
val hashResult = argon2.hash(
|
||||
mode = Argon2Mode.ARGON2_ID,
|
||||
password = password.toByteArray(Charsets.UTF_8),
|
||||
salt = salt.toByteArray(Charsets.UTF_8),
|
||||
tCostInIterations = iterations,
|
||||
mCostInKibibyte = memorySize,
|
||||
parallelism = parallelism,
|
||||
hashLengthInBytes = 32,
|
||||
version = Argon2Version.V13,
|
||||
)
|
||||
|
||||
return hashResult.rawHashAsByteArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the encrypted database in the storage provider.
|
||||
* @param encryptedData The encrypted database as a base64 encoded string
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
android:viewportWidth="500"
|
||||
android:viewportHeight="500">
|
||||
<group
|
||||
android:scaleX="0.6"
|
||||
android:scaleY="0.6"
|
||||
android:translateX="100"
|
||||
android:translateY="100">
|
||||
android:scaleX="0.56"
|
||||
android:scaleY="0.56"
|
||||
android:translateX="110"
|
||||
android:translateY="110">
|
||||
<path
|
||||
android:fillColor="#EEC170"
|
||||
android:pathData="m459.87,294.95c0.016,5.4 0.032,10.801 -0.35,16.873c-1.111,6.339 -1.194,12.173 -2.635,17.649c-10.922,41.508 -36.731,69.481 -77.351,83.408c-7.216,2.474 -14.972,3.37 -22.479,4.995c-23.629,0.042 -47.257,0.115 -70.886,0.12c-46.762,0.011 -93.523,-0.014 -140.95,-0.434c-8.59,-2.002 -16.766,-2.835 -24.398,-5.333c-21.595,-7.067 -39.523,-19.656 -53.708,-37.552c-10.227,-12.903 -17.579,-27.17 -21.28,-43.221c-1.475,-6.397 -2.471,-12.904 -3.685,-19.361c-0.052,-5.747 -0.104,-11.494 0.269,-17.886c4.159,-42.973 27.68,-71.638 63.562,-92.153c0,-0.708 -0.002,-1.699 0,-2.69c0.022,-9.829 -1.307,-19.894 0.357,-29.438c3.239,-18.579 11.08,-35.272 23.763,-49.773c12.098,-13.832 26.457,-23.989 43.609,-30.029c7.813,-2.751 16.14,-4.042 24.234,-5.995c7.392,-0.026 14.784,-0.051 22.835,0.323c4.196,0.954 7.795,1.254 11.258,2.105c17.16,4.219 32.287,12.176 45.469,24.104c2.256,2.041 4.372,6.624 9.621,3.868c16.839,-8.842 34.718,-11.597 53.603,-8.594c16.791,2.67 31.602,9.431 44.236,20.636c11.531,10.227 19.84,22.841 25.393,37.236c6.344,16.445 10.389,33.163 6.08,49.389c7.959,8.932 15.807,16.704 22.421,25.414c9.162,12.065 15.33,25.746 18.144,40.776c0.97,5.185 1.911,10.375 2.865,15.563m-71.597,71.012c5.562,-5.228 12.002,-9.799 16.508,-15.817c10.474,-13.992 14.333,-29.916 11.288,-47.446c-2.25,-12.95 -8.197,-24.076 -17.243,-33.063c-12.746,-12.663 -28.865,-18.614 -46.786,-18.569c-69.912,0.177 -139.82,0.568 -209.74,0.962c-15.922,0.09 -29.168,7.421 -39.685,18.296c-14.45,14.944 -20.408,33.343 -16.655,54.368c2.276,12.754 8.217,23.748 17.158,32.66c13.299,13.255 30.097,18.653 48.728,18.651c59.321,-0.005 118.64,0.042 177.96,-0.047c9.591,-0.014 19.181,-0.866 28.773,-0.889c10.649,-0.025 19.978,-3.825 29.687,-9.107z" />
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<!-- AliasVault logo scaled and centered with padding -->
|
||||
<group
|
||||
android:translateX="23.5"
|
||||
android:translateY="23.5"
|
||||
android:scaleX="0.122"
|
||||
android:scaleY="0.122">
|
||||
<!-- Main vault shape -->
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m459.87 294.95c0.016205 5.4005 0.03241 10.801-0.35022 16.873-1.111 6.3392-1.1941 12.173-2.6351 17.649-10.922 41.508-36.731 69.481-77.351 83.408-7.2157 2.4739-14.972 3.3702-22.479 4.995-23.629 0.042205-47.257 0.11453-70.886 0.12027-46.762 0.011322-93.523-0.01416-140.95-0.43411-8.59-2.0024-16.766-2.8352-24.398-5.3326-21.595-7.0666-39.523-19.656-53.708-37.552-10.227-12.903-17.579-27.17-21.28-43.221-1.475-6.3967-2.4711-12.904-3.6852-19.361-0.051849-5.747-0.1037-11.494 0.26915-17.886 4.159-42.973 27.68-71.638 63.562-92.153 0-0.70761-0.001961-1.6988 3.12e-4 -2.69 0.022484-9.8293-1.3071-19.894 0.35664-29.438 3.2391-18.579 11.08-35.272 23.763-49.773 12.098-13.832 26.457-23.989 43.609-30.029 7.813-2.7512 16.14-4.0417 24.234-5.9948 7.392-0.025734 14.784-0.05146 22.835 0.32253 4.1959 0.95392 7.7946 1.2538 11.258 2.1053 17.16 4.2192 32.287 12.176 45.469 24.104 2.2558 2.0411 4.372 6.6241 9.621 3.868 16.839-8.8419 34.718-11.597 53.603-8.594 16.791 2.6699 31.602 9.4308 44.236 20.636 11.531 10.227 19.84 22.841 25.393 37.236 6.3436 16.445 10.389 33.163 6.0798 49.389 7.9587 8.9321 15.807 16.704 22.421 25.414 9.162 12.065 15.33 25.746 18.144 40.776 0.97046 5.1848 1.9111 10.375 2.8654 15.563m-71.597 71.012c5.5615-5.2284 12.002-9.7986 16.508-15.817 10.474-13.992 14.333-29.916 11.288-47.446-2.2496-12.95-8.1973-24.076-17.243-33.063-12.746-12.663-28.865-18.614-46.786-18.569-69.912 0.17712-139.82 0.56831-209.74 0.96176-15.922 0.089599-29.168 7.4209-39.685 18.296-14.45 14.944-20.408 33.343-16.655 54.368 2.2763 12.754 8.2167 23.748 17.158 32.66 13.299 13.255 30.097 18.653 48.728 18.651 59.321-0.005188 118.64 0.042358 177.96-0.046601 9.5912-0.014374 19.181-0.86588 28.773-0.88855 10.649-0.025146 19.978-3.825 29.687-9.1074z"/>
|
||||
<!-- First dot -->
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m162.77 293c15.654 4.3883 20.627 22.967 10.304 34.98-5.3104 6.1795-14.817 8.3208-24.278 5.0472-7.0723-2.4471-12.332-10.362-12.876-17.933-1.0451-14.542 11.089-23.176 21.705-23.046 1.5794 0.019287 3.1517 0.61566 5.1461 0.95184z"/>
|
||||
<!-- Second dot -->
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m227.18 293.64c7.8499 2.3973 11.938 8.2143 13.524 15.077 1.8591 8.0439-0.44817 15.706-7.1588 21.121-6.7633 5.4572-14.417 6.8794-22.578 3.1483-8.2972-3.7933-12.836-10.849-12.736-19.438 0.1687-14.497 14.13-25.368 28.948-19.908z"/>
|
||||
<!-- Third dot -->
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m261.57 319.07c-2.495-14.418 4.6853-22.603 14.596-26.108 9.8945-3.4995 23.181 3.4303 26.267 13.779 4.6504 15.591-7.1651 29.064-21.665 28.161-8.5254-0.53088-17.202-6.5094-19.198-15.831z"/>
|
||||
<!-- Fourth dot -->
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="m336.91 333.41c-9.0175-4.2491-15.337-14.349-13.829-21.682 3.0825-14.989 13.341-20.304 23.018-19.585 10.653 0.79141 17.93 7.407 19.765 17.547 1.9588 10.824-4.1171 19.939-13.494 23.703-5.272 2.1162-10.091 1.5086-15.46 0.017883z"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -2,4 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||
</adaptive-icon>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<!-- mipmap-anydpi-v26/ic_launcher_round.xml -->
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">AliasVault</string>
|
||||
<string name="autofill_service_description" translatable="true">AliasVault AutoFill</string>
|
||||
<string name="aliasvault_icon">AliasVault icon</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
|
||||
<string name="autofill_no_match_found">No match found, create new?</string>
|
||||
<string name="autofill_open_app">Open app</string>
|
||||
<string name="autofill_vault_locked">Vault locked</string>
|
||||
<!-- Biometric prompts -->
|
||||
<string name="biometric_store_key_title">Store Encryption Key</string>
|
||||
<string name="biometric_store_key_subtitle">Authenticate to securely store your encryption key in the Android Keystore. This enables secure access to your vault.</string>
|
||||
<string name="biometric_unlock_vault_title">Unlock Vault</string>
|
||||
<string name="biometric_unlock_vault_subtitle">Authenticate to access your vault</string>
|
||||
</resources>
|
||||
@@ -1,4 +1,5 @@
|
||||
<resources>
|
||||
<color name="splashscreen_background">#ffffff</color>
|
||||
<color name="colorPrimary">#023c69</color>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<locale android:name="de" />
|
||||
<locale android:name="en" />
|
||||
<locale android:name="fi" />
|
||||
<locale android:name="he" />
|
||||
<locale android:name="it" />
|
||||
<locale android:name="nl" />
|
||||
<locale android:name="uk" />
|
||||
<locale android:name="zh" />
|
||||
</locale-config>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"expo": {
|
||||
"name": "AliasVault",
|
||||
"slug": "AliasVault",
|
||||
"version": "0.23.0",
|
||||
"version": "0.23.2",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "net.aliasvault.app",
|
||||
|
||||
@@ -58,6 +58,13 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
const [originalAttachmentIds, setOriginalAttachmentIds] = useState<string[]>([]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Track last generated values to avoid overwriting manual entries
|
||||
const [lastGeneratedValues, setLastGeneratedValues] = useState<{
|
||||
username: string | null;
|
||||
password: string | null;
|
||||
email: string | null;
|
||||
}>({ username: null, password: null, email: null });
|
||||
|
||||
const { control, handleSubmit, setValue, watch } = useForm<Credential>({
|
||||
resolver: yupResolver(createCredentialSchema(t)) as Resolver<Credential>,
|
||||
defaultValues: {
|
||||
@@ -189,30 +196,56 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
const defaultEmailDomain = await dbContext.sqliteClient!.getDefaultEmailDomain();
|
||||
const email = defaultEmailDomain ? `${identity.emailPrefix}@${defaultEmailDomain}` : identity.emailPrefix;
|
||||
|
||||
setValue('Alias.Email', email);
|
||||
// Check current values
|
||||
const currentUsername = watch('Username') ?? '';
|
||||
const currentPassword = watch('Password') ?? '';
|
||||
const currentEmail = watch('Alias.Email') ?? '';
|
||||
|
||||
// Only overwrite email if it's empty or matches the last generated value
|
||||
if (!currentEmail || currentEmail === lastGeneratedValues.email) {
|
||||
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
|
||||
// Only overwrite username if it's empty or matches the last generated value
|
||||
if (!currentUsername || currentUsername === lastGeneratedValues.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
|
||||
// Only overwrite password if it's empty or matches the last generated value
|
||||
if (!currentPassword || currentPassword === lastGeneratedValues.password) {
|
||||
setValue('Password', password);
|
||||
// Make password visible when newly generated
|
||||
setIsPasswordVisible(true);
|
||||
}
|
||||
}, [isEditMode, watch, setValue, setIsPasswordVisible, initializeGenerators, dbContext.sqliteClient]);
|
||||
|
||||
// Update tracking with new generated values
|
||||
setLastGeneratedValues({
|
||||
username: identity.nickName,
|
||||
password: password,
|
||||
email: email
|
||||
});
|
||||
}, [watch, setValue, setIsPasswordVisible, initializeGenerators, dbContext.sqliteClient, lastGeneratedValues, setLastGeneratedValues]);
|
||||
|
||||
/**
|
||||
* Clear all alias fields.
|
||||
*/
|
||||
const clearAliasFields = useCallback(() => {
|
||||
setValue('Alias.FirstName', '');
|
||||
setValue('Alias.LastName', '');
|
||||
setValue('Alias.NickName', '');
|
||||
setValue('Alias.Gender', '');
|
||||
setValue('Alias.BirthDate', '');
|
||||
}, [setValue]);
|
||||
|
||||
/**
|
||||
* Check if any alias fields have values.
|
||||
*/
|
||||
const hasAliasValues = watch('Alias.FirstName') || watch('Alias.LastName') || watch('Alias.NickName') || watch('Alias.Gender') || watch('Alias.BirthDate');
|
||||
|
||||
/**
|
||||
* Handle the generate random alias button press.
|
||||
@@ -224,8 +257,13 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
} else if (Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
await generateRandomAlias();
|
||||
}, [generateRandomAlias]);
|
||||
|
||||
if (hasAliasValues) {
|
||||
clearAliasFields();
|
||||
} else {
|
||||
await generateRandomAlias();
|
||||
}
|
||||
}, [generateRandomAlias, clearAliasFields, hasAliasValues]);
|
||||
|
||||
/**
|
||||
* Submit the form for either creating or updating a credential.
|
||||
@@ -355,8 +393,13 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
// Generate identity with gender preference
|
||||
const identity = identityGenerator.generateRandomIdentity(genderPreference);
|
||||
|
||||
// Set the username to the identity's nickname
|
||||
setValue('Username', identity.nickName);
|
||||
// Only overwrite username if it's empty or matches the last generated value
|
||||
const currentUsername = watch('Username') ?? '';
|
||||
if (!currentUsername || currentUsername === lastGeneratedValues.username) {
|
||||
setValue('Username', identity.nickName);
|
||||
// Update the tracking for username
|
||||
setLastGeneratedValues(prev => ({ ...prev, username: identity.nickName }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating random username:', error);
|
||||
Toast.show({
|
||||
@@ -445,13 +488,18 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
},
|
||||
generateButton: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
flexDirection: 'row',
|
||||
marginBottom: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
generateButtonPrimary: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
generateButtonSecondary: {
|
||||
backgroundColor: colors.textMuted,
|
||||
},
|
||||
generateButtonText: {
|
||||
color: colors.primarySurfaceText,
|
||||
fontWeight: '600',
|
||||
@@ -645,11 +693,20 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>{t('credentials.alias')}</ThemedText>
|
||||
<RobustPressable
|
||||
style={styles.generateButton}
|
||||
style={[
|
||||
styles.generateButton,
|
||||
hasAliasValues ? styles.generateButtonSecondary : styles.generateButtonPrimary
|
||||
]}
|
||||
onPress={handleGenerateRandomAlias}
|
||||
>
|
||||
<MaterialIcons name="auto-fix-high" size={20} color="#fff" />
|
||||
<ThemedText style={styles.generateButtonText}>{t('credentials.generateRandomAlias')}</ThemedText>
|
||||
<MaterialIcons
|
||||
name={hasAliasValues ? "clear" : "auto-fix-high"}
|
||||
size={20}
|
||||
color="#fff"
|
||||
/>
|
||||
<ThemedText style={styles.generateButtonText}>
|
||||
{hasAliasValues ? t('credentials.clearAliasFields') : t('credentials.generateRandomAlias')}
|
||||
</ThemedText>
|
||||
</RobustPressable>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
|
||||
@@ -69,17 +69,16 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
|
||||
if (dbContext.sqliteClient) {
|
||||
const settings = await dbContext.sqliteClient.getPasswordSettings();
|
||||
setCurrentSettings(settings);
|
||||
// Only set slider value from settings if we don't have a password value yet
|
||||
if (!hasSetInitialLength.current && isNewCredential) {
|
||||
setSliderValue(settings.Length);
|
||||
}
|
||||
// Always set slider value from loaded settings
|
||||
setSliderValue(settings.Length);
|
||||
hasSetInitialLength.current = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading password settings:', error);
|
||||
}
|
||||
};
|
||||
loadSettings();
|
||||
}, [dbContext.sqliteClient, isNewCredential]);
|
||||
}, [dbContext.sqliteClient]);
|
||||
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
|
||||
@@ -97,7 +97,7 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
setIsCustomDomain(!isKnownDomain);
|
||||
} else {
|
||||
setLocalPart(value);
|
||||
setIsCustomDomain(false);
|
||||
// Don't reset isCustomDomain here - preserve the current mode
|
||||
|
||||
// Set default domain if not already set
|
||||
if (!selectedDomain && !value.includes('@')) {
|
||||
@@ -112,6 +112,13 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
|
||||
// Handle local part changes
|
||||
const handleLocalPartChange = useCallback((newText: string) => {
|
||||
// If in custom domain mode, always pass through the full value
|
||||
if (isCustomDomain) {
|
||||
onChange(newText);
|
||||
// Stay in custom domain mode - don't auto-switch back
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if new value contains '@' symbol, if so, switch to custom domain mode
|
||||
if (newText.includes('@')) {
|
||||
setIsCustomDomain(true);
|
||||
@@ -120,10 +127,11 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
}
|
||||
|
||||
setLocalPart(newText);
|
||||
if (!isCustomDomain && selectedDomain) {
|
||||
// If the local part is empty, treat the whole field as empty
|
||||
if (!newText || newText.trim() === '') {
|
||||
onChange('');
|
||||
} else if (selectedDomain) {
|
||||
onChange(`${newText}@${selectedDomain}`);
|
||||
} else {
|
||||
onChange(newText);
|
||||
}
|
||||
}, [isCustomDomain, selectedDomain, onChange]);
|
||||
|
||||
@@ -131,7 +139,12 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
const selectDomain = useCallback((domain: string) => {
|
||||
setSelectedDomain(domain);
|
||||
const cleanLocalPart = localPart.includes('@') ? localPart.split('@')[0] : localPart;
|
||||
onChange(`${cleanLocalPart}@${domain}`);
|
||||
// If the local part is empty, treat the whole field as empty
|
||||
if (!cleanLocalPart || cleanLocalPart.trim() === '') {
|
||||
onChange('');
|
||||
} else {
|
||||
onChange(`${cleanLocalPart}@${domain}`);
|
||||
}
|
||||
setIsCustomDomain(false);
|
||||
setIsModalVisible(false);
|
||||
}, [localPart, onChange]);
|
||||
@@ -141,13 +154,28 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
const newIsCustom = !isCustomDomain;
|
||||
setIsCustomDomain(newIsCustom);
|
||||
|
||||
if (!newIsCustom && !value.includes('@')) {
|
||||
// Switching to domain chooser mode, add default domain
|
||||
if (newIsCustom) {
|
||||
// Switching to custom domain mode
|
||||
// If we have a domain-based value, extract just the local part
|
||||
if (value && value.includes('@')) {
|
||||
const [local] = value.split('@');
|
||||
onChange(local);
|
||||
setLocalPart(local);
|
||||
}
|
||||
} else {
|
||||
// Switching to domain chooser mode
|
||||
const defaultDomain = showPrivateDomains && privateEmailDomains[0]
|
||||
? privateEmailDomains[0]
|
||||
: PUBLIC_EMAIL_DOMAINS[0];
|
||||
onChange(`${localPart}@${defaultDomain}`);
|
||||
setSelectedDomain(defaultDomain);
|
||||
|
||||
// Only add domain if we have a local part
|
||||
if (localPart && localPart.trim()) {
|
||||
onChange(`${localPart}@${defaultDomain}`);
|
||||
} else if (value && !value.includes('@')) {
|
||||
// If we have a value without @, add the domain
|
||||
onChange(`${value}@${defaultDomain}`);
|
||||
}
|
||||
}
|
||||
}, [isCustomDomain, value, localPart, showPrivateDomains, privateEmailDomains, onChange]);
|
||||
|
||||
|
||||
@@ -5,16 +5,20 @@ import { initReactI18next } from 'react-i18next';
|
||||
import de from './locales/de.json';
|
||||
import en from './locales/en.json';
|
||||
import fi from './locales/fi.json';
|
||||
import he from './locales/he.json';
|
||||
import it from './locales/it.json';
|
||||
import nl from './locales/nl.json';
|
||||
import uk from './locales/uk.json';
|
||||
import zh from './locales/zh.json';
|
||||
|
||||
const resources = {
|
||||
de: { translation: de },
|
||||
en: { translation: en },
|
||||
fi: { translation: fi },
|
||||
he: { translation: he },
|
||||
nl: { translation: nl },
|
||||
it: { translation: it },
|
||||
uk: { translation: uk },
|
||||
zh: { translation: zh },
|
||||
};
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"randomAlias": "Random Alias",
|
||||
"manual": "Manual",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix",
|
||||
"useDomainChooser": "Use domain chooser",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"randomAlias": "Zufälliger Alias",
|
||||
"manual": "Manuell",
|
||||
"generateRandomAlias": "Zufällige Alias generieren",
|
||||
"clearAliasFields": "Alias-Felder löschen",
|
||||
"enterFullEmail": "Vollständige E-Mail-Adresse eingeben",
|
||||
"enterEmailPrefix": "E-Mail-Präfix eingeben",
|
||||
"useDomainChooser": "Domain-Auswahl verwenden",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"randomAlias": "Random Alias",
|
||||
"manual": "Manual",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix",
|
||||
"useDomainChooser": "Use domain chooser",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"randomAlias": "Random Alias",
|
||||
"manual": "Manual",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix",
|
||||
"useDomainChooser": "Use domain chooser",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"randomAlias": "Satunnainen Alias",
|
||||
"manual": "Käyttöopas",
|
||||
"generateRandomAlias": "Luo satunnainen alias",
|
||||
"clearAliasFields": "Tyhjennä aliaksen kentät",
|
||||
"enterFullEmail": "Syötä täysi sähköpostiosoite",
|
||||
"enterEmailPrefix": "Syötä sähköpostin etuliite",
|
||||
"useDomainChooser": "Käytä verkkotunnuksen valintaa",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"randomAlias": "Random Alias",
|
||||
"manual": "Manual",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix",
|
||||
"useDomainChooser": "Use domain chooser",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"cancel": "ביטול",
|
||||
"close": "Close",
|
||||
"close": "סגירה",
|
||||
"delete": "מחיקה",
|
||||
"save": "שמירה",
|
||||
"yes": "כן",
|
||||
@@ -15,8 +15,8 @@
|
||||
"copied": "הועתק ללוח הגזירים",
|
||||
"loadMore": "לטעון עוד",
|
||||
"use": "להשתמש",
|
||||
"confirm": "Confirm",
|
||||
"unknownError": "Unknown error"
|
||||
"confirm": "אישור",
|
||||
"unknownError": "שגיאה לא ידועה"
|
||||
},
|
||||
"auth": {
|
||||
"login": "כניסה",
|
||||
@@ -109,6 +109,7 @@
|
||||
"randomAlias": "כינוי אקראי",
|
||||
"manual": "ידני",
|
||||
"generateRandomAlias": "יצירת כינוי אקראי",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"enterFullEmail": "נא למלא כתובת דוא״ל מלאה",
|
||||
"enterEmailPrefix": "נא למלא קידומת דוא״ל",
|
||||
"useDomainChooser": "להשתמש בבורר שמות התחום",
|
||||
@@ -219,17 +220,17 @@
|
||||
"batteryOptimizationHelpDescription": "מיטוב הסוללה של Android מונע פינוי אמין של לוח הגזירים כשהיישום פועל ברקע. השבתת מיטוב הסוללה ל־AliasVault מאפשרת פינוי מדויק של לוח הגזירים ברקע ומאשרת אוטומטית הרשאות התראות הכרחיות.",
|
||||
"disableBatteryOptimization": "השבתת מיטוב סוללה",
|
||||
"identityGenerator": "מייצר זהויות",
|
||||
"passwordGenerator": "Password Generator",
|
||||
"importExport": "Import / Export",
|
||||
"importSectionTitle": "Import",
|
||||
"importSectionDescription": "Import your passwords from other password managers or from a previous AliasVault export.",
|
||||
"importWebNote": "To import credentials from existing password managers, please login to the web app. The import feature is currently only available on the web version.",
|
||||
"exportSectionTitle": "Export",
|
||||
"exportSectionDescription": "Export your vault data to a CSV file. This file can be used as a back-up and can also be imported into other password managers.",
|
||||
"exportCsvButton": "Export vault to CSV file",
|
||||
"exporting": "Exporting...",
|
||||
"exportConfirmTitle": "Export Vault",
|
||||
"exportWarning": "Warning: Exporting your vault to an unencrypted file will expose all of your passwords and sensitive information in plain text. Only do this on trusted devices and ensure you:\n\n• Store the exported file in a secure location\n• Delete the file when you no longer need it\n• Never share the exported file with others\n\nAre you sure you want to continue with the export?",
|
||||
"passwordGenerator": "יוצר סיסמאות",
|
||||
"importExport": "ייבוא / ייצוא",
|
||||
"importSectionTitle": "ייבוא",
|
||||
"importSectionDescription": "אפשר לייבא את הסיסמאות שלך ממנהלי סיסמאות אחרים או מנתונים שיוצאו מ־AliasVault בעבר.",
|
||||
"importWebNote": "כדי לייבא פרטי גישה ממנהלי סיסמאות קיימים, נא להיכנס דרך יישום הדפדפן. יכולת הייבוא זמינה רק במהדורת הדפדפן.",
|
||||
"exportSectionTitle": "ייצוא",
|
||||
"exportSectionDescription": "ייצוא נתוני הכספת שלך לקובץ CSV. אפשר להשתמש בקובץ הזה כגיבוי וגם לייבא אותו למנהלי סיסמאות אחרים.",
|
||||
"exportCsvButton": "ייצוא כספת לקובץ CSV",
|
||||
"exporting": "מתבצע ייצוא…",
|
||||
"exportConfirmTitle": "ייצוא כספת",
|
||||
"exportWarning": "אזהרה: ייצוא הכספת שלך לקובץ לא מוצפן תחשוף את כל הסיסמאות והפרטים הרגישים שלך בטקסט גלוי. יש לעשות זאת על מכשירים מהימנים וגם לוודא:\n\n• שאחסנת את הקובץ המיוצר במקום בטוח\n• שמחקת את הקובץ אם אין בו צורך עוד\n • לעולם לא לשתף את הקובץ המיוצא עם אחרים\n\nלהמשיך בייצוא?",
|
||||
"security": "אבטחה",
|
||||
"appVersion": "גרסת היישום היא {{version}} ({{url}})",
|
||||
"autoLockOptions": {
|
||||
@@ -286,8 +287,8 @@
|
||||
}
|
||||
},
|
||||
"passwordGeneratorSettings": {
|
||||
"description": "Configure the default settings used when generating new passwords. These settings will be used for all new passwords unless overridden for specific entries.",
|
||||
"preview": "Preview"
|
||||
"description": "הגדרת תצורת ברירת המחדל לשימוש בעת יצירת סיסמאות חדשות. ההגדרות האלו תשמשנה לכל הסיסמאות החדשות אלא אם כן ההגדרות האלו נדרסו עבור רשומות מסוימות.",
|
||||
"preview": "תצוגה מקדימה"
|
||||
},
|
||||
"securitySettings": {
|
||||
"title": "אבטחה",
|
||||
@@ -439,9 +440,9 @@
|
||||
"retryingConnection": "מתבצע ניסיון להתחבר מחדש…"
|
||||
},
|
||||
"offline": {
|
||||
"banner": "Offline mode (read-only)",
|
||||
"backOnline": "Back online",
|
||||
"stillOffline": "Still offline"
|
||||
"banner": "מצב בלתי מקוון (קריאה בלבד)",
|
||||
"backOnline": "להתחבר בחזרה",
|
||||
"stillOffline": "עדיין בלתי מקוון"
|
||||
},
|
||||
"alerts": {
|
||||
"syncIssue": "תקלת סנכרון",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"randomAlias": "Alias casuale",
|
||||
"manual": "Manuale",
|
||||
"generateRandomAlias": "Genera alias casuale",
|
||||
"clearAliasFields": "Cancella Campi Alias",
|
||||
"enterFullEmail": "Inserisci l'indirizzo email completo",
|
||||
"enterEmailPrefix": "Inserisci prefisso email",
|
||||
"useDomainChooser": "Usa selettore di dominio",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"randomAlias": "Alias",
|
||||
"manual": "Handmatig",
|
||||
"generateRandomAlias": "Genereer willekeurige alias",
|
||||
"clearAliasFields": "Leeg alias velden",
|
||||
"enterFullEmail": "Voer volledig e-mailadres in",
|
||||
"enterEmailPrefix": "Emailvoorvoegsel invoeren",
|
||||
"useDomainChooser": "Domein kiezen",
|
||||
|
||||
507
apps/mobile-app/i18n/locales/pt.json
Normal file
@@ -0,0 +1,507 @@
|
||||
{
|
||||
"common": {
|
||||
"cancel": "Cancelar",
|
||||
"close": "Fechar",
|
||||
"delete": "Excluir",
|
||||
"save": "Salvar",
|
||||
"yes": "Sim",
|
||||
"no": "Não",
|
||||
"ok": "Ok",
|
||||
"continue": "Continuar",
|
||||
"loading": "Carregando...",
|
||||
"error": "Erro",
|
||||
"success": "Sucesso",
|
||||
"never": "Nunca",
|
||||
"copied": "Copiado para a área de transferência",
|
||||
"loadMore": "Carregar mais",
|
||||
"use": "Utilizar",
|
||||
"confirm": "Confirmar",
|
||||
"unknownError": "Erro desconhecido"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"logout": "Sair",
|
||||
"username": "Usuário ou e-mail",
|
||||
"password": "Senha",
|
||||
"authCode": "Código de Autenticação",
|
||||
"unlock": "Desbloquear",
|
||||
"unlocking": "Desbloqueando...",
|
||||
"loggingIn": "Fazendo login",
|
||||
"validatingCredentials": "Validando credenciais",
|
||||
"syncingVault": "Sincronizando cofre",
|
||||
"verifyingAuthCode": "Verificando código de autenticação",
|
||||
"verify": "Verificar",
|
||||
"unlockVault": "Desbloquear cofre",
|
||||
"enterPassword": "Digite sua senha para desbloquear o cofre",
|
||||
"enterPasswordPlaceholder": "Senha",
|
||||
"enterAuthCode": "Digite o código de 6 dígitos",
|
||||
"usernamePlaceholder": "nome / nome@empresa.com",
|
||||
"passwordPlaceholder": "Digite sua senha",
|
||||
"enableBiometric": "Habilitar {{biometric}}?",
|
||||
"biometricPrompt": "Gostaria de utilizar {{biometric}} para desbloquear seu cofre?",
|
||||
"tryBiometricAgain": "Tente novamente com {{biometric}}",
|
||||
"authCodeNote": "Nota: se você não tem acesso ao seu aparelho de verificação, você pode resetar seu 2FA com um código de recuperação fazendo login no site.",
|
||||
"errors": {
|
||||
"credentialsRequired": "Usuário e senha são obrigatórios",
|
||||
"invalidAuthCode": "Por favor digite o código de autenticação de 6 dígitos",
|
||||
"incorrectPassword": "Senha incorreta. Por favor tente novamente.",
|
||||
"enterPassword": "Por favor digite sua senha",
|
||||
"serverError": "Não foi possível conectar ao servidor do AliasVault. Por favor tente novamente mais tarde ou entre em contato com o suporte caso o problema persista.",
|
||||
"serverErrorSelfHosted": "Não foi possível conectar à API. Para instâncias self-hosted, por favor verifique se os endpoints da API podem ser chamados através de um navegador. Ele deve mostrar 'OK'.",
|
||||
"networkError": "Conexão falhou. Por favor verifique sua conexão com a internet e tente novamente.",
|
||||
"networkErrorSelfHosted": "Conexão falhou. Verifique sua conexão com a rede e a disponibilidade do servidor. Para instâncias self-hosted, por favor confirme que possue um certificado SSL válido instalado. Certificados self-signed não são suportados em celulares por questões de segurança.",
|
||||
"sessionExpired": "Sua sessão expirou. Por favor faça login novamente.",
|
||||
"tokenRefreshFailed": "Falha ao atualizar token de autenticação",
|
||||
"httpError": "Erro HTTP: {{status}}"
|
||||
},
|
||||
"confirmLogout": "Tem certeza que deseja sair? Você precisará fazer login novamente com sua senha mestre para acessar o cofre.",
|
||||
"noAccountYet": "Não tem conta ainda?",
|
||||
"createNewVault": "Criar novo cofre",
|
||||
"connectingTo": "Conectando à",
|
||||
"loggedInAs": "Logado como"
|
||||
},
|
||||
"vault": {
|
||||
"syncingVault": "Sincronizando cofre",
|
||||
"uploadingVaultToServer": "Fazendo upload do cofre para o servidor",
|
||||
"savingChangesToVault": "Salvando mudanças no cofre",
|
||||
"checkingForVaultUpdates": "Verificando atualizações do cofre",
|
||||
"executingOperation": "Realizando operação...",
|
||||
"checkingVaultUpdates": "Verificando atualizações do cofre",
|
||||
"syncingUpdatedVault": "Sincronizando cofre atualizado",
|
||||
"errors": {
|
||||
"failedToGetEncryptedDatabase": "Falha ao acessar dados criptografados",
|
||||
"usernameNotFound": "Usuário não encontrado",
|
||||
"vaultMergeRequired": "Junção de cofres necessária. Por favor realize o login via site para adicionar as atualizações pendentes ao cofre.",
|
||||
"vaultOutdated": "Seu cofre está desatualizado. Por favor realize login pelo site do AliasVault e siga as instruções.",
|
||||
"failedToUploadVault": "Falha ao enviar cofre ao servidor. Por favor tente novamente reabrindo o aplicativo.",
|
||||
"usernameNotFoundLoginAgain": "Usuário não encontrado. Por favor realize login novamente.",
|
||||
"errorDuringPasswordChange": "Erro ao atualizar a senha. Por favor realize login novamente para recuperar seu último cofre.",
|
||||
"failedToSyncVault": "Falha ao sincronizar cofre",
|
||||
"operationFailed": "Operação falhou",
|
||||
"versionNotSupported": "Esta versão do aplicativo AliasVault não é mais suportada pelo servidor. Por favor atualize seu aplicativo para a última versão.",
|
||||
"serverNeedsUpdate": "O servidor do AliasVault precisa ser atualizado para a última versão para poder utilizar este aplicativo. Por favor entre em contato com o suporte se precisar de ajuda.",
|
||||
"vaultDecryptFailed": "Cofre não pôde ser descriptografado, se o problema persistir por favor saia e realize login novamente.",
|
||||
"passwordChanged": "Sua senha mudou desde o último login. Por favor realize login novamente por questões de segurança."
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"title": "Credenciais",
|
||||
"addCredential": "Adicionar Credencial",
|
||||
"editCredential": "Editar Credencial",
|
||||
"deleteCredential": "Excluir Credencial",
|
||||
"deleteConfirm": "Tem certeza que deseja excluir esta credencial? Essa operação não pode ser desfeita.",
|
||||
"service": "Serviço",
|
||||
"serviceName": "Nome do Serviço",
|
||||
"serviceUrl": "URL do Serviço",
|
||||
"loginCredentials": "Credenciais de login",
|
||||
"username": "Usuário",
|
||||
"email": "E-mail",
|
||||
"alias": "Alias",
|
||||
"metadata": "Metadados",
|
||||
"firstName": "Primeiro Nome",
|
||||
"lastName": "Sobrenome",
|
||||
"nickName": "Apelido",
|
||||
"fullName": "Nome Completo",
|
||||
"gender": "Gênero",
|
||||
"birthDate": "Data de Nascimento",
|
||||
"birthDatePlaceholder": "AAAA-MM-DD",
|
||||
"notes": "Notas",
|
||||
"randomAlias": "Alias Aleatório",
|
||||
"manual": "Manual",
|
||||
"generateRandomAlias": "Gerar Alias Aleatório",
|
||||
"clearAliasFields": "Limpar Campos de Alias",
|
||||
"enterFullEmail": "Digite o endereço de e-mail completo",
|
||||
"enterEmailPrefix": "Digite o prefixo do e-mail",
|
||||
"useDomainChooser": "Utilizar escolhedor de domínio",
|
||||
"enterCustomDomain": "Digitar domínio personalizado",
|
||||
"selectEmailDomain": "Selecionar domínio de e-mail",
|
||||
"privateEmailTitle": "E-mail privado",
|
||||
"privateEmailAliasVaultServer": "Servidor AliasVault",
|
||||
"privateEmailDescription": "Criptografia E2E, totalmente privado.",
|
||||
"publicEmailTitle": "Provedores Públicos de E-mail Temporário",
|
||||
"publicEmailDescription": "Anônimo mas com privacidade limitada. Conteúdo do e-mail pode ser lido por qualquer um que souber o endereço.",
|
||||
"searchPlaceholder": "Pesquisar credenciais...",
|
||||
"noMatchingCredentials": "Nenhuma credencial foi encontrada",
|
||||
"noCredentialsFound": "Nenhuma credencial encontrada. Crie uma para iniciar. Dica: você também pode fazer login no site do AliasVault e importar credenciais de outros gerenciadores de senhas.",
|
||||
"recentEmails": "E-mails recentes",
|
||||
"loadingEmails": "Carregando emails...",
|
||||
"noEmailsYet": "Nenhum e-mail recebido ainda.",
|
||||
"offlineEmailsMessage": "Você está offline. Por favor reconecte à internet para carregar seus e-mails.",
|
||||
"emailLoadError": "Ocorreu um erro ao carregar os e-mails. Por favor tente novamente mais tarde.",
|
||||
"emailUnexpectedError": "Ocorreu um erro inesperado ao carregar e-mails. Por favor tente novamente mais tarde.",
|
||||
"password": "Senha",
|
||||
"passwordLength": "Tamanho da Senha",
|
||||
"changePasswordComplexity": "Configurações de Senha",
|
||||
"includeLowercase": "Minúsculas (a-z)",
|
||||
"includeUppercase": "Maiúsculas (A-Z)",
|
||||
"includeNumbers": "Números (0-9)",
|
||||
"includeSpecialChars": "Caracteres Especiais (!@#)",
|
||||
"avoidAmbiguousChars": "Evitar Caracteres Ambíguos",
|
||||
"deletingCredential": "Excluindo credencial...",
|
||||
"errorLoadingCredentials": "Erro ao carregar credenciais",
|
||||
"vaultSyncFailed": "Sincronização do cofre falhou",
|
||||
"vaultSyncedSuccessfully": "Cofre sincronizado com sucesso",
|
||||
"vaultUpToDate": "Cofre está atualizado",
|
||||
"offlineMessage": "Você está offline. Por favor conecte-se à internet para sincronizar seu cofre.",
|
||||
"credentialCreated": "Credencial Criada!",
|
||||
"credentialCreatedMessage": "Sua nova credencial foi adicionada ao seu cofre e está pronta para ser usada.",
|
||||
"credentialDetails": "Detalhes da Credencial",
|
||||
"emailPreview": "Prévia de E-mail",
|
||||
"switchBackToBrowser": "Volte ao navegador para continuar.",
|
||||
"twoFactorAuth": "Autenticação de dois fatores",
|
||||
"totpCode": "Código TOTP",
|
||||
"attachments": "Anexos",
|
||||
"loadingAttachments": "Carregando anexos...",
|
||||
"addAttachments": "Adicionar Anexo",
|
||||
"deleteAttachment": "Excluir",
|
||||
"toasts": {
|
||||
"credentialUpdated": "Credencial atualizada com sucesso",
|
||||
"credentialCreated": "Credencial criada com sucesso",
|
||||
"credentialDeleted": "Credencial excluída com sucesso"
|
||||
},
|
||||
"createNewAliasFor": "Criar novo alias para",
|
||||
"errors": {
|
||||
"loadFailed": "Falha ao carregar crerencial",
|
||||
"generateUsernameFailed": "Falha ao gerar usuário",
|
||||
"generatePasswordFailed": "Falha ao gerar senha"
|
||||
},
|
||||
"contextMenu": {
|
||||
"title": "Opções da Credencial",
|
||||
"edit": "Editar",
|
||||
"delete": "Excluir",
|
||||
"copyUsername": "Copiar Usuário",
|
||||
"copyEmail": "Copiar E-mail",
|
||||
"copyPassword": "Copiar Senha"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configurações",
|
||||
"iosAutofill": "Autopreenchimento no iOS",
|
||||
"iosAutofillSettings": {
|
||||
"headerText": "Você pode configurar o AliasVault para preencher senhas nativamente pelo iOS. Siga as instruções abaixo para habilitar.",
|
||||
"howToEnable": "Como habilitar:",
|
||||
"step1": "1. Abra as Configurações do iOS através do botão abaixo",
|
||||
"step2": "2. Vá até \"Geral\"",
|
||||
"step3": "3. Clique \"Autopreenchimento & Senhas\"",
|
||||
"step4": "4. Habilite \"AliasVault\"",
|
||||
"step5": "5. Desabilite outros provedores de senha (ex. \"iCloud Passwords\") para evitar conflitos",
|
||||
"openIosSettings": "Abrir Configurações do iOS",
|
||||
"alreadyConfigured": "Já configurei",
|
||||
"warningText": "Nota: Você terá que autenticar com Face ID/Touch ID ou a senha do seu dispositivo quando utilizar o autopreenchimento."
|
||||
},
|
||||
"androidAutofill": "Autopreenchimento no Android",
|
||||
"androidAutofillSettings": {
|
||||
"warningTitle": "⚠️ Funcionalidade Experimental",
|
||||
"warningDescription": "Autopreenchimento no Android está atualmente em fase experimental.",
|
||||
"warningLink": "Leia mais sobre isso aqui",
|
||||
"headerText": "Você pode configurar o AliasVault para preencher senhas nativamente no Android. Siga as instruções abaixo para habilitar.",
|
||||
"howToEnable": "Como habilitar:",
|
||||
"step1": "1. Abra as Configurações do Android através do botão abaixo, e troque o \"serviço de autopreenchimento preferido\" para \"AliasVault\"",
|
||||
"openAutofillSettings": "Abrir Configurações de Autopreenchimento",
|
||||
"buttonTip": "Se o botão acima não funcionar pode estar bloqueado pelas configurações de segurança. Você pode ir manualmente às Configurações do Android → Configurações Gerais → Senhas e autopreenchimento.",
|
||||
"step2": "2. Alguns aplicativos, ex. Google Chrome, podem solicitar configurações manuais nas suas configurações para habilitar autopreenchimento de aplicativos terceiros. Porém, a maioria dos aplicativos deve funcionar com autopreenchimento por padrão.",
|
||||
"alreadyConfigured": "Já configurei"
|
||||
},
|
||||
"vaultUnlock": "Método de Desbloqueio do Cofre",
|
||||
"autoLock": "Tempo para Bloqueio Automático",
|
||||
"clipboardClear": "Limpar área de transferência",
|
||||
"clipboardClearDescription": "Limpa automaticamente as senhas e dados sensíveis copiados da sua área de transferência após o tempo especificado.",
|
||||
"clipboardClearAndroidWarning": "Nota: alguns dispositivos Android têm histórico de área de transferência habilitado, o que pode manter dados copiados anteriormente, mesmo após o AliasVault limpar a área de transferência. O AliasVault só pode remover o item mais recente, porém outros itens podem permanecer visíveis no histórico. Por questões de segurança, recomendamos desabilitar qualquer histórico de área de transferência nas configurações do seu dispositivo.",
|
||||
"clipboardClearOptions": {
|
||||
"never": "Nunca",
|
||||
"5seconds": "5 segundos",
|
||||
"10seconds": "10 segundos",
|
||||
"15seconds": "15 segundos",
|
||||
"30seconds": "30 segundos"
|
||||
},
|
||||
"batteryOptimizationHelpTitle": "Habilitar Limpeza de Área de Transferência no Background",
|
||||
"batteryOptimizationActive": "Otimização de bateria está bloqueando tarefas em background",
|
||||
"batteryOptimizationDisabled": "Limpeza da área de transferência em background habilitada",
|
||||
"batteryOptimizationHelpDescription": "A otimização de bateria do Android previne a limpeza correta da área de transferência enquanto o aplicativo está em background. Desabilitar a otimização de bateria para o AliasVault permite a limpeza correta da área de transferência em background e automaticamente autoriza as permissões de alarme.",
|
||||
"disableBatteryOptimization": "Desabilitar otimização de bateria",
|
||||
"identityGenerator": "Gerador de Identidade",
|
||||
"passwordGenerator": "Gerador de Senha",
|
||||
"importExport": "Importar / Exportar",
|
||||
"importSectionTitle": "Importar",
|
||||
"importSectionDescription": "Importe suas senhas de outros gerenciadores de senhas ou de dados exportados anteriormente do AliasVault.",
|
||||
"importWebNote": "Para importar credenciais de um gerenciador de senhas existente, por favor realize o login pelo site. Por enquanto, a função de importar só está disponível pela versão web.",
|
||||
"exportSectionTitle": "Exportar",
|
||||
"exportSectionDescription": "Exporte seu cofre para um arquivo CSV. Este arquivo pode ser utilizado como backup e também pode ser importado em outros gerenciadores de senhas.",
|
||||
"exportCsvButton": "Exportar cofre para arquivo CSV",
|
||||
"exporting": "Exportando...",
|
||||
"exportConfirmTitle": "Exportar Cofre",
|
||||
"exportWarning": "Aviso: Exportar seu cofre para um arquivo descriptografado vai expor todas as suas senhas e dados sensíveis em texto. Realize isso apenas em dispositivos confiados e garanta que:\n\n• Guarde o arquivo em um local seguro\n• Exclua o arquivo quando não precisar mais\n• Nunca compartilhe o arquivo exportado com outras pessoas\n\nTem certeza que deseja continuar com o exporte?",
|
||||
"security": "Segurança",
|
||||
"appVersion": "Versão do aplicativo {{version}} ({{url}})",
|
||||
"autoLockOptions": {
|
||||
"never": "Nunca",
|
||||
"5seconds": "5 segundos",
|
||||
"30seconds": "30 segundos",
|
||||
"1minute": "1 minuto",
|
||||
"15minutes": "15 minutos",
|
||||
"30minutes": "30 minutos",
|
||||
"1hour": "1 hora",
|
||||
"4hours": "4 horas",
|
||||
"8hours": "8 horas"
|
||||
},
|
||||
"language": "Idioma",
|
||||
"languageSystemMessage": "Para mudar o idioma do aplicativo, configure o idioma preferido para o AliasVault nas configurações do seu dispositivo.",
|
||||
"openSettings": "Abrir Configurações",
|
||||
"vaultUnlockSettings": {
|
||||
"description": "Escolha como você quer desbloquear seu cofre.",
|
||||
"biometrics": "Biometria",
|
||||
"faceId": "Face ID",
|
||||
"touchId": "Touch ID",
|
||||
"faceIdTouchId": "Face ID / Touch ID",
|
||||
"biometricEnabled": "{{biometric}} foi ativado com sucesso",
|
||||
"biometricNotAvailable": "{{biometric}} Não Disponível",
|
||||
"biometricDisabledMessage": "{{biometric}} está desabitado para o AliasVault. Para utilizar, por favor primeiro habilite nas configurações do seu dispositivo.",
|
||||
"biometricHelp": "A chave de descriptografia do seu cofre será salva seguramente no seu dispositivo local na {{keystore}} e pode ser acessada com {{biometric}}.",
|
||||
"biometricUnavailableHelp": "{{biometric}} não está disponível. Clique para abrir as configurações e/ou vá às configurações do seu dispositivo para habilitar e configurar.",
|
||||
"passwordHelp": "Re-digite sua senha mestre para desbloquear seu cofre. Isso sempre estará disponível como uma opção alternativa.",
|
||||
"keystoreIOS": "iOS Keychain",
|
||||
"keystoreAndroid": "Android Keystore"
|
||||
},
|
||||
"autoLockSettings": {
|
||||
"description": "Escolha por quanto tempo o aplicativos pode ficar em background antes de solicitar re-autenticação. Você precisará usar o Face ID ou digitar sua senha para desbloquear seu cofre novamente."
|
||||
},
|
||||
"identityGeneratorSettings": {
|
||||
"description": "Configure o idioma padrão e preferência de gênero para geração de novas identidades.",
|
||||
"languageSection": "Idioma",
|
||||
"languageDescription": "Defina o idioma que será utilizado ao gerar novas identidades.",
|
||||
"genderSection": "Gênero",
|
||||
"genderDescription": "Defina a preferência de gênero para geração de novas identidades.",
|
||||
"languageOptions": {
|
||||
"english": "Inglês",
|
||||
"dutch": "Holandês"
|
||||
},
|
||||
"genderOptions": {
|
||||
"random": "Aleatório",
|
||||
"male": "Masculino",
|
||||
"female": "Feminino"
|
||||
},
|
||||
"errors": {
|
||||
"loadFailed": "Falha ao carregar configurações do gerador.",
|
||||
"languageUpdateFailed": "Falha ao atualizar configurações de idioma.",
|
||||
"genderUpdateFailed": "Falha ao atualizar configurações de gênero."
|
||||
}
|
||||
},
|
||||
"passwordGeneratorSettings": {
|
||||
"description": "Configure as configurações padrão usadas para gerar novas senhas. Estas configurações serão usadas para todas as novas senhas a menos que seja sobrescrito para itens específicos.",
|
||||
"preview": "Prévia"
|
||||
},
|
||||
"securitySettings": {
|
||||
"title": "Segurança",
|
||||
"description": "Gerencie as configurações de sua conta e de segurança do seu cofre.",
|
||||
"changeMasterPassword": "Alterar Senha Mestre",
|
||||
"activeSessionsTitle": "Sessões Ativas",
|
||||
"recentAuthLogs": "Logs de Autenticação Recente",
|
||||
"deleteAccountTitle": "Excluir Conta",
|
||||
"changePassword": {
|
||||
"headerText": "Alterar sua senha mestre também irá alterar as chaves de criptografia do cofre. É aconselhável alterar sua senha mestre periodicamente para manter seu cofre seguro.",
|
||||
"currentPassword": "Senha Atual",
|
||||
"newPassword": "Nova Senha",
|
||||
"confirmNewPassword": "Confirmar Nova Senha",
|
||||
"enterCurrentPassword": "Digite a senha atual",
|
||||
"enterNewPassword": "Digite a nova senha",
|
||||
"changePassword": "Mudar Senha",
|
||||
"fillAllFields": "Por favor preencha todos os campos",
|
||||
"passwordsDoNotMatch": "Novas senhas não são iguais",
|
||||
"userNotAuthenticated": "Usuário não está autenticado",
|
||||
"initiatingChange": "Iniciando troca de senha...",
|
||||
"currentPasswordIncorrect": "Senha atual incorreta",
|
||||
"passwordChangedSuccessfully": "Senha atualizada com sucesso",
|
||||
"failedToChange": "Falha ao atualizar senha. Por favor tente novamente."
|
||||
},
|
||||
"activeSessions": {
|
||||
"headerText": "Abaixo está uma lista de dispositivos onde sua conta está atualmente logada ou tem uma sessão ativa. Você pode deslogar de qualquer uma dessas sessões por aqui.",
|
||||
"noSessions": "Nenhuma sessão ativa",
|
||||
"revoke": "Revogar",
|
||||
"revokeSession": "Revogar Sessão",
|
||||
"revokeConfirmation": "Tem certeza que quer revogar esta sessão? Isso vai deslogar sua conta do dispositivo selecionado.",
|
||||
"sessionRevoked": "Sessão revogada com sucesso",
|
||||
"failedToRevoke": "Falha ao revogar sessão",
|
||||
"failedToLoad": "Falha ao carregar sessões ativas",
|
||||
"lastActive": "Última atividade",
|
||||
"expires": "Expira"
|
||||
},
|
||||
"authLogs": {
|
||||
"headerText": "Abaixo você pode ter uma visão de tentativas de login recentes na sua conta.",
|
||||
"noLogs": "No auth logs found",
|
||||
"success": "Success",
|
||||
"failed": "Failed",
|
||||
"time": "Time",
|
||||
"device": "Device",
|
||||
"ipAddress": "IP Address",
|
||||
"client": "Client",
|
||||
"failedToLoad": "Failed to load auth logs"
|
||||
},
|
||||
"deleteAccount": {
|
||||
"headerText": "Deleting your account will immediately and permanently delete all of your data.",
|
||||
"warningText": "Warning: This action cannot be undone. All your data will be permanently deleted.",
|
||||
"finalWarning": "Final warning: Enter your password to permanently delete your account.",
|
||||
"warningVaults": "All encrypted vaults which includes all of your credentials will be permanently deleted",
|
||||
"warningAliases": "Your email aliases will be orphaned and cannot be claimed by other users",
|
||||
"warningRecovery": "Your account cannot be recovered after deletion",
|
||||
"irreversibleWarning": "Account deletion is irreversible and cannot be undone. Pressing the button below will delete your account immediately and permanently.",
|
||||
"enterUsername": "Enter your username to continue",
|
||||
"password": "Password",
|
||||
"enterPassword": "Enter password",
|
||||
"deleteAccount": "Delete Account",
|
||||
"confirmationMessage": "Are you absolutely sure you want to delete your account? This action cannot be undone.",
|
||||
"usernameDoesNotMatch": "Username does not match",
|
||||
"verifyingPassword": "Verifying password...",
|
||||
"currentPasswordIncorrect": "Current password is not correct",
|
||||
"initiatingDeletion": "Initiating account deletion",
|
||||
"verifyingWithServer": "Verifying with server",
|
||||
"deletingAccount": "Deleting account",
|
||||
"accountDeleted": "Account deleted successfully",
|
||||
"failedToDelete": "Failed to delete account. Please try again.",
|
||||
"usernameNotFound": "Username not found. Please login again."
|
||||
}
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"credentials": "Credentials",
|
||||
"emails": "Emails",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
"emailDetails": "Email Details",
|
||||
"subject": "Subject:",
|
||||
"date": "Date:",
|
||||
"from": "From:",
|
||||
"to": "To:",
|
||||
"attachments": "Attachments",
|
||||
"deleteEmail": "Delete Email",
|
||||
"deleteEmailConfirm": "Are you sure you want to delete this email? This action is permanent and cannot be undone.",
|
||||
"emailNotFound": "Email not found",
|
||||
"noPlainText": "This email does not contain any plain-text.",
|
||||
"sizeKB": "KB",
|
||||
"offlineMessage": "You are offline. Please connect to the internet to load your emails.",
|
||||
"emptyMessage": "You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.",
|
||||
"time": {
|
||||
"justNow": "just now",
|
||||
"minutesAgo_single": "{{count}} min ago",
|
||||
"minutesAgo_plural": "{{count}} mins ago",
|
||||
"hoursAgo_single": "{{count}} hr ago",
|
||||
"hoursAgo_plural": "{{count}} hrs ago",
|
||||
"yesterday": "yesterday"
|
||||
},
|
||||
"errors": {
|
||||
"generic": "An error occurred",
|
||||
"loadFailed": "Failed to load emails",
|
||||
"deleteFailed": "Failed to delete email",
|
||||
"dbNotAvailable": "Database context or email not available",
|
||||
"decryptFailed": "Failed to decrypt attachment",
|
||||
"downloadFailed": "Failed to download attachment"
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format",
|
||||
"invalidEmailFormat": "Invalid email format"
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "The current chosen email address is already in use. Please change the email address by editing this credential.",
|
||||
"CLAIM_DOES_NOT_EXIST": "An error occurred while trying to load the emails. Please try to edit and save the credential entry to synchronize the database, then try again.",
|
||||
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",
|
||||
"ACCOUNT_LOCKED": "Account temporarily locked due to too many failed attempts. Please try again later.",
|
||||
"ACCOUNT_BLOCKED": "Your account has been disabled. If you believe this is a mistake, please contact support.",
|
||||
"USER_NOT_FOUND": "Invalid username or password. Please try again.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Invalid authenticator code. Please try again.",
|
||||
"INVALID_RECOVERY_CODE": "Invalid recovery code. Please try again.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Refresh token is required.",
|
||||
"INVALID_REFRESH_TOKEN": "Invalid refresh token.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token revoked successfully.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "New account registration is currently disabled on this server. Please contact the administrator.",
|
||||
"USERNAME_REQUIRED": "Username is required.",
|
||||
"USERNAME_ALREADY_IN_USE": "Username is already in use.",
|
||||
"USERNAME_AVAILABLE": "Username is available.",
|
||||
"USERNAME_MISMATCH": "Username does not match the current user.",
|
||||
"PASSWORD_MISMATCH": "The provided password does not match your current password.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Account successfully deleted.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Username cannot be empty or whitespace.",
|
||||
"USERNAME_TOO_SHORT": "Username too short: must be at least 3 characters long.",
|
||||
"USERNAME_TOO_LONG": "Username too long: cannot be longer than 40 characters.",
|
||||
"USERNAME_INVALID_EMAIL": "Invalid email address.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Username is invalid, can only contain letters or digits.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Your vault is not up-to-date. Please synchronize your vault and try again.",
|
||||
"INTERNAL_SERVER_ERROR": "Internal server error.",
|
||||
"VAULT_ERROR": "The local vault is not up-to-date. Please synchronize your vault by refreshing the page and try again."
|
||||
},
|
||||
"app": {
|
||||
"status": {
|
||||
"unlockingVault": "Unlocking vault",
|
||||
"decryptingVault": "Decrypting vault",
|
||||
"openingVaultReadOnly": "Opening vault in read-only mode",
|
||||
"retryingConnection": "Retrying connection..."
|
||||
},
|
||||
"offline": {
|
||||
"banner": "Offline mode (read-only)",
|
||||
"backOnline": "Back online",
|
||||
"stillOffline": "Still offline"
|
||||
},
|
||||
"alerts": {
|
||||
"syncIssue": "Sync Issue",
|
||||
"syncIssueMessage": "The AliasVault server could not be reached and your vault could not be synced. Would you like to open your local vault in read-only mode or retry the connection?",
|
||||
"openLocalVault": "Open Local Vault",
|
||||
"retrySync": "Retry Sync"
|
||||
},
|
||||
"navigation": {
|
||||
"login": "Login",
|
||||
"loginSettings": "Login Settings",
|
||||
"notFound": "Not Found"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Page not found",
|
||||
"message": "This page has been moved or deleted.",
|
||||
"goHome": "Go back to the start"
|
||||
},
|
||||
"appName": "AliasVault",
|
||||
"reinitialize": {
|
||||
"vaultAutoLockedMessage": "Vault auto-locked after timeout.",
|
||||
"attemptingToUnlockMessage": "Attempting to unlock."
|
||||
},
|
||||
"loginSettings": {
|
||||
"title": "API Connection",
|
||||
"aliasvaultNet": "Aliasvault.net",
|
||||
"selfHosted": "Self-hosted",
|
||||
"customApiUrl": "Custom API URL",
|
||||
"customApiUrlPlaceholder": "https://my-aliasvault-instance.com/api",
|
||||
"version": "Version: {{version}}"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "Upgrade Vault",
|
||||
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
|
||||
"versionInformation": "Version Information",
|
||||
"yourVault": "Your vault:",
|
||||
"newVersion": "New version:",
|
||||
"upgrade": "Upgrade",
|
||||
"upgrading": "Upgrading...",
|
||||
"logout": "Logout",
|
||||
"whatsNew": "What's New",
|
||||
"whatsNewDescription": "An upgrade is required to support the following changes:",
|
||||
"noDescriptionAvailable": "No description available for this version.",
|
||||
"status": {
|
||||
"preparingUpgrade": "Preparing upgrade...",
|
||||
"vaultAlreadyUpToDate": "Vault is already up to date",
|
||||
"startingDatabaseTransaction": "Starting database transaction...",
|
||||
"applyingDatabaseMigrations": "Applying database migrations...",
|
||||
"applyingMigration": "Applying migration {{current}} of {{total}}...",
|
||||
"committingChanges": "Committing changes..."
|
||||
},
|
||||
"alerts": {
|
||||
"unableToGetVersionInfo": "Unable to get version information. Please try again.",
|
||||
"selfHostedServer": "Self-Hosted Server",
|
||||
"selfHostedWarning": "If you're using a self-hosted server, make sure to also update your self-hosted instance as otherwise logging in to the web client will stop working.",
|
||||
"continueUpgrade": "Continue Upgrade",
|
||||
"upgradeFailed": "Upgrade Failed",
|
||||
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
|
||||
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"cancel": "Отмена",
|
||||
"close": "Close",
|
||||
"close": "Закрыть",
|
||||
"delete": "Удалить",
|
||||
"save": "Сохранить",
|
||||
"yes": "Да",
|
||||
@@ -15,8 +15,8 @@
|
||||
"copied": "Скопировано в буфер обмена",
|
||||
"loadMore": "Загрузить ещё",
|
||||
"use": "Использовать",
|
||||
"confirm": "Confirm",
|
||||
"unknownError": "Unknown error"
|
||||
"confirm": "Подтвердить",
|
||||
"unknownError": "Неизвестная ошибка"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Войти",
|
||||
@@ -109,6 +109,7 @@
|
||||
"randomAlias": "Случайный псевдоним",
|
||||
"manual": "Инструкция",
|
||||
"generateRandomAlias": "Сгенерировать случайный псевдоним",
|
||||
"clearAliasFields": "Очистить поля псевдонимов",
|
||||
"enterFullEmail": "Введите полный адрес электронной почты",
|
||||
"enterEmailPrefix": "Введите префикс электронной почты",
|
||||
"useDomainChooser": "Использовать выбор домена",
|
||||
@@ -219,17 +220,17 @@
|
||||
"batteryOptimizationHelpDescription": "Оптимизация заряда батареи в Android предотвращает надежную очистку буфера обмена, когда приложение работает в фоновом режиме. Отключение оптимизации заряда батареи для AliasVault обеспечивает точную очистку буфера обмена в фоновом режиме и автоматически предоставляет необходимые разрешения для оповещения.",
|
||||
"disableBatteryOptimization": "Отключить оптимизацию заряда батареи",
|
||||
"identityGenerator": "Генератор личности",
|
||||
"passwordGenerator": "Password Generator",
|
||||
"importExport": "Import / Export",
|
||||
"importSectionTitle": "Import",
|
||||
"importSectionDescription": "Import your passwords from other password managers or from a previous AliasVault export.",
|
||||
"importWebNote": "To import credentials from existing password managers, please login to the web app. The import feature is currently only available on the web version.",
|
||||
"exportSectionTitle": "Export",
|
||||
"exportSectionDescription": "Export your vault data to a CSV file. This file can be used as a back-up and can also be imported into other password managers.",
|
||||
"exportCsvButton": "Export vault to CSV file",
|
||||
"exporting": "Exporting...",
|
||||
"exportConfirmTitle": "Export Vault",
|
||||
"exportWarning": "Warning: Exporting your vault to an unencrypted file will expose all of your passwords and sensitive information in plain text. Only do this on trusted devices and ensure you:\n\n• Store the exported file in a secure location\n• Delete the file when you no longer need it\n• Never share the exported file with others\n\nAre you sure you want to continue with the export?",
|
||||
"passwordGenerator": "Генератор паролей",
|
||||
"importExport": "Импорт / экспорт",
|
||||
"importSectionTitle": "Импорт",
|
||||
"importSectionDescription": "Импортируйте свои пароли из других менеджеров паролей или из предыдущего экспорта AliasVault.",
|
||||
"importWebNote": "Импорт из других менеджеров паролей доступен только в веб‑версии. Пожалуйста, войдите в веб‑приложение.",
|
||||
"exportSectionTitle": "Экспорт",
|
||||
"exportSectionDescription": "Экспортируйте данные вашего хранилища в CSV-файл. Этот файл можно использовать как резервную копию или импортировать в другие менеджеры паролей.",
|
||||
"exportCsvButton": "Экспорт хранилища в CSV-файл",
|
||||
"exporting": "Экспорт...",
|
||||
"exportConfirmTitle": "Экспортировать хранилище",
|
||||
"exportWarning": "Предупреждение: Экспорт хранилища в незашифрованный файл приведёт к тому, что все ваши пароли и конфиденциальная информация будут доступны в открытом виде. Делайте это только на доверенных устройствах и убедитесь, что вы:\n\n• Храните экспортированный файл в безопасном месте\n• Удаляете файл, когда он больше не нужен\n• Никогда не передаёте файл другим людям\n\nВы уверены, что хотите продолжить экспорт?",
|
||||
"security": "Безопасность",
|
||||
"appVersion": "Версия приложения {{version}} ({{url}})",
|
||||
"autoLockOptions": {
|
||||
@@ -286,8 +287,8 @@
|
||||
}
|
||||
},
|
||||
"passwordGeneratorSettings": {
|
||||
"description": "Configure the default settings used when generating new passwords. These settings will be used for all new passwords unless overridden for specific entries.",
|
||||
"preview": "Preview"
|
||||
"description": "Настройте параметры по умолчанию, которые будут использоваться при генерации новых паролей. Эти настройки будут применяться ко всем новым паролям, если только они не будут изменены для конкретных записей.",
|
||||
"preview": "Предпросмотр"
|
||||
},
|
||||
"securitySettings": {
|
||||
"title": "Безопасность",
|
||||
@@ -439,9 +440,9 @@
|
||||
"retryingConnection": "Повторная попытка подключения..."
|
||||
},
|
||||
"offline": {
|
||||
"banner": "Offline mode (read-only)",
|
||||
"backOnline": "Back online",
|
||||
"stillOffline": "Still offline"
|
||||
"banner": "Офлайн режим (только для чтения)",
|
||||
"backOnline": "Восстановить подключение",
|
||||
"stillOffline": "Нет подключения"
|
||||
},
|
||||
"alerts": {
|
||||
"syncIssue": "Проблема с синхронизацией",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"randomAlias": "Random Alias",
|
||||
"manual": "Manual",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix",
|
||||
"useDomainChooser": "Use domain chooser",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"randomAlias": "Random Alias",
|
||||
"manual": "Manual",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"enterFullEmail": "E-posta adresinizi girin",
|
||||
"enterEmailPrefix": "E-posta önekini girin",
|
||||
"useDomainChooser": "Alan adı seçiciyi kullan",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"randomAlias": "Випадковий псевдонім",
|
||||
"manual": "Посібник",
|
||||
"generateRandomAlias": "Генерувати випадковий псевдонім",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"enterFullEmail": "Введіть повну електронну адресу",
|
||||
"enterEmailPrefix": "Введіть префікс електронної адреси",
|
||||
"useDomainChooser": "Використовувати засіб вибору домену",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"randomAlias": "随机别名",
|
||||
"manual": "手动输入",
|
||||
"generateRandomAlias": "生成随机别名",
|
||||
"clearAliasFields": "清除别名字段",
|
||||
"enterFullEmail": "输入完整邮箱地址",
|
||||
"enterEmailPrefix": "输入邮箱前缀",
|
||||
"useDomainChooser": "使用域名选择器",
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
@@ -9,7 +9,10 @@
|
||||
{
|
||||
"layers" : [
|
||||
{
|
||||
"fill" : "automatic",
|
||||
"blend-mode" : "overlay",
|
||||
"fill" : {
|
||||
"automatic-gradient" : "display-p3:0.90471,0.76358,0.48553,1.00000"
|
||||
},
|
||||
"glass" : true,
|
||||
"hidden" : false,
|
||||
"image-name" : "icon-1024.png",
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 60;
|
||||
objectVersion = 70;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -22,7 +22,7 @@
|
||||
CE48FD372DBE95EB00E5E3D6 /* VaultModels.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEE482AA2DBE8EFE00F4A367 /* VaultModels.framework */; };
|
||||
CE48FD632DBEA3B800E5E3D6 /* VaultUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEE4816A2DBE8AC800F4A367 /* VaultUI.framework */; };
|
||||
CE59C7632E4F47FE0024A246 /* VaultUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEE4816A2DBE8AC800F4A367 /* VaultUI.framework */; };
|
||||
CE73B3A02DFC8A0C0081B6CB /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = CE73B39F2DFC8A0C0081B6CB /* AppIcon.icon */; };
|
||||
CE73B3A02DFC8A0C0081B6CB /* AliasVault.icon in Resources */ = {isa = PBXBuildFile; fileRef = CE73B39F2DFC8A0C0081B6CB /* AliasVault.icon */; };
|
||||
CE9A58FC2DBA982100CB0A4C /* RCTNativeVaultManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = CE9A58FB2DBA982100CB0A4C /* RCTNativeVaultManager.mm */; };
|
||||
CE9A5A022DBAAE5000CB0A4C /* RCTNativeVaultManager.h in Sources */ = {isa = PBXBuildFile; fileRef = CE9A58FA2DBA982100CB0A4C /* RCTNativeVaultManager.h */; };
|
||||
CED3AB3C2E70CF8700F3FDEB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED3AB3B2E70CF8700F3FDEB /* AppDelegate.swift */; };
|
||||
@@ -179,7 +179,7 @@
|
||||
CE26D68C2DA7FCD2006DC04D /* VaultManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultManager.swift; sourceTree = "<group>"; };
|
||||
CE3AE2B02DC7ACD700E7745E /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
|
||||
CE59C75F2E4F47FD0024A246 /* VaultUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VaultUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CE73B39F2DFC8A0C0081B6CB /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = AppIcon.icon; sourceTree = "<group>"; };
|
||||
CE73B39F2DFC8A0C0081B6CB /* AliasVault.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AliasVault.icon; sourceTree = "<group>"; };
|
||||
CE9A58FA2DBA982100CB0A4C /* RCTNativeVaultManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTNativeVaultManager.h; sourceTree = "<group>"; };
|
||||
CE9A58FB2DBA982100CB0A4C /* RCTNativeVaultManager.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTNativeVaultManager.mm; sourceTree = "<group>"; };
|
||||
CE9A5B662DBAE42B00CB0A4C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
@@ -198,7 +198,7 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
CEE9098F2DA548C7008D568F /* Exceptions for "Autofill" folder in "Autofill" target */ = {
|
||||
CEE9098F2DA548C7008D568F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
@@ -208,73 +208,12 @@
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
CE59C7602E4F47FD0024A246 /* VaultUITests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEE480882DBE86DC00F4A367 /* VaultStoreKit */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultStoreKit;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEE480972DBE86DD00F4A367 /* VaultStoreKitTests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultStoreKitTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEE4816B2DBE8AC800F4A367 /* VaultUI */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultUI;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEE482AB2DBE8EFE00F4A367 /* VaultModels */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultModels;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEE909812DA548C7008D568F /* Autofill */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
CEE9098F2DA548C7008D568F /* Exceptions for "Autofill" folder in "Autofill" target */,
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = Autofill;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE59C7602E4F47FD0024A246 /* VaultUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultUITests; sourceTree = "<group>"; };
|
||||
CEE480882DBE86DC00F4A367 /* VaultStoreKit */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultStoreKit; sourceTree = "<group>"; };
|
||||
CEE480972DBE86DD00F4A367 /* VaultStoreKitTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultStoreKitTests; sourceTree = "<group>"; };
|
||||
CEE4816B2DBE8AC800F4A367 /* VaultUI */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultUI; sourceTree = "<group>"; };
|
||||
CEE482AB2DBE8EFE00F4A367 /* VaultModels */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultModels; sourceTree = "<group>"; };
|
||||
CEE909812DA548C7008D568F /* Autofill */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (CEE9098F2DA548C7008D568F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Autofill; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -353,7 +292,7 @@
|
||||
CED3AB3B2E70CF8700F3FDEB /* AppDelegate.swift */,
|
||||
962DE0FB93C9458BAFEEED60 /* AliasVault-Bridging-Header.h */,
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||
CE73B39F2DFC8A0C0081B6CB /* AppIcon.icon */,
|
||||
CE73B39F2DFC8A0C0081B6CB /* AliasVault.icon */,
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
76D5DBE3C1FA4900A7CD27FB /* noop-file.swift */,
|
||||
BA88D6CB4FD656C7180FE9F7 /* PrivacyInfo.xcprivacy */,
|
||||
@@ -718,8 +657,10 @@
|
||||
de,
|
||||
en,
|
||||
fi,
|
||||
he,
|
||||
it,
|
||||
nl,
|
||||
uk,
|
||||
zh,
|
||||
es,
|
||||
sv,
|
||||
@@ -728,6 +669,7 @@
|
||||
fr,
|
||||
ru,
|
||||
uk,
|
||||
he,
|
||||
);
|
||||
mainGroup = 83CBB9F61A601CBA00E9B192;
|
||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||
@@ -751,7 +693,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
|
||||
CE73B3A02DFC8A0C0081B6CB /* AppIcon.icon in Resources */,
|
||||
CE73B3A02DFC8A0C0081B6CB /* AliasVault.icon in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
||||
D6E19ADE0A608FA46984C60D /* PrivacyInfo.xcprivacy in Resources */,
|
||||
@@ -1198,12 +1140,13 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 5C1EA06BE7608888B8DF9CE0 /* Pods-AliasVault.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AliasVault;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
@@ -1218,7 +1161,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.23.0;
|
||||
MARKETING_VERSION = 0.23.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1240,10 +1183,11 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 82C6F986E9F08FE1179D0A40 /* Pods-AliasVault.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AliasVault;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
INFOPLIST_FILE = AliasVault/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = AliasVault;
|
||||
@@ -1253,7 +1197,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.23.0;
|
||||
MARKETING_VERSION = 0.23.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1323,10 +1267,7 @@
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
@@ -1380,10 +1321,7 @@
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
USE_HERMES = true;
|
||||
@@ -1404,7 +1342,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1440,7 +1378,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1472,7 +1410,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1525,7 +1463,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1574,7 +1512,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1609,7 +1547,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1642,7 +1580,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1695,7 +1633,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1744,7 +1682,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1796,7 +1734,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1847,7 +1785,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = autofill/autofill.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1863,7 +1801,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 0.23.0;
|
||||
MARKETING_VERSION = 0.23.2;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
@@ -1892,7 +1830,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = autofill/autofill.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1908,7 +1846,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 0.23.0;
|
||||
MARKETING_VERSION = 0.23.2;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.app.autofill;
|
||||
|
||||
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |