mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-30 17:48:18 -05:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a70902d69 | ||
|
|
eee41df9a4 | ||
|
|
d563d6d448 | ||
|
|
db1474397c | ||
|
|
e881f9486a | ||
|
|
645fd605e6 | ||
|
|
254f0a1212 | ||
|
|
64d29ebcd4 | ||
|
|
df0d74595f | ||
|
|
2131e4922c | ||
|
|
d846825b84 | ||
|
|
2a902eeb97 | ||
|
|
d9a6dfab03 | ||
|
|
3da99ed4b1 | ||
|
|
5414f40c98 | ||
|
|
6c561e8ece | ||
|
|
3654b12cd7 | ||
|
|
266e7b36d4 | ||
|
|
cbe9978367 | ||
|
|
6b949bcb2f | ||
|
|
6a4fbb9193 | ||
|
|
c459a48927 | ||
|
|
d3f132df63 | ||
|
|
b5edc6ef76 | ||
|
|
4e0db87bc3 | ||
|
|
62cc0e7c2b | ||
|
|
dad3a6fa2c | ||
|
|
9560d550e4 | ||
|
|
0930ae03cd | ||
|
|
23c9bf2fc9 | ||
|
|
6ebaf8e1b8 |
@@ -1 +1 @@
|
||||
0
|
||||
2
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.25.0
|
||||
0.25.2
|
||||
|
||||
34
apps/browser-extension/package-lock.json
generated
34
apps/browser-extension/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "aliasvault-browser-extension",
|
||||
"version": "0.25.0",
|
||||
"version": "0.26.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aliasvault-browser-extension",
|
||||
"version": "0.25.0",
|
||||
"version": "0.26.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
@@ -229,6 +229,7 @@
|
||||
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -607,6 +608,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -630,6 +632,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -2134,6 +2137,7 @@
|
||||
"integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -2215,6 +2219,7 @@
|
||||
"integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.32.1",
|
||||
"@typescript-eslint/types": "8.32.1",
|
||||
@@ -2843,6 +2848,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3550,6 +3556,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001716",
|
||||
"electron-to-chromium": "^1.5.149",
|
||||
@@ -5105,6 +5112,7 @@
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -5275,6 +5283,7 @@
|
||||
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.8",
|
||||
@@ -6477,6 +6486,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
@@ -7332,6 +7342,7 @@
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@@ -7378,6 +7389,7 @@
|
||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
@@ -8251,9 +8263,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz",
|
||||
"integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==",
|
||||
"dev": true,
|
||||
"license": "(BSD-3-Clause OR GPL-2.0)",
|
||||
"engines": {
|
||||
@@ -9058,6 +9070,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -9609,6 +9622,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -9618,6 +9632,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -9630,6 +9645,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.57.0.tgz",
|
||||
"integrity": "sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@@ -9979,6 +9995,7 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz",
|
||||
"integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.7"
|
||||
},
|
||||
@@ -11111,6 +11128,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -11389,6 +11407,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -11536,6 +11555,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"napi-postinstall": "^0.2.2"
|
||||
},
|
||||
@@ -11663,6 +11683,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@@ -11793,6 +11814,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -11805,6 +11827,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz",
|
||||
"integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "3.1.3",
|
||||
"@vitest/mocker": "3.1.3",
|
||||
@@ -12376,6 +12399,7 @@
|
||||
"integrity": "sha512-DqqHc/5COs8GR21ii99bANXf/mu6zuDpiXFV1YKNsqO5/PvkrCx5arY0aVPL5IATsuacAnNzdj4eMc1qbzS53Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@1natsu/wait-element": "^4.1.2",
|
||||
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "aliasvault-browser-extension",
|
||||
"description": "AliasVault Browser Extension",
|
||||
"private": true,
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:chrome": "wxt -b chrome",
|
||||
|
||||
@@ -463,7 +463,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2500900;
|
||||
CURRENT_PROJECT_VERSION = 2502900;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -476,7 +476,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.25.0;
|
||||
MARKETING_VERSION = 0.25.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -495,7 +495,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2500900;
|
||||
CURRENT_PROJECT_VERSION = 2502900;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -508,7 +508,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.25.0;
|
||||
MARKETING_VERSION = 0.25.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -532,7 +532,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 2500900;
|
||||
CURRENT_PROJECT_VERSION = 2502900;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -547,7 +547,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.25.0;
|
||||
MARKETING_VERSION = 0.25.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -571,7 +571,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 2500900;
|
||||
CURRENT_PROJECT_VERSION = 2502900;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -586,7 +586,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.25.0;
|
||||
MARKETING_VERSION = 0.25.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { handleClipboardCopied, handleCancelClipboardClear, handleGetClipboardCl
|
||||
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
|
||||
import { handleGetWebAuthnSettings, handleWebAuthnCreate, handleWebAuthnGet, handlePasskeyPopupResponse, handleGetRequestData } from '@/entrypoints/background/PasskeyHandler';
|
||||
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 { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetFilteredCredentials, handleGetSearchCredentials, 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';
|
||||
import { EncryptionKeyDerivationParams } from "@/utils/dist/shared/models/metadata";
|
||||
@@ -28,6 +28,8 @@ export default defineBackground({
|
||||
onMessage('GET_ENCRYPTION_KEY_DERIVATION_PARAMS', () => handleGetEncryptionKeyDerivationParams());
|
||||
onMessage('GET_VAULT', () => handleGetVault());
|
||||
onMessage('GET_CREDENTIALS', () => handleGetCredentials());
|
||||
onMessage('GET_FILTERED_CREDENTIALS', ({ data }) => handleGetFilteredCredentials(data as { currentUrl: string, pageTitle: string, matchingMode?: string }));
|
||||
onMessage('GET_SEARCH_CREDENTIALS', ({ data }) => handleGetSearchCredentials(data as { searchTerm: string }));
|
||||
|
||||
onMessage('GET_DEFAULT_EMAIL_DOMAIN', () => handleGetDefaultEmailDomain());
|
||||
onMessage('GET_DEFAULT_IDENTITY_SETTINGS', () => handleGetDefaultIdentitySettings());
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { handleGetEncryptionKey } from '@/entrypoints/background/VaultMessageHandler';
|
||||
import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/CredentialMatcher';
|
||||
|
||||
import {
|
||||
PASSKEY_PROVIDER_ENABLED_KEY,
|
||||
PASSKEY_DISABLED_SITES_KEY
|
||||
} from '@/utils/Constants';
|
||||
import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher';
|
||||
import { EncryptionUtility } from '@/utils/EncryptionUtility';
|
||||
import { PasskeyHelper } from '@/utils/passkey/PasskeyHelper';
|
||||
import type {
|
||||
|
||||
@@ -18,6 +18,21 @@ import { WebApiService } from '@/utils/WebApiService';
|
||||
|
||||
import { t } from '@/i18n/StandaloneI18n';
|
||||
|
||||
/**
|
||||
* Cache for the SqliteClient to avoid repeated decryption and initialization.
|
||||
* The cached instance is the single source of truth for the in-memory vault.
|
||||
*
|
||||
* Cache Strategy:
|
||||
* - Local mutations (createCredential, etc.): Work directly on cachedSqliteClient, no cache clearing
|
||||
* - New vault from remote (login, sync): Clear cache by setting both to null
|
||||
* - Logout/clear vault: Clear cache by setting both to null
|
||||
*
|
||||
* The cache is cleared by setting cachedSqliteClient and cachedVaultBlob to null directly
|
||||
* in the functions that receive new vault data from external sources.
|
||||
*/
|
||||
let cachedSqliteClient: SqliteClient | null = null;
|
||||
let cachedVaultBlob: string | null = null;
|
||||
|
||||
/**
|
||||
* Check if the user is logged in and if the vault is locked, and also check for pending migrations.
|
||||
*/
|
||||
@@ -58,8 +73,6 @@ export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, i
|
||||
hasPendingMigrations
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error checking pending migrations:', error);
|
||||
|
||||
// If it's a version incompatibility error, we need to handle it specially
|
||||
if (error instanceof VaultVersionIncompatibleError) {
|
||||
// Return the error so the UI can handle it appropriately (logout user)
|
||||
@@ -92,6 +105,10 @@ export async function handleStoreVault(
|
||||
// Store new encrypted vault in session storage.
|
||||
await storage.setItem('session:encryptedVault', vaultRequest.vaultBlob);
|
||||
|
||||
// Clear cached client since we received a new vault blob from external source
|
||||
cachedSqliteClient = null;
|
||||
cachedVaultBlob = null;
|
||||
|
||||
/*
|
||||
* For all other values, check if they have a value and store them in session storage if they do.
|
||||
* Some updates, e.g. when mutating local database, these values will not be set.
|
||||
@@ -155,7 +172,7 @@ export async function handleStoreEncryptionKeyDerivationParams(
|
||||
*/
|
||||
export async function handleSyncVault(
|
||||
) : Promise<messageBoolResponse> {
|
||||
const webApi = new WebApiService(() => {});
|
||||
const webApi = new WebApiService();
|
||||
const statusResponse = await webApi.getStatus();
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
@@ -175,6 +192,10 @@ export async function handleSyncVault(
|
||||
{ key: 'session:hiddenPrivateEmailDomains', value: vaultResponse.vault.hiddenPrivateEmailDomainList },
|
||||
{ key: 'session:vaultRevisionNumber', value: vaultResponse.vault.currentRevisionNumber }
|
||||
]);
|
||||
|
||||
// Clear cached client since we received a new vault blob from server
|
||||
cachedSqliteClient = null;
|
||||
cachedVaultBlob = null;
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
@@ -240,6 +261,10 @@ export function handleClearVault(
|
||||
'session:vaultRevisionNumber'
|
||||
]);
|
||||
|
||||
// Clear cached client since vault was cleared
|
||||
cachedSqliteClient = null;
|
||||
cachedVaultBlob = null;
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -264,6 +289,100 @@ export async function handleGetCredentials(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get credentials filtered by URL and page title for autofill performance optimization.
|
||||
* Filters credentials in the background script before sending to reduce message payload size.
|
||||
* Critical for large vaults (1000+ credentials) to avoid multi-second delays.
|
||||
*
|
||||
* @param message - Filtering parameters: currentUrl, pageTitle, matchingMode
|
||||
*/
|
||||
export async function handleGetFilteredCredentials(
|
||||
message: { currentUrl: string, pageTitle: string, matchingMode?: string }
|
||||
) : Promise<messageCredentialsResponse> {
|
||||
const encryptionKey = await handleGetEncryptionKey();
|
||||
|
||||
if (!encryptionKey) {
|
||||
return { success: false, error: await t('common.errors.vaultIsLocked') };
|
||||
}
|
||||
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const allCredentials = sqliteClient.getAllCredentials();
|
||||
|
||||
const { filterCredentials, AutofillMatchingMode } = await import('@/utils/credentialMatcher/CredentialMatcher');
|
||||
|
||||
// Parse matching mode from string
|
||||
let matchingMode = AutofillMatchingMode.DEFAULT;
|
||||
if (message.matchingMode) {
|
||||
matchingMode = message.matchingMode as typeof AutofillMatchingMode[keyof typeof AutofillMatchingMode];
|
||||
}
|
||||
|
||||
// Filter credentials in background to reduce payload size (~95% reduction)
|
||||
const filteredCredentials = filterCredentials(
|
||||
allCredentials,
|
||||
message.currentUrl,
|
||||
message.pageTitle,
|
||||
matchingMode
|
||||
);
|
||||
|
||||
return { success: true, credentials: filteredCredentials };
|
||||
} catch (error) {
|
||||
console.error('Error getting filtered credentials:', error);
|
||||
return { success: false, error: await t('common.errors.unknownError') };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get credentials filtered by text search query.
|
||||
* Searches across entire vault (service name, username, email, URL) and returns matches.
|
||||
*
|
||||
* @param message - Search parameters: searchTerm
|
||||
*/
|
||||
export async function handleGetSearchCredentials(
|
||||
message: { searchTerm: string }
|
||||
) : Promise<messageCredentialsResponse> {
|
||||
const encryptionKey = await handleGetEncryptionKey();
|
||||
|
||||
if (!encryptionKey) {
|
||||
return { success: false, error: await t('common.errors.vaultIsLocked') };
|
||||
}
|
||||
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const allCredentials = sqliteClient.getAllCredentials();
|
||||
|
||||
// If search term is empty, return empty array
|
||||
if (!message.searchTerm || message.searchTerm.trim() === '') {
|
||||
return { success: true, credentials: [] };
|
||||
}
|
||||
|
||||
const searchTerm = message.searchTerm.toLowerCase().trim();
|
||||
|
||||
// Filter credentials by search term across multiple fields
|
||||
const searchResults = allCredentials.filter(cred => {
|
||||
const searchableFields = [
|
||||
cred.ServiceName?.toLowerCase(),
|
||||
cred.Username?.toLowerCase(),
|
||||
cred.Alias?.Email?.toLowerCase(),
|
||||
cred.ServiceUrl?.toLowerCase()
|
||||
];
|
||||
return searchableFields.some(field => field?.includes(searchTerm));
|
||||
}).sort((a, b) => {
|
||||
// Sort by service name, then username
|
||||
const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? '');
|
||||
if (serviceNameComparison !== 0) {
|
||||
return serviceNameComparison;
|
||||
}
|
||||
return (a.Username ?? '').localeCompare(b.Username ?? '');
|
||||
});
|
||||
|
||||
return { success: true, credentials: searchResults };
|
||||
} catch (error) {
|
||||
console.error('Error searching credentials:', error);
|
||||
return { success: false, error: await t('common.errors.unknownError') };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an identity.
|
||||
*/
|
||||
@@ -405,13 +524,11 @@ export async function handleUploadVault(
|
||||
message: any
|
||||
) : Promise<messageVaultUploadResponse> {
|
||||
try {
|
||||
// Store the new vault blob in session storage.
|
||||
// Persist the current updated vault blob in session storage.
|
||||
await storage.setItem('session:encryptedVault', message.vaultBlob);
|
||||
|
||||
// Create new sqlite client which will use the new vault blob.
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
|
||||
// Upload the new vault to the server.
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const response = await uploadNewVaultToServer(sqliteClient);
|
||||
return { success: true, status: response.status, newRevisionNumber: response.newRevisionNumber };
|
||||
} catch (error) {
|
||||
@@ -486,10 +603,17 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<Vaul
|
||||
encryptionKey
|
||||
);
|
||||
|
||||
// Update storage with the newly encrypted vault (serialized from current in-memory state)
|
||||
await storage.setItems([
|
||||
{ key: 'session:encryptedVault', value: encryptedVault }
|
||||
]);
|
||||
|
||||
/*
|
||||
* Update cached vault blob to match the new encrypted version
|
||||
* This prevents unnecessary cache invalidation since the in-memory sqliteClient is already up to date
|
||||
*/
|
||||
cachedVaultBlob = encryptedVault;
|
||||
|
||||
// Get metadata from storage
|
||||
const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number;
|
||||
|
||||
@@ -510,7 +634,7 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<Vaul
|
||||
encryptionPublicKey: '',
|
||||
};
|
||||
|
||||
const webApi = new WebApiService(() => {});
|
||||
const webApi = new WebApiService();
|
||||
const response = await webApi.post<Vault, VaultPostResponse>('Vault', newVault);
|
||||
|
||||
// Check if response is successful (.status === 0)
|
||||
@@ -525,6 +649,7 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<Vaul
|
||||
|
||||
/**
|
||||
* Create a new sqlite client for the stored vault.
|
||||
* Uses a cache to avoid repeated decryption and initialization for read operations.
|
||||
*/
|
||||
async function createVaultSqliteClient() : Promise<SqliteClient> {
|
||||
const encryptedVault = await storage.getItem('session:encryptedVault') as string;
|
||||
@@ -533,15 +658,24 @@ async function createVaultSqliteClient() : Promise<SqliteClient> {
|
||||
throw new Error(await t('common.errors.unknownError'));
|
||||
}
|
||||
|
||||
// Decrypt the vault.
|
||||
// Check if we have a valid cached client
|
||||
if (cachedSqliteClient && cachedVaultBlob === encryptedVault) {
|
||||
return cachedSqliteClient;
|
||||
}
|
||||
|
||||
// Decrypt the vault
|
||||
const decryptedVault = await EncryptionUtility.symmetricDecrypt(
|
||||
encryptedVault,
|
||||
encryptionKey
|
||||
);
|
||||
|
||||
// Initialize the SQLite client with the decrypted vault.
|
||||
// Initialize the SQLite client with the decrypted vault
|
||||
const sqliteClient = new SqliteClient();
|
||||
await sqliteClient.initializeFromBase64(decryptedVault);
|
||||
|
||||
// Cache the client and vault blob
|
||||
cachedSqliteClient = sqliteClient;
|
||||
cachedVaultBlob = encryptedVault;
|
||||
|
||||
return sqliteClient;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { sendMessage } from 'webext-bridge/content-script';
|
||||
|
||||
import { filterCredentials, AutofillMatchingMode } from '@/entrypoints/contentScript/CredentialMatcher';
|
||||
import { fillCredential } from '@/entrypoints/contentScript/Form';
|
||||
|
||||
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 { AutofillMatchingMode } from '@/utils/credentialMatcher/CredentialMatcher';
|
||||
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';
|
||||
@@ -49,7 +49,14 @@ export function openAutofillPopup(input: HTMLInputElement, container: HTMLElemen
|
||||
document.addEventListener('keydown', handleEnterKey);
|
||||
|
||||
(async () : Promise<void> => {
|
||||
const response = await sendMessage('GET_CREDENTIALS', { }, 'background') as CredentialsResponse;
|
||||
// Load autofill matching mode setting to send to background for filtering
|
||||
const matchingMode = await storage.getItem(AUTOFILL_MATCHING_MODE_KEY) as AutofillMatchingMode ?? AutofillMatchingMode.DEFAULT;
|
||||
|
||||
const response = await sendMessage('GET_FILTERED_CREDENTIALS', {
|
||||
currentUrl: window.location.href,
|
||||
pageTitle: document.title,
|
||||
matchingMode: matchingMode
|
||||
}, 'background') as CredentialsResponse;
|
||||
|
||||
if (response.success) {
|
||||
await createAutofillPopup(input, response.credentials, container);
|
||||
@@ -182,22 +189,12 @@ export async function createAutofillPopup(input: HTMLInputElement, credentials:
|
||||
credentialList.className = 'av-credential-list';
|
||||
popup.appendChild(credentialList);
|
||||
|
||||
// Add initial credentials
|
||||
// Add initial credentials (already filtered by background script for performance)
|
||||
if (!credentials) {
|
||||
credentials = [];
|
||||
}
|
||||
|
||||
// Load autofill matching mode setting
|
||||
const matchingMode = await storage.getItem(AUTOFILL_MATCHING_MODE_KEY) as AutofillMatchingMode ?? AutofillMatchingMode.DEFAULT;
|
||||
|
||||
const filteredCredentials = filterCredentials(
|
||||
credentials,
|
||||
window.location.href,
|
||||
document.title,
|
||||
matchingMode
|
||||
);
|
||||
|
||||
updatePopupContent(filteredCredentials, credentialList, input, rootContainer, noMatchesText);
|
||||
updatePopupContent(credentials, credentialList, input, rootContainer, noMatchesText);
|
||||
|
||||
// Add divider
|
||||
const divider = document.createElement('div');
|
||||
@@ -549,62 +546,41 @@ export async function createVaultLockedPopup(input: HTMLInputElement, rootContai
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle popup search input by filtering credentials based on the search term.
|
||||
* Handle popup search input - searches entire vault when user types.
|
||||
* When empty, shows the initially URL-filtered credentials.
|
||||
* When user types, searches ALL credentials in vault (not just the pre-filtered set).
|
||||
*
|
||||
* @param searchInput - The search input element
|
||||
* @param initialCredentials - The initially URL-filtered credentials to show when search is empty
|
||||
* @param rootContainer - The root container element
|
||||
* @param searchTimeout - Timeout for debouncing search
|
||||
* @param credentialList - The credential list element to update
|
||||
* @param input - The input field that triggered the popup
|
||||
* @param noMatchesText - Text to show when no matches found
|
||||
*/
|
||||
async function handleSearchInput(searchInput: HTMLInputElement, credentials: Credential[], rootContainer: HTMLElement, searchTimeout: NodeJS.Timeout | null, credentialList: HTMLElement | null, input: HTMLInputElement, noMatchesText?: string) : Promise<void> {
|
||||
async function handleSearchInput(searchInput: HTMLInputElement, initialCredentials: Credential[], rootContainer: HTMLElement, searchTimeout: NodeJS.Timeout | null, credentialList: HTMLElement | null, input: HTMLInputElement, noMatchesText?: string) : Promise<void> {
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout);
|
||||
}
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
|
||||
// Ensure we have unique credentials
|
||||
const uniqueCredentials = Array.from(new Map(credentials.map(cred => [cred.Id, cred])).values());
|
||||
let filteredCredentials;
|
||||
const searchTerm = searchInput.value.trim();
|
||||
|
||||
if (searchTerm === '') {
|
||||
// Load autofill matching mode setting
|
||||
const matchingMode = await storage.getItem(AUTOFILL_MATCHING_MODE_KEY) as AutofillMatchingMode ?? AutofillMatchingMode.DEFAULT;
|
||||
|
||||
// If search is empty, use original URL-based filtering
|
||||
filteredCredentials = filterCredentials(
|
||||
uniqueCredentials,
|
||||
window.location.href,
|
||||
document.title,
|
||||
matchingMode
|
||||
).sort((a, b) => {
|
||||
// First compare by service name
|
||||
const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? '');
|
||||
if (serviceNameComparison !== 0) {
|
||||
return serviceNameComparison;
|
||||
}
|
||||
|
||||
// If service names are equal, compare by username/nickname
|
||||
return (a.Username ?? '').localeCompare(b.Username ?? '');
|
||||
});
|
||||
// If search is empty, show the initially URL-filtered credentials
|
||||
updatePopupContent(initialCredentials, credentialList, input, rootContainer, noMatchesText);
|
||||
} else {
|
||||
// Otherwise filter based on search term
|
||||
filteredCredentials = uniqueCredentials.filter(cred => {
|
||||
const searchableFields = [
|
||||
cred.ServiceName?.toLowerCase(),
|
||||
cred.Username?.toLowerCase(),
|
||||
cred.Alias?.Email?.toLowerCase(),
|
||||
cred.ServiceUrl?.toLowerCase()
|
||||
];
|
||||
return searchableFields.some(field => field?.includes(searchTerm));
|
||||
}).sort((a, b) => {
|
||||
// First compare by service name
|
||||
const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? '');
|
||||
if (serviceNameComparison !== 0) {
|
||||
return serviceNameComparison;
|
||||
}
|
||||
// Search in full vault with search term
|
||||
const response = await sendMessage('GET_SEARCH_CREDENTIALS', {
|
||||
searchTerm: searchTerm
|
||||
}, 'background') as CredentialsResponse;
|
||||
|
||||
// If service names are equal, compare by username/nickname
|
||||
return (a.Username ?? '').localeCompare(b.Username ?? '');
|
||||
});
|
||||
if (response.success && response.credentials) {
|
||||
updatePopupContent(response.credentials, credentialList, input, rootContainer, noMatchesText);
|
||||
} else {
|
||||
// On error, fallback to showing initial filtered credentials
|
||||
updatePopupContent(initialCredentials, credentialList, input, rootContainer, noMatchesText);
|
||||
}
|
||||
}
|
||||
|
||||
// Update popup content with filtered results
|
||||
updatePopupContent(filteredCredentials, credentialList, input, rootContainer, noMatchesText);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/CredentialMatcher';
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import PasskeyBypassDialog from '@/entrypoints/popup/components/Dialogs/PasskeyBypassDialog';
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
@@ -12,6 +11,7 @@ import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useVaultLockRedirect } from '@/entrypoints/popup/hooks/useVaultLockRedirect';
|
||||
|
||||
import { PASSKEY_DISABLED_SITES_KEY } from '@/utils/Constants';
|
||||
import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher';
|
||||
import { PasskeyAuthenticator } from '@/utils/passkey/PasskeyAuthenticator';
|
||||
import { PasskeyHelper } from '@/utils/passkey/PasskeyHelper';
|
||||
import type { GetRequest, PasskeyGetCredentialResponse, PendingPasskeyGetRequest, StoredPasskeyRecord } from '@/utils/passkey/types';
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/CredentialMatcher';
|
||||
import Alert from '@/entrypoints/popup/components/Alert';
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import PasskeyBypassDialog from '@/entrypoints/popup/components/Dialogs/PasskeyBypassDialog';
|
||||
@@ -16,6 +15,7 @@ import { useVaultLockRedirect } from '@/entrypoints/popup/hooks/useVaultLockRedi
|
||||
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
|
||||
|
||||
import { PASSKEY_DISABLED_SITES_KEY } from '@/utils/Constants';
|
||||
import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher';
|
||||
import type { Passkey } from '@/utils/dist/shared/models/vault';
|
||||
import { PasskeyAuthenticator } from '@/utils/passkey/PasskeyAuthenticator';
|
||||
import { PasskeyHelper } from '@/utils/passkey/PasskeyHelper';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { AutofillMatchingMode } from '@/entrypoints/contentScript/CredentialMatcher';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
import {
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
TEMPORARY_DISABLED_SITES_KEY,
|
||||
AUTOFILL_MATCHING_MODE_KEY
|
||||
} from '@/utils/Constants';
|
||||
import { AutofillMatchingMode } from '@/utils/credentialMatcher/CredentialMatcher';
|
||||
|
||||
import { storage, browser } from "#imports";
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/CredentialMatcher';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
import {
|
||||
PASSKEY_PROVIDER_ENABLED_KEY,
|
||||
PASSKEY_DISABLED_SITES_KEY
|
||||
} from '@/utils/Constants';
|
||||
import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher';
|
||||
|
||||
import { storage, browser } from "#imports";
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"next": "Seuraava",
|
||||
"use": "Käytä",
|
||||
"delete": "Poista",
|
||||
"save": "Save",
|
||||
"save": "Tallenna",
|
||||
"or": "Tai",
|
||||
"close": "Sulje",
|
||||
"copied": "Kopioitu!",
|
||||
@@ -242,13 +242,13 @@
|
||||
"enterEmailPrefix": "Syötä sähköpostin etuliite"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Add 2FA Code",
|
||||
"instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.",
|
||||
"nameOptional": "Name (optional)",
|
||||
"secretKey": "Secret Key",
|
||||
"saveToViewCode": "Save to view code",
|
||||
"addCode": "Lisää 2FA TOTP -koodi",
|
||||
"instructions": "Syötä salainen avain, joka näkyy sivustossa, jossa haluat lisätä kaksivaiheisen tunnistautumisen",
|
||||
"nameOptional": "Nimi (valinnainen)",
|
||||
"secretKey": "Salainen avain",
|
||||
"saveToViewCode": "Tallenna nähdäksesi koodin",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Invalid secret key format."
|
||||
"invalidSecretKey": "Virheellinen salatun avaimen muoto."
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"next": "Dalej",
|
||||
"use": "Użyj",
|
||||
"delete": "Usuń",
|
||||
"save": "Save",
|
||||
"save": "Zapisz",
|
||||
"or": "lub",
|
||||
"close": "Zamknąć",
|
||||
"copied": "Skopiowano",
|
||||
@@ -238,22 +238,22 @@
|
||||
"publicEmailDescription": "Anonimowa, ale ograniczona prywatność. Treści e-mail są czytelne dla każdego, kto zna adres.",
|
||||
"useDomainChooser": "Użyj wybierania domen",
|
||||
"enterCustomDomain": "Wprowadź własną domenę",
|
||||
"enterFullEmail": "Wprowadź pełny adres e-mail",
|
||||
"enterFullEmail": "Wprowadź adres e-mail",
|
||||
"enterEmailPrefix": "Wprowadź prefiks e-mail"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Add 2FA Code",
|
||||
"instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.",
|
||||
"nameOptional": "Name (optional)",
|
||||
"secretKey": "Secret Key",
|
||||
"saveToViewCode": "Save to view code",
|
||||
"addCode": "Dodaj kod 2FA",
|
||||
"instructions": "Wprowadź tajny klucz wyświetlony na stronie internetowej, na której chcesz dodać uwierzytelnianie dwuskładnikowe.",
|
||||
"nameOptional": "Nazwa (opcjonalnie)",
|
||||
"secretKey": "Tajny klucz",
|
||||
"saveToViewCode": "Zapisz, aby wyświetlić kod",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Invalid secret key format."
|
||||
"invalidSecretKey": "Nieprawidłowy format tajnego klucza."
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "Skrzynka odbiorcza",
|
||||
"deleteEmailTitle": "Usuń adres e-mail",
|
||||
"deleteEmailTitle": "Usuń e-mail",
|
||||
"deleteEmailConfirm": "Czy na pewno chcesz trwale usunąć ten e-mail?",
|
||||
"from": "Od",
|
||||
"to": "Do",
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"next": "Далее",
|
||||
"use": "Использовать",
|
||||
"delete": "Удалить",
|
||||
"save": "Save",
|
||||
"save": "Сохранить",
|
||||
"or": "Или",
|
||||
"close": "Закрыть",
|
||||
"copied": "Скопировано!",
|
||||
@@ -242,13 +242,13 @@
|
||||
"enterEmailPrefix": "Введите префикс электронной почты"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Add 2FA Code",
|
||||
"instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.",
|
||||
"nameOptional": "Name (optional)",
|
||||
"secretKey": "Secret Key",
|
||||
"saveToViewCode": "Save to view code",
|
||||
"addCode": "Добавить код 2FA",
|
||||
"instructions": "Введите секретный ключ, указанный на веб-сайте, где вы хотите добавить двухфакторную аутентификацию.",
|
||||
"nameOptional": "Имя (необязательно)",
|
||||
"secretKey": "Секретный ключ",
|
||||
"saveToViewCode": "Сохранить для просмотра кода",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Invalid secret key format."
|
||||
"invalidSecretKey": "Неверный формат секретного ключа."
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
@@ -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.25.0';
|
||||
public static readonly VERSION = '0.25.2';
|
||||
|
||||
/**
|
||||
* The API version to send to the server (base semver without stage suffixes).
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
|
||||
import { filterCredentials } from '@/utils/credentialMatcher/CredentialMatcher';
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
import { filterCredentials } from '../CredentialMatcher';
|
||||
|
||||
describe('CredentialMatcher - Credential URL Matching', () => {
|
||||
let testCredentials: Credential[];
|
||||
|
||||
@@ -36,6 +36,20 @@ declare class PasswordGenerator {
|
||||
private readonly uppercaseChars;
|
||||
private readonly numberChars;
|
||||
private readonly specialChars;
|
||||
/**
|
||||
* Ambiguous characters that look similar and are easy to confuse when typing:
|
||||
* - I, l, 1, | (pipe) - all look like vertical lines
|
||||
* - O, 0, o - all look like circles
|
||||
* - Z, 2 - similar appearance
|
||||
* - S, 5 - similar appearance
|
||||
* - B, 8 - similar appearance
|
||||
* - G, 6 - similar appearance
|
||||
* - Brackets, braces, parentheses: [], {}, ()
|
||||
* - Quotes: ', ", `
|
||||
* - Punctuation pairs: ;:, .,
|
||||
* - Dashes: -, _
|
||||
* - Angle brackets: <>
|
||||
*/
|
||||
private readonly ambiguousChars;
|
||||
private length;
|
||||
private useLowercase;
|
||||
|
||||
@@ -39,7 +39,21 @@ var PasswordGenerator = class {
|
||||
this.uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
this.numberChars = "0123456789";
|
||||
this.specialChars = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||||
this.ambiguousChars = "Il1O0o";
|
||||
/**
|
||||
* Ambiguous characters that look similar and are easy to confuse when typing:
|
||||
* - I, l, 1, | (pipe) - all look like vertical lines
|
||||
* - O, 0, o - all look like circles
|
||||
* - Z, 2 - similar appearance
|
||||
* - S, 5 - similar appearance
|
||||
* - B, 8 - similar appearance
|
||||
* - G, 6 - similar appearance
|
||||
* - Brackets, braces, parentheses: [], {}, ()
|
||||
* - Quotes: ', ", `
|
||||
* - Punctuation pairs: ;:, .,
|
||||
* - Dashes: -, _
|
||||
* - Angle brackets: <>
|
||||
*/
|
||||
this.ambiguousChars = "Il1O0oZzSsBbGg2568|[]{}()<>;:,.`'\"_-";
|
||||
this.length = 18;
|
||||
this.useLowercase = true;
|
||||
this.useUppercase = true;
|
||||
|
||||
@@ -13,7 +13,21 @@ var PasswordGenerator = class {
|
||||
this.uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
this.numberChars = "0123456789";
|
||||
this.specialChars = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||||
this.ambiguousChars = "Il1O0o";
|
||||
/**
|
||||
* Ambiguous characters that look similar and are easy to confuse when typing:
|
||||
* - I, l, 1, | (pipe) - all look like vertical lines
|
||||
* - O, 0, o - all look like circles
|
||||
* - Z, 2 - similar appearance
|
||||
* - S, 5 - similar appearance
|
||||
* - B, 8 - similar appearance
|
||||
* - G, 6 - similar appearance
|
||||
* - Brackets, braces, parentheses: [], {}, ()
|
||||
* - Quotes: ', ", `
|
||||
* - Punctuation pairs: ;:, .,
|
||||
* - Dashes: -, _
|
||||
* - Angle brackets: <>
|
||||
*/
|
||||
this.ambiguousChars = "Il1O0oZzSsBbGg2568|[]{}()<>;:,.`'\"_-";
|
||||
this.length = 18;
|
||||
this.useLowercase = true;
|
||||
this.useUppercase = true;
|
||||
|
||||
@@ -27,18 +27,16 @@ export class FormFiller {
|
||||
* @param credential The credential to fill the form with.
|
||||
*/
|
||||
public async fillFields(credential: Credential): Promise<void> {
|
||||
// Perform security validation before filling any fields
|
||||
if (!await this.validateFormSecurity()) {
|
||||
console.warn('[AliasVault Security] Autofill blocked due to security validation failure');
|
||||
return;
|
||||
}
|
||||
// Perform security validation to identify safe fields
|
||||
const securityResults = await this.validateFormSecurity();
|
||||
|
||||
/*
|
||||
* Fill fields sequentially to avoid race conditions and conflicts.
|
||||
* Some websites have event handlers that can interfere with parallel filling.
|
||||
* Only fill fields that passed security validation.
|
||||
*/
|
||||
await this.fillBasicFields(credential);
|
||||
await this.fillPasswordFields(credential);
|
||||
await this.fillBasicFields(credential, securityResults);
|
||||
await this.fillPasswordFields(credential, securityResults);
|
||||
|
||||
this.fillBirthdateFields(credential);
|
||||
this.fillGenderFields(credential);
|
||||
@@ -51,12 +49,18 @@ export class FormFiller {
|
||||
* - Form field obstruction via overlays
|
||||
* - Suspicious element positioning
|
||||
* - Multiple forms with identical fields (potential decoy attacks)
|
||||
*
|
||||
* @returns A map of field elements to their security validation result (true = safe, false = unsafe)
|
||||
*/
|
||||
private async validateFormSecurity(): Promise<boolean> {
|
||||
private async validateFormSecurity(): Promise<Map<HTMLElement, boolean>> {
|
||||
const results = new Map<HTMLElement, boolean>();
|
||||
|
||||
try {
|
||||
// Skip security validation in test environments where browser APIs may not be available
|
||||
if (typeof window === 'undefined' || typeof MouseEvent === 'undefined') {
|
||||
return true;
|
||||
// In test environments, mark all fields as safe
|
||||
this.getAllFormFields().forEach(field => results.set(field, true));
|
||||
return results;
|
||||
}
|
||||
|
||||
// 1. Check page-wide security using ClickValidator (detects body/HTML opacity tricks)
|
||||
@@ -68,30 +72,40 @@ export class FormFiller {
|
||||
});
|
||||
// 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;
|
||||
const pageWideSecure = await this.clickValidator.validateClick(dummyEvent);
|
||||
if (!pageWideSecure) {
|
||||
console.warn('[AliasVault Security] Page-wide attack detected - blocking all autofill');
|
||||
// Mark all fields as unsafe
|
||||
this.getAllFormFields().forEach(field => results.set(field, false));
|
||||
return results;
|
||||
}
|
||||
|
||||
// 2. Check form field obstruction and positioning
|
||||
// 2. Check for suspicious form duplication (decoy attack)
|
||||
const hasDecoyForms = this.detectDecoyForms();
|
||||
if (hasDecoyForms) {
|
||||
console.warn('[AliasVault Security] Multiple suspicious forms detected - blocking all autofill');
|
||||
// Mark all fields as unsafe
|
||||
this.getAllFormFields().forEach(field => results.set(field, false));
|
||||
return results;
|
||||
}
|
||||
|
||||
// 3. Check individual form field obstruction and positioning
|
||||
const formFields = this.getAllFormFields();
|
||||
for (const field of formFields) {
|
||||
if (!this.validateFieldSecurity(field)) {
|
||||
console.warn('[AliasVault Security] Form autofill blocked: Field obstruction detected', field);
|
||||
return false;
|
||||
const isFieldSecure = this.validateFieldSecurity(field);
|
||||
results.set(field, isFieldSecure);
|
||||
|
||||
if (!isFieldSecure) {
|
||||
console.warn('[AliasVault Security] Field failed security check (will be skipped):', field);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check for suspicious form duplication (decoy attack)
|
||||
if (this.detectDecoyForms()) {
|
||||
console.warn('[AliasVault Security] Form autofill blocked: Multiple suspicious forms detected');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('[AliasVault Security] Form security validation error:', error);
|
||||
return false; // Fail safely - block autofill if validation fails
|
||||
// Fail safely - mark all fields as unsafe if validation fails
|
||||
this.getAllFormFields().forEach(field => results.set(field, false));
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,13 +305,14 @@ export class FormFiller {
|
||||
/**
|
||||
* Fill the basic fields of the form.
|
||||
* @param credential The credential to fill the form with.
|
||||
* @param securityResults Security validation results for each field.
|
||||
*/
|
||||
private async fillBasicFields(credential: Credential): Promise<void> {
|
||||
if (this.form.usernameField && credential.Username) {
|
||||
private async fillBasicFields(credential: Credential, securityResults: Map<HTMLElement, boolean>): Promise<void> {
|
||||
if (this.form.usernameField && credential.Username && securityResults.get(this.form.usernameField) !== false) {
|
||||
await this.fillTextFieldWithTyping(this.form.usernameField, credential.Username);
|
||||
}
|
||||
|
||||
if (this.form.emailField && (credential.Alias?.Email !== undefined || credential.Username !== undefined)) {
|
||||
if (this.form.emailField && (credential.Alias?.Email !== undefined || credential.Username !== undefined) && securityResults.get(this.form.emailField) !== false) {
|
||||
if (credential.Alias?.Email) {
|
||||
this.setElementValue(this.form.emailField, credential.Alias.Email);
|
||||
this.triggerInputEvents(this.form.emailField);
|
||||
@@ -317,7 +332,7 @@ export class FormFiller {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.form.emailConfirmField && credential.Alias?.Email) {
|
||||
if (this.form.emailConfirmField && credential.Alias?.Email && securityResults.get(this.form.emailConfirmField) !== false) {
|
||||
this.setElementValue(this.form.emailConfirmField, credential.Alias.Email);
|
||||
this.triggerInputEvents(this.form.emailConfirmField);
|
||||
}
|
||||
@@ -388,19 +403,20 @@ export class FormFiller {
|
||||
* 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.
|
||||
* @param securityResults Security validation results for each field.
|
||||
*/
|
||||
private async fillPasswordFields(credential: Credential): Promise<void> {
|
||||
private async fillPasswordFields(credential: Credential, securityResults: Map<HTMLElement, boolean>): Promise<void> {
|
||||
if (!credential.Password) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill main password field first
|
||||
if (this.form.passwordField) {
|
||||
// Fill main password field first (only if it passed security check)
|
||||
if (this.form.passwordField && securityResults.get(this.form.passwordField) !== false) {
|
||||
await this.fillPasswordField(this.form.passwordField, credential.Password);
|
||||
}
|
||||
|
||||
// Then fill password confirm field after main field is complete
|
||||
if (this.form.passwordConfirmField) {
|
||||
// Then fill password confirm field after main field is complete (only if it passed security check)
|
||||
if (this.form.passwordConfirmField && securityResults.get(this.form.passwordConfirmField) !== false) {
|
||||
await this.fillPasswordField(this.form.passwordConfirmField, credential.Password);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export default defineConfig({
|
||||
return {
|
||||
name: "AliasVault",
|
||||
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
|
||||
version: "0.25.0",
|
||||
version: "0.25.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 2500901
|
||||
versionName "0.25.0"
|
||||
versionCode 2501900
|
||||
versionName "0.25.1"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
@@ -202,6 +202,9 @@ dependencies {
|
||||
// Add modern SQLite library with VACUUM INTO and backup API support
|
||||
implementation("com.github.requery:sqlite-android:3.49.0")
|
||||
|
||||
// Add ZXing library for QR code scanning (F-Droid compatible)
|
||||
implementation("com.journeyapps:zxing-android-embedded:4.3.0")
|
||||
|
||||
// Test dependencies
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.mockito:mockito-core:4.0.0'
|
||||
|
||||
@@ -58,6 +58,13 @@
|
||||
android:theme="@style/PasskeyRegistrationTheme"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<!-- QR Scanner Activity -->
|
||||
<activity
|
||||
android:name=".qrscanner.QRScannerActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/zxing_CaptureTheme"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
@@ -113,6 +113,8 @@ class MainActivity : ReactActivity() {
|
||||
handlePinUnlockResult(resultCode, data)
|
||||
} else if (requestCode == net.aliasvault.app.nativevaultmanager.NativeVaultManager.PIN_SETUP_REQUEST_CODE) {
|
||||
handlePinSetupResult(resultCode, data)
|
||||
} else if (requestCode == net.aliasvault.app.nativevaultmanager.NativeVaultManager.QR_SCANNER_REQUEST_CODE) {
|
||||
handleQRScannerResult(resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,4 +196,31 @@ class MainActivity : ReactActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle QR scanner result.
|
||||
* @param resultCode The result code from the QR scanner activity.
|
||||
* @param data The intent data containing the scanned QR code.
|
||||
*/
|
||||
private fun handleQRScannerResult(resultCode: Int, data: Intent?) {
|
||||
val promise = net.aliasvault.app.nativevaultmanager.NativeVaultManager.pendingActivityResultPromise
|
||||
net.aliasvault.app.nativevaultmanager.NativeVaultManager.pendingActivityResultPromise = null
|
||||
|
||||
if (promise == null) {
|
||||
return
|
||||
}
|
||||
|
||||
when (resultCode) {
|
||||
RESULT_OK -> {
|
||||
val scannedData = data?.getStringExtra("SCAN_RESULT")
|
||||
promise.resolve(scannedData)
|
||||
}
|
||||
RESULT_CANCELED -> {
|
||||
promise.resolve(null)
|
||||
}
|
||||
else -> {
|
||||
promise.resolve(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.aliasvault.app.qrscanner.QRScannerActivity
|
||||
import net.aliasvault.app.vaultstore.VaultStore
|
||||
import net.aliasvault.app.vaultstore.VaultSyncError
|
||||
import net.aliasvault.app.vaultstore.keystoreprovider.AndroidKeystoreProvider
|
||||
@@ -63,6 +64,11 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
*/
|
||||
const val PIN_SETUP_REQUEST_CODE = 1002
|
||||
|
||||
/**
|
||||
* Request code for QR scanner activity.
|
||||
*/
|
||||
const val QR_SCANNER_REQUEST_CODE = 1003
|
||||
|
||||
/**
|
||||
* Static holder for the pending promise from showPinUnlock.
|
||||
* This allows MainActivity to resolve/reject the promise directly without
|
||||
@@ -1436,6 +1442,42 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
* @param subtitle The subtitle for authentication. If null or empty, uses default.
|
||||
* @param promise The promise to resolve with authentication result.
|
||||
*/
|
||||
@ReactMethod
|
||||
override fun scanQRCode(prefixes: ReadableArray?, statusText: String?, promise: Promise) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val activity = currentActivity
|
||||
if (activity == null) {
|
||||
promise.reject("NO_ACTIVITY", "No activity available", null)
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Store promise for later resolution by MainActivity
|
||||
pendingActivityResultPromise = promise
|
||||
|
||||
// Launch QR scanner activity with optional prefixes and status text
|
||||
val intent = Intent(activity, QRScannerActivity::class.java)
|
||||
if (prefixes != null && prefixes.size() > 0) {
|
||||
val prefixList = ArrayList<String>()
|
||||
for (i in 0 until prefixes.size()) {
|
||||
val prefix = prefixes.getString(i)
|
||||
if (prefix != null) {
|
||||
prefixList.add(prefix)
|
||||
}
|
||||
}
|
||||
intent.putStringArrayListExtra(QRScannerActivity.EXTRA_PREFIXES, prefixList)
|
||||
}
|
||||
if (statusText != null && statusText.isNotEmpty()) {
|
||||
intent.putExtra(QRScannerActivity.EXTRA_STATUS_TEXT, statusText)
|
||||
}
|
||||
activity.startActivityForResult(intent, QR_SCANNER_REQUEST_CODE)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to launch QR scanner", e)
|
||||
promise.reject("SCANNER_ERROR", "Failed to launch QR scanner: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
override fun authenticateUser(title: String?, subtitle: String?, promise: Promise) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package net.aliasvault.app.qrscanner
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.google.zxing.ResultPoint
|
||||
import com.journeyapps.barcodescanner.BarcodeCallback
|
||||
import com.journeyapps.barcodescanner.BarcodeResult
|
||||
import com.journeyapps.barcodescanner.CaptureManager
|
||||
import com.journeyapps.barcodescanner.DecoratedBarcodeView
|
||||
|
||||
/**
|
||||
* Activity for scanning QR codes using ZXing.
|
||||
*/
|
||||
class QRScannerActivity : Activity() {
|
||||
private lateinit var barcodeView: DecoratedBarcodeView
|
||||
private lateinit var capture: CaptureManager
|
||||
private var hasScanned = false
|
||||
private var prefixes: List<String>? = null
|
||||
|
||||
companion object {
|
||||
/** Intent extra key for prefixes. */
|
||||
const val EXTRA_PREFIXES = "EXTRA_PREFIXES"
|
||||
|
||||
/** Intent extra key for status text. */
|
||||
const val EXTRA_STATUS_TEXT = "EXTRA_STATUS_TEXT"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Get prefixes from intent if provided
|
||||
prefixes = intent.getStringArrayListExtra(EXTRA_PREFIXES)
|
||||
|
||||
// Get status text from intent, default to "Scan QR code" if not provided
|
||||
val statusText = intent.getStringExtra(EXTRA_STATUS_TEXT)?.takeIf { it.isNotEmpty() } ?: "Scan QR code"
|
||||
|
||||
// Create and configure barcode view
|
||||
barcodeView = DecoratedBarcodeView(this)
|
||||
barcodeView.setStatusText(statusText)
|
||||
setContentView(barcodeView)
|
||||
|
||||
// Initialize capture manager
|
||||
capture = CaptureManager(this, barcodeView)
|
||||
capture.initializeFromIntent(intent, savedInstanceState)
|
||||
|
||||
// Set custom callback to add visual feedback
|
||||
barcodeView.decodeContinuous(object : BarcodeCallback {
|
||||
override fun barcodeResult(result: BarcodeResult?) {
|
||||
if (result != null && !hasScanned) {
|
||||
val scannedText = result.text
|
||||
|
||||
// Check if prefixes filter is enabled
|
||||
if (prefixes != null && prefixes!!.isNotEmpty()) {
|
||||
// Check if the scanned code starts with any of the accepted prefixes
|
||||
val hasValidPrefix = prefixes!!.any { prefix ->
|
||||
scannedText.startsWith(prefix)
|
||||
}
|
||||
|
||||
if (!hasValidPrefix) {
|
||||
// Invalid QR code - continue scanning without setting hasScanned
|
||||
// Note: ZXing library continues scanning automatically
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Valid QR code
|
||||
hasScanned = true
|
||||
|
||||
// Show success animation
|
||||
showScanSuccessAnimation()
|
||||
|
||||
// Pause scanning
|
||||
barcodeView.pause()
|
||||
|
||||
// Set result and finish after animation
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra("SCAN_RESULT", scannedText)
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
|
||||
// Delay finish to allow animation to complete
|
||||
barcodeView.postDelayed({
|
||||
finish()
|
||||
}, 400) // 400ms delay for animation
|
||||
}
|
||||
}
|
||||
|
||||
override fun possibleResultPoints(resultPoints: List<ResultPoint>) {
|
||||
// No visualization needed
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a success animation when QR code is scanned.
|
||||
*/
|
||||
private fun showScanSuccessAnimation() {
|
||||
// Flash animation - fade viewfinder quickly
|
||||
val viewFinder: View? = barcodeView.viewFinder
|
||||
if (viewFinder != null) {
|
||||
// Create flash effect by animating alpha
|
||||
val fadeOut = ObjectAnimator.ofFloat(viewFinder, "alpha", 1f, 0.3f)
|
||||
fadeOut.duration = 100
|
||||
|
||||
val fadeIn = ObjectAnimator.ofFloat(viewFinder, "alpha", 0.3f, 1f)
|
||||
fadeIn.duration = 100
|
||||
|
||||
fadeOut.start()
|
||||
fadeOut.addListener(object : android.animation.AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: android.animation.Animator) {
|
||||
fadeIn.start()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
capture.onResume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
capture.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
capture.onDestroy()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
capture.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray,
|
||||
) {
|
||||
capture.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
}
|
||||
1
apps/mobile-app/android/fdroid/.gitignore
vendored
1
apps/mobile-app/android/fdroid/.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
fdroiddata
|
||||
fdroidserver
|
||||
net.aliasvault.app.yml
|
||||
|
||||
@@ -14,6 +14,8 @@ services:
|
||||
- ./net.aliasvault.app.yml:/net.aliasvault.app.yml
|
||||
# Add build script to the container
|
||||
- ./scripts/build.sh:/build.sh:Z
|
||||
# Bind the outputs directory to capture APK build output
|
||||
- ./outputs:/outputs:rw
|
||||
# Increase memory limits for Gradle builds
|
||||
shm_size: '2gb'
|
||||
mem_limit: 12g
|
||||
|
||||
@@ -13,9 +13,9 @@ RepoType: git
|
||||
Repo: https://github.com/aliasvault/aliasvault.git
|
||||
|
||||
Builds:
|
||||
- versionName: 0.1.0
|
||||
versionCode: 1
|
||||
commit: main
|
||||
- versionName: __VERSION_NAME__
|
||||
versionCode: __VERSION_CODE__
|
||||
commit: __COMMIT__
|
||||
subdir: apps/mobile-app/android/app/
|
||||
sudo:
|
||||
- sysctl fs.inotify.max_user_watches=524288 || true
|
||||
@@ -26,7 +26,7 @@ Builds:
|
||||
init:
|
||||
- cd ../..
|
||||
- sed -i -e '/signingConfig /d' android/app/build.gradle
|
||||
- npm install --build-from-source
|
||||
- npm install --production --build-from-source
|
||||
gradle:
|
||||
- yes
|
||||
scanignore:
|
||||
@@ -44,8 +44,6 @@ Builds:
|
||||
- apps/mobile-app/node_modules/react-native-context-menu-view/android/build.gradle
|
||||
- apps/mobile-app/node_modules/react-native-get-random-values/android/build.gradle
|
||||
- apps/mobile-app/node_modules/react-native-svg/android/build.gradle
|
||||
- apps/mobile-app/node_modules/expo-dev-launcher/android/build.gradle
|
||||
- apps/mobile-app/node_modules/expo-dev-menu/android/build.gradle
|
||||
scandelete:
|
||||
- apps/mobile-app/node_modules/
|
||||
|
||||
@@ -2,11 +2,64 @@
|
||||
set -e # Exit on any error, except where explicitly ignored
|
||||
trap 'echo "🛑 Interrupted. Exiting..."; exit 130' INT # Handle Ctrl+C cleanly
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BUILD_GRADLE="${SCRIPT_DIR}/../app/build.gradle"
|
||||
TEMPLATE_FILE="${SCRIPT_DIR}/net.aliasvault.app.yml.template"
|
||||
OUTPUT_FILE="${SCRIPT_DIR}/net.aliasvault.app.yml"
|
||||
|
||||
# Check if template exists
|
||||
if [ ! -f "$TEMPLATE_FILE" ]; then
|
||||
echo "❌ Error: Template file not found: $TEMPLATE_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if build.gradle exists
|
||||
if [ ! -f "$BUILD_GRADLE" ]; then
|
||||
echo "❌ Error: build.gradle not found: $BUILD_GRADLE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract version information from build.gradle
|
||||
echo "📱 Extracting version information from build.gradle..."
|
||||
VERSION_CODE=$(grep -E '^\s*versionCode\s+' "$BUILD_GRADLE" | sed -E 's/.*versionCode\s+([0-9]+).*/\1/')
|
||||
VERSION_NAME=$(grep -E '^\s*versionName\s+' "$BUILD_GRADLE" | sed -E 's/.*versionName\s+"([^"]+)".*/\1/')
|
||||
|
||||
if [ -z "$VERSION_CODE" ] || [ -z "$VERSION_NAME" ]; then
|
||||
echo "❌ Error: Could not extract version information from build.gradle"
|
||||
echo " versionCode: ${VERSION_CODE:-not found}"
|
||||
echo " versionName: ${VERSION_NAME:-not found}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get current git branch
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")
|
||||
|
||||
echo "✅ Version information extracted:"
|
||||
echo " versionCode: $VERSION_CODE"
|
||||
echo " versionName: $VERSION_NAME"
|
||||
echo " commit: $CURRENT_BRANCH"
|
||||
|
||||
# Generate the F-Droid metadata file from template
|
||||
echo "📝 Generating F-Droid metadata file..."
|
||||
sed -e "s/__VERSION_NAME__/$VERSION_NAME/g" \
|
||||
-e "s/__VERSION_CODE__/$VERSION_CODE/g" \
|
||||
-e "s/__COMMIT__/$CURRENT_BRANCH/g" \
|
||||
"$TEMPLATE_FILE" > "$OUTPUT_FILE"
|
||||
|
||||
echo "✅ Generated: $OUTPUT_FILE"
|
||||
|
||||
# Create outputs bind dir and set correct permissions
|
||||
mkdir -p outputs
|
||||
sudo chown -R 1000:1000 outputs
|
||||
|
||||
# Build and run the Docker environment
|
||||
echo "Building Docker images..."
|
||||
echo "🐳 Building Docker images..."
|
||||
if ! docker compose build; then
|
||||
echo "⚠️ Warning: Docker build failed, continuing..."
|
||||
fi
|
||||
|
||||
echo "Running fdroid-buildserver..."
|
||||
docker compose run --rm fdroid-buildserver
|
||||
echo "🚀 Running fdroid-buildserver..."
|
||||
docker compose run --rm fdroid-buildserver
|
||||
|
||||
echo "✅ F-Droid build completed!"
|
||||
|
||||
@@ -22,5 +22,9 @@ cd /home/vagrant/build
|
||||
fdroid fetchsrclibs net.aliasvault.app --verbose
|
||||
# Format build receipe
|
||||
fdroid rewritemeta net.aliasvault.app
|
||||
# Lint app
|
||||
fdroid lint --verbose net.aliasvault.app
|
||||
# Build app and scan for any binary files that are prohibited
|
||||
fdroid build --verbose --latest --scan-binary --on-server --no-tarball net.aliasvault.app
|
||||
fdroid build --verbose --test --latest --scan-binary --on-server --no-tarball net.aliasvault.app
|
||||
# Copy any outputs to the bind mount folder
|
||||
rsync -avh /home/vagrant/build/build/net.aliasvault.app/apps/mobile-app/android/app/build/outputs/ /outputs/
|
||||
|
||||
124
apps/mobile-app/android/fdroid/scripts/sign-apk.sh
Executable file
124
apps/mobile-app/android/fdroid/scripts/sign-apk.sh
Executable file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# ================================
|
||||
# This script is used to sign an unsigned F-Droid APK file with the local debug keystore (on MacOS) for testing purposes.
|
||||
# ================================
|
||||
# Flow:
|
||||
# 1. First do the run.sh / build.sh flow to build the F-Droid APK file on a (Linux) machine with enough memory and CPU power.
|
||||
# 2. Extract the unsigned APK file from the local (bind-mounted) outputs directory
|
||||
# 3. Then use this script to sign the APK file with the local debug keystore (on MacOS).
|
||||
#
|
||||
# ================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Colors ---
|
||||
RED="\033[0;31m"
|
||||
GREEN="\033[0;32m"
|
||||
YELLOW="\033[1;33m"
|
||||
CYAN="\033[0;36m"
|
||||
RESET="\033[0m"
|
||||
|
||||
info() { echo -e "${CYAN}[INFO]${RESET} $1"; }
|
||||
ok() { echo -e "${GREEN}[OK]${RESET} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${RESET} $1"; }
|
||||
|
||||
echo -e "${YELLOW}=== APK Debug Signer (macOS) ===${RESET}"
|
||||
|
||||
# --- Ask for unsigned APK ---
|
||||
read -rp "Enter unsigned APK filename (example: app-release-unsigned.apk): " APK_IN
|
||||
|
||||
if [[ ! -f "$APK_IN" ]]; then
|
||||
error "File not found: $APK_IN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "Input APK: $APK_IN"
|
||||
|
||||
# --- Detect SDK and build-tools ---
|
||||
SDK_ROOT="${ANDROID_SDK_ROOT:-$HOME/Library/Android/sdk}"
|
||||
BT_DIR="$SDK_ROOT/build-tools"
|
||||
|
||||
if [[ ! -d "$BT_DIR" ]]; then
|
||||
error "build-tools not found in: $BT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "Scanning build-tools..."
|
||||
|
||||
LATEST_BT="$(ls "$BT_DIR" | sort -V | tail -n 1)"
|
||||
|
||||
if [[ -z "$LATEST_BT" ]]; then
|
||||
error "No build-tools found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "Using build-tools version: ${YELLOW}${LATEST_BT}${RESET}"
|
||||
|
||||
ZIPALIGN="$BT_DIR/$LATEST_BT/zipalign"
|
||||
APKSIGNER="$BT_DIR/$LATEST_BT/apksigner"
|
||||
|
||||
[[ -x "$ZIPALIGN" ]] || { error "zipalign missing: $ZIPALIGN"; exit 1; }
|
||||
[[ -x "$APKSIGNER" ]] || { error "apksigner missing: $APKSIGNER"; exit 1; }
|
||||
|
||||
# --- Filenames ---
|
||||
APK_ALIGNED="${APK_IN%.apk}-aligned-temp.apk"
|
||||
APK_SIGNED="${APK_IN%.apk}-signed.apk"
|
||||
|
||||
info "Temporary aligned APK: $APK_ALIGNED"
|
||||
info "Final signed APK: $APK_SIGNED"
|
||||
|
||||
# --- Debug keystore ---
|
||||
DEBUG_KEYSTORE="$HOME/.android/debug.keystore"
|
||||
DEBUG_ALIAS="androiddebugkey"
|
||||
DEBUG_PASS="android"
|
||||
|
||||
[[ -f "$DEBUG_KEYSTORE" ]] || {
|
||||
error "Debug keystore missing: $DEBUG_KEYSTORE"
|
||||
exit 1
|
||||
}
|
||||
|
||||
info "Using debug keystore: $DEBUG_KEYSTORE"
|
||||
|
||||
# --- Step 1: zipalign ---
|
||||
echo -e "${YELLOW}=== Step 1: zipalign ===${RESET}"
|
||||
echo -e "[CMD] \"$ZIPALIGN\" -p -f 4 \"$APK_IN\" \"$APK_ALIGNED\""
|
||||
|
||||
"$ZIPALIGN" -p -f 4 "$APK_IN" "$APK_ALIGNED"
|
||||
ok "zipalign complete"
|
||||
|
||||
# --- Step 2: sign ---
|
||||
echo -e "${YELLOW}=== Step 2: apksigner ===${RESET}"
|
||||
echo -e "[CMD] \"$APKSIGNER\" sign --ks \"$DEBUG_KEYSTORE\" --out \"$APK_SIGNED\" \"$APK_ALIGNED\""
|
||||
|
||||
"$APKSIGNER" sign \
|
||||
--ks "$DEBUG_KEYSTORE" \
|
||||
--ks-key-alias "$DEBUG_ALIAS" \
|
||||
--ks-pass "pass:$DEBUG_PASS" \
|
||||
--key-pass "pass:$DEBUG_PASS" \
|
||||
--out "$APK_SIGNED" \
|
||||
"$APK_ALIGNED"
|
||||
|
||||
ok "Signing complete"
|
||||
|
||||
# --- Step 3: verify ---
|
||||
echo -e "${YELLOW}=== Step 3: Verify ===${RESET}"
|
||||
|
||||
"$APKSIGNER" verify --verbose "$APK_SIGNED"
|
||||
ok "APK verified"
|
||||
|
||||
# --- Step 4: Cleanup ---
|
||||
echo -e "${YELLOW}=== Cleanup ===${RESET}"
|
||||
|
||||
if [[ -f "$APK_ALIGNED" ]]; then
|
||||
rm -f "$APK_ALIGNED"
|
||||
ok "Removed temporary file: $APK_ALIGNED"
|
||||
fi
|
||||
|
||||
ok "Cleanup complete"
|
||||
|
||||
echo -e "${GREEN}=== DONE ===${RESET}"
|
||||
echo -e "Signed APK created → ${YELLOW}$APK_SIGNED${RESET}"
|
||||
echo -e "Install with:"
|
||||
echo -e " ${CYAN}adb install -r \"$APK_SIGNED\"${RESET}"
|
||||
|
||||
@@ -52,6 +52,9 @@ expo.webp.animated=false
|
||||
# Enable network inspector
|
||||
EX_DEV_CLIENT_NETWORK_INSPECTOR=true
|
||||
|
||||
# Enable VisionCamera code scanner
|
||||
VisionCamera_enableCodeScanner=true
|
||||
|
||||
# Use legacy packaging to compress native libraries in the resulting APK.
|
||||
expo.useLegacyPackaging=false
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"expo": {
|
||||
"name": "AliasVault",
|
||||
"slug": "AliasVault",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "aliasvault",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { CameraView, useCameraPermissions } from 'expo-camera';
|
||||
import { Href, router, useLocalSearchParams } from 'expo-router';
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import { View, Alert, StyleSheet } from 'react-native';
|
||||
import { View, StyleSheet, Platform, Alert } from 'react-native';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
@@ -10,6 +8,7 @@ import { useTranslation } from '@/hooks/useTranslation';
|
||||
import LoadingIndicator from '@/components/LoadingIndicator';
|
||||
import { ThemedContainer } from '@/components/themed/ThemedContainer';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
|
||||
// QR Code type prefixes
|
||||
const QR_CODE_PREFIXES = {
|
||||
@@ -54,71 +53,73 @@ function parseQRCode(data: string): ScannedQRCode {
|
||||
export default function QRScannerScreen() : React.ReactNode {
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const [permission, requestPermission] = useCameraPermissions();
|
||||
const { url } = useLocalSearchParams<{ url?: string }>();
|
||||
const hasProcessedUrl = useRef(false);
|
||||
const processedUrls = useRef(new Set<string>());
|
||||
|
||||
// Request camera permission on mount
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Request camera permission.
|
||||
*/
|
||||
const requestCameraPermission = async () : Promise<void> => {
|
||||
if (!permission) {
|
||||
return; // Still loading permission status
|
||||
}
|
||||
|
||||
if (!permission.granted && permission.canAskAgain) {
|
||||
// Request permission
|
||||
await requestPermission();
|
||||
} else if (!permission.granted && !permission.canAskAgain) {
|
||||
// Permission was permanently denied
|
||||
Alert.alert(
|
||||
t('settings.qrScanner.cameraPermissionTitle'),
|
||||
t('settings.qrScanner.cameraPermissionMessage'),
|
||||
[{ text: t('common.ok'), /**
|
||||
* Go back to the settings tab.
|
||||
*/
|
||||
onPress: (): void => router.back() }]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
requestCameraPermission();
|
||||
}, [permission, requestPermission, t]);
|
||||
const hasLaunchedScanner = useRef(false);
|
||||
|
||||
/*
|
||||
* Handle barcode scanned - parse and navigate to appropriate page.
|
||||
* Only processes AliasVault QR codes, silently ignores others.
|
||||
* Native scanner already filters by prefix, so we only get AliasVault QR codes here.
|
||||
* Validation is handled by the destination page.
|
||||
*/
|
||||
const handleBarcodeScanned = useCallback(({ data }: { data: string }) : void => {
|
||||
const handleQRCodeScanned = useCallback((data: string) : void => {
|
||||
// Prevent processing the same URL multiple times
|
||||
if (processedUrls.current.has(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the QR code to determine its type
|
||||
const parsedData = parseQRCode(data);
|
||||
|
||||
// Silently ignore non-AliasVault QR codes
|
||||
if (!parsedData.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark this URL as processed
|
||||
processedUrls.current.add(data);
|
||||
|
||||
// Parse the QR code to determine its type
|
||||
const parsedData = parseQRCode(data);
|
||||
|
||||
/*
|
||||
* Navigate to the appropriate page based on QR code type
|
||||
* Validation will be handled by the destination page
|
||||
* Use push instead of replace to navigate while scanner is still dismissing
|
||||
* This creates a smoother transition without returning to settings first
|
||||
*/
|
||||
if (parsedData.type === 'MOBILE_UNLOCK') {
|
||||
router.replace(`/(tabs)/settings/mobile-unlock/${parsedData.payload}` as Href);
|
||||
router.push(`/(tabs)/settings/mobile-unlock/${parsedData.payload}` as Href);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Launch the native QR scanner.
|
||||
*/
|
||||
const launchScanner = useCallback(async () => {
|
||||
if (hasLaunchedScanner.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasLaunchedScanner.current = true;
|
||||
|
||||
try {
|
||||
// Pass prefixes to native scanner for filtering and translated status text
|
||||
const prefixes = Object.values(QR_CODE_PREFIXES);
|
||||
const statusText = t('settings.qrScanner.scanningMessage');
|
||||
const scannedData = await NativeVaultManager.scanQRCode(prefixes, statusText);
|
||||
|
||||
if (scannedData) {
|
||||
handleQRCodeScanned(scannedData);
|
||||
} else {
|
||||
// User cancelled or scan failed, go back
|
||||
router.back();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('QR scan error:', error);
|
||||
Alert.alert(
|
||||
t('common.error'),
|
||||
'Failed to scan QR code',
|
||||
[{ text: t('common.ok'), /**
|
||||
* Navigate back.
|
||||
*/
|
||||
onPress: (): void => router.back() }]
|
||||
);
|
||||
}
|
||||
}, [handleQRCodeScanned, t]);
|
||||
|
||||
/**
|
||||
* Reset hasProcessedUrl when URL changes to allow processing new URLs.
|
||||
*/
|
||||
@@ -132,45 +133,24 @@ export default function QRScannerScreen() : React.ReactNode {
|
||||
useEffect(() => {
|
||||
if (url && typeof url === 'string' && !hasProcessedUrl.current) {
|
||||
hasProcessedUrl.current = true;
|
||||
handleBarcodeScanned({ data: url });
|
||||
handleQRCodeScanned(url);
|
||||
}
|
||||
}, [url, handleBarcodeScanned]);
|
||||
}, [url, handleQRCodeScanned]);
|
||||
|
||||
/**
|
||||
* Launch scanner when component mounts (Android/iOS only).
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (Platform.OS === 'android' || Platform.OS === 'ios') {
|
||||
launchScanner();
|
||||
}
|
||||
}, [launchScanner]);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
camera: {
|
||||
flex: 1,
|
||||
},
|
||||
cameraContainer: {
|
||||
backgroundColor: colors.black,
|
||||
flex: 1,
|
||||
},
|
||||
cameraOverlay: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
},
|
||||
cameraOverlayText: {
|
||||
color: colors.white,
|
||||
fontSize: 16,
|
||||
marginTop: 20,
|
||||
paddingHorizontal: 40,
|
||||
textAlign: 'center',
|
||||
},
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
right: 16,
|
||||
top: 16,
|
||||
zIndex: 10,
|
||||
},
|
||||
loadingContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
@@ -179,35 +159,14 @@ export default function QRScannerScreen() : React.ReactNode {
|
||||
},
|
||||
});
|
||||
|
||||
// Show permission request screen
|
||||
if (!permission || !permission.granted) {
|
||||
return (
|
||||
<ThemedContainer>
|
||||
<View style={styles.loadingContainer}>
|
||||
<LoadingIndicator />
|
||||
</View>
|
||||
</ThemedContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// Show loading while scanner is launching
|
||||
return (
|
||||
<ThemedContainer style={styles.container}>
|
||||
<View style={styles.cameraContainer}>
|
||||
<CameraView
|
||||
style={styles.camera}
|
||||
facing="back"
|
||||
barcodeScannerSettings={{
|
||||
barcodeTypes: ['qr'],
|
||||
}}
|
||||
onBarcodeScanned={handleBarcodeScanned}
|
||||
>
|
||||
<View style={styles.cameraOverlay}>
|
||||
<Ionicons name="qr-code-outline" size={100} color={colors.white} />
|
||||
<ThemedText style={styles.cameraOverlayText}>
|
||||
{t('settings.qrScanner.scanningMessage')}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</CameraView>
|
||||
<View style={styles.loadingContainer}>
|
||||
<LoadingIndicator />
|
||||
<ThemedText style={{ marginTop: 20, color: colors.textMuted }}>
|
||||
{t('settings.qrScanner.scanningMessage')}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</ThemedContainer>
|
||||
);
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
"notice": "Huomautus",
|
||||
"enabled": "Otettu käyttöön",
|
||||
"disabled": "Pois käytöstä",
|
||||
"twoFactorAuthentication": "Two-Factor Authentication",
|
||||
"deleteItemConfirmTitle": "Delete Item",
|
||||
"deleteItemConfirmDescription": "Are you sure you want to delete this item?",
|
||||
"twoFactorAuthentication": "Kaksivaiheinen tunnistautuminen",
|
||||
"deleteItemConfirmTitle": "Poista kohde",
|
||||
"deleteItemConfirmDescription": "Haluatko varmasti poistaa tämän kohteen?",
|
||||
"errors": {
|
||||
"unknownError": "Tapahtui tuntematon virhe. Yritä uudelleen.",
|
||||
"unknownErrorTryAgain": "Tapahtui tuntematon virhe. Yritä uudelleen.",
|
||||
@@ -207,13 +207,13 @@
|
||||
"passkeyWillBeDeleted": "Tämä todennusavain poistetaan, kun tallennat tämän käyttäjätiedon."
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Add 2FA Code",
|
||||
"nameOptional": "Name (optional)",
|
||||
"secretKey": "Secret Key",
|
||||
"instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.",
|
||||
"saveToViewCode": "Save to view code",
|
||||
"addCode": "Lisää 2FA TOTP -koodi",
|
||||
"nameOptional": "Nimi (valinnainen)",
|
||||
"secretKey": "Salainen avain",
|
||||
"instructions": "Syötä salainen avain, joka näkyy sivustossa, jossa haluat lisätä kaksivaiheisen tunnistautumisen",
|
||||
"saveToViewCode": "Tallenna nähdäksesi koodin",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Invalid secret key format."
|
||||
"invalidSecretKey": "Virheellinen salatun avaimen muoto."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -328,8 +328,8 @@
|
||||
"languageDescription": "Aseta kieli, jota käytetään luotaessa uusia henkilöllisyyksiä.",
|
||||
"genderSection": "Sukupuoli",
|
||||
"genderDescription": "Aseta oletussukupuoli uusien henkilöllisyyksien luomiseksi. ",
|
||||
"ageRangeSection": "Age Range",
|
||||
"ageRangeDescription": "Set the age range for generating new identities.",
|
||||
"ageRangeSection": "Ikähaarukka",
|
||||
"ageRangeDescription": "Aseta ikähaarukka uusia henkilöllisyyksien luomisessa",
|
||||
"genderOptions": {
|
||||
"random": "Satunnainen",
|
||||
"male": "Mies",
|
||||
|
||||
@@ -7,22 +7,22 @@
|
||||
"yes": "Oui",
|
||||
"no": "Non",
|
||||
"ok": "OK",
|
||||
"continue": "Continue",
|
||||
"loading": "Loading",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"never": "Never",
|
||||
"copied": "Copied to clipboard",
|
||||
"continue": "Continuer",
|
||||
"loading": "Chargement",
|
||||
"error": "Erreur",
|
||||
"success": "Succès",
|
||||
"never": "Jamais",
|
||||
"copied": "Copier dans le presse-papiers",
|
||||
"loadMore": "Voir plus",
|
||||
"use": "Use",
|
||||
"confirm": "Confirm",
|
||||
"next": "Next",
|
||||
"notice": "Notice",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"twoFactorAuthentication": "Two-Factor Authentication",
|
||||
"deleteItemConfirmTitle": "Delete Item",
|
||||
"deleteItemConfirmDescription": "Are you sure you want to delete this item?",
|
||||
"use": "Utiliser",
|
||||
"confirm": "Confirmer",
|
||||
"next": "Suivant",
|
||||
"notice": "Notification",
|
||||
"enabled": "Activé",
|
||||
"disabled": "Désactivé",
|
||||
"twoFactorAuthentication": "Authentification à deux facteurs",
|
||||
"deleteItemConfirmTitle": "Supprimer l'élement",
|
||||
"deleteItemConfirmDescription": "Êtes-vous certain de vouloir supprimer cet élément?",
|
||||
"errors": {
|
||||
"unknownError": "An unknown error occurred. Please try again.",
|
||||
"unknownErrorTryAgain": "An unknown error occurred. Please try again.",
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
"notice": "Uwaga",
|
||||
"enabled": "Włączone",
|
||||
"disabled": "Wyłączone",
|
||||
"twoFactorAuthentication": "Two-Factor Authentication",
|
||||
"deleteItemConfirmTitle": "Delete Item",
|
||||
"deleteItemConfirmDescription": "Are you sure you want to delete this item?",
|
||||
"twoFactorAuthentication": "Uwierzytelnianie dwuskładnikowe",
|
||||
"deleteItemConfirmTitle": "Usuń element",
|
||||
"deleteItemConfirmDescription": "Czy na pewno chcesz usunąć ten element?",
|
||||
"errors": {
|
||||
"unknownError": "Wystąpił nieznany błąd. Spróbuj ponownie.",
|
||||
"unknownErrorTryAgain": "Wystąpił nieznany błąd. Spróbuj ponownie.",
|
||||
@@ -207,13 +207,13 @@
|
||||
"passkeyWillBeDeleted": "Ten klucz dostępu zostanie usunięty po zapisaniu tych danych."
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Add 2FA Code",
|
||||
"nameOptional": "Name (optional)",
|
||||
"secretKey": "Secret Key",
|
||||
"instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.",
|
||||
"saveToViewCode": "Save to view code",
|
||||
"addCode": "Dodaj kod 2FA",
|
||||
"nameOptional": "Nazwa (opcjonalnie)",
|
||||
"secretKey": "Tajny klucz",
|
||||
"instructions": "Wprowadź tajny klucz wyświetlony na stronie internetowej, na której chcesz dodać uwierzytelnianie dwuskładnikowe.",
|
||||
"saveToViewCode": "Zapisz, aby wyświetlić kod",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Invalid secret key format."
|
||||
"invalidSecretKey": "Nieprawidłowy format tajnego klucza."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
"notice": "Примечание",
|
||||
"enabled": "Включено",
|
||||
"disabled": "Отключено",
|
||||
"twoFactorAuthentication": "Two-Factor Authentication",
|
||||
"deleteItemConfirmTitle": "Delete Item",
|
||||
"deleteItemConfirmDescription": "Are you sure you want to delete this item?",
|
||||
"twoFactorAuthentication": "Двухфакторная аутентификация",
|
||||
"deleteItemConfirmTitle": "Удалить элемент",
|
||||
"deleteItemConfirmDescription": "Вы уверены, что хотите удалить этот элемент?",
|
||||
"errors": {
|
||||
"unknownError": "Произошла неизвестная ошибка. Пожалуйста, попробуйте снова.",
|
||||
"unknownErrorTryAgain": "Произошла неизвестная ошибка. Попробуйте снова.",
|
||||
@@ -207,13 +207,13 @@
|
||||
"passkeyWillBeDeleted": "Этот ключ доступа будет удален при сохранении этой учетной записи."
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Add 2FA Code",
|
||||
"nameOptional": "Name (optional)",
|
||||
"secretKey": "Secret Key",
|
||||
"instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.",
|
||||
"saveToViewCode": "Save to view code",
|
||||
"addCode": "Добавить код 2FA",
|
||||
"nameOptional": "Имя (необязательно)",
|
||||
"secretKey": "Секретный ключ",
|
||||
"instructions": "Введите секретный ключ, указанный на веб-сайте, где вы хотите добавить двухфакторную аутентификацию.",
|
||||
"saveToViewCode": "Сохранить для просмотра кода",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Invalid secret key format."
|
||||
"invalidSecretKey": "Неверный формат секретного ключа."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 60;
|
||||
objectVersion = 70;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -212,7 +212,7 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
CEE9098F2DA548C7008D568F /* Exceptions for "Autofill" folder in "Autofill" target */ = {
|
||||
CEE9098F2DA548C7008D568F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
@@ -222,84 +222,13 @@
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
CE59C7602E4F47FD0024A246 /* VaultUITests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE77825E2EA1822400A75E6F /* VaultUtils */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = VaultUtils;
|
||||
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>"; };
|
||||
CE77825E2EA1822400A75E6F /* VaultUtils */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultUtils; 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 */
|
||||
@@ -1297,7 +1226,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2500901;
|
||||
CURRENT_PROJECT_VERSION = 2501900;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
@@ -1312,7 +1241,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.25.0;
|
||||
MARKETING_VERSION = 0.25.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1338,7 +1267,7 @@
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 2500901;
|
||||
CURRENT_PROJECT_VERSION = 2501900;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
INFOPLIST_FILE = AliasVault/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = AliasVault;
|
||||
@@ -1348,7 +1277,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.25.0;
|
||||
MARKETING_VERSION = 0.25.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1418,10 +1347,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";
|
||||
@@ -1475,10 +1401,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;
|
||||
@@ -1499,7 +1422,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2500901;
|
||||
CURRENT_PROJECT_VERSION = 2501900;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1535,7 +1458,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2500901;
|
||||
CURRENT_PROJECT_VERSION = 2501900;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1569,7 +1492,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2500901;
|
||||
CURRENT_PROJECT_VERSION = 2501900;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1626,7 +1549,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2500901;
|
||||
CURRENT_PROJECT_VERSION = 2501900;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1679,7 +1602,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2500901;
|
||||
CURRENT_PROJECT_VERSION = 2501900;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1732,7 +1655,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2500901;
|
||||
CURRENT_PROJECT_VERSION = 2501900;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1781,7 +1704,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2500901;
|
||||
CURRENT_PROJECT_VERSION = 2501900;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1816,7 +1739,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2500901;
|
||||
CURRENT_PROJECT_VERSION = 2501900;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1849,7 +1772,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2500901;
|
||||
CURRENT_PROJECT_VERSION = 2501900;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1902,7 +1825,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2500901;
|
||||
CURRENT_PROJECT_VERSION = 2501900;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1951,7 +1874,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2500901;
|
||||
CURRENT_PROJECT_VERSION = 2501900;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2003,7 +1926,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2500901;
|
||||
CURRENT_PROJECT_VERSION = 2501900;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2054,7 +1977,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = autofill/autofill.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2500901;
|
||||
CURRENT_PROJECT_VERSION = 2501900;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -2070,7 +1993,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 0.25.0;
|
||||
MARKETING_VERSION = 0.25.1;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
@@ -2099,7 +2022,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = autofill/autofill.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2500901;
|
||||
CURRENT_PROJECT_VERSION = 2501900;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -2115,7 +2038,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 0.25.0;
|
||||
MARKETING_VERSION = 0.25.1;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.app.autofill;
|
||||
|
||||
@@ -291,4 +291,10 @@
|
||||
[vaultManager authenticateUser:title subtitle:subtitle resolver:resolve rejecter:reject];
|
||||
}
|
||||
|
||||
// MARK: - QR Code Scanner
|
||||
|
||||
- (void)scanQRCode:(NSArray<NSString *> *)prefixes statusText:(NSString *)statusText resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
||||
[vaultManager scanQRCode:prefixes statusText:statusText resolver:resolve rejecter:reject];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -5,6 +5,7 @@ import VaultStoreKit
|
||||
import VaultModels
|
||||
import SwiftUI
|
||||
import VaultUI
|
||||
import AVFoundation
|
||||
|
||||
/**
|
||||
* This class is used as a bridge to allow React Native to interact with the VaultStoreKit class.
|
||||
@@ -913,6 +914,42 @@ public class VaultManager: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func scanQRCode(_ prefixes: [String]?,
|
||||
statusText: String?,
|
||||
resolver resolve: @escaping RCTPromiseResolveBlock,
|
||||
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
||||
DispatchQueue.main.async {
|
||||
// Get the root view controller from React Native
|
||||
guard let rootVC = RCTPresentedViewController() else {
|
||||
reject("NO_VIEW_CONTROLLER", "No view controller available", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Create QR scanner view with optional prefix filtering and custom status text
|
||||
let scannerView = QRScannerView(
|
||||
prefixes: prefixes,
|
||||
statusText: statusText,
|
||||
onCodeScanned: { code in
|
||||
// Resolve immediately and dismiss without waiting (matches Android behavior)
|
||||
resolve(code)
|
||||
rootVC.dismiss(animated: true)
|
||||
},
|
||||
onCancel: {
|
||||
// Cancel resolves nil and dismisses
|
||||
resolve(nil)
|
||||
rootVC.dismiss(animated: true)
|
||||
}
|
||||
)
|
||||
|
||||
let hostingController = UIHostingController(rootView: scannerView)
|
||||
|
||||
// Present modally as full screen
|
||||
hostingController.modalPresentationStyle = .fullScreen
|
||||
rootVC.present(hostingController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func authenticateUser(_ title: String?,
|
||||
subtitle: String?,
|
||||
|
||||
@@ -272,10 +272,6 @@ PODS:
|
||||
- ExpoModulesCore
|
||||
- ExpoBlur (14.1.5):
|
||||
- ExpoModulesCore
|
||||
- ExpoCamera (16.1.11):
|
||||
- ExpoModulesCore
|
||||
- ZXingObjC/OneD
|
||||
- ZXingObjC/PDF417
|
||||
- ExpoClipboard (7.1.5):
|
||||
- ExpoModulesCore
|
||||
- ExpoDocumentPicker (13.1.6):
|
||||
@@ -2449,11 +2445,6 @@ PODS:
|
||||
- SwiftLint (0.59.1)
|
||||
- SWXMLHash (7.0.2)
|
||||
- Yoga (0.0.0)
|
||||
- ZXingObjC/Core (3.6.9)
|
||||
- ZXingObjC/OneD (3.6.9):
|
||||
- ZXingObjC/Core
|
||||
- ZXingObjC/PDF417 (3.6.9):
|
||||
- ZXingObjC/Core
|
||||
|
||||
DEPENDENCIES:
|
||||
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
||||
@@ -2468,7 +2459,6 @@ DEPENDENCIES:
|
||||
- expo-dev-menu-interface (from `../node_modules/expo-dev-menu-interface/ios`)
|
||||
- ExpoAsset (from `../node_modules/expo-asset/ios`)
|
||||
- ExpoBlur (from `../node_modules/expo-blur/ios`)
|
||||
- ExpoCamera (from `../node_modules/expo-camera/ios`)
|
||||
- ExpoClipboard (from `../node_modules/expo-clipboard/ios`)
|
||||
- ExpoDocumentPicker (from `../node_modules/expo-document-picker/ios`)
|
||||
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
|
||||
@@ -2582,7 +2572,6 @@ SPEC REPOS:
|
||||
- SQLite.swift
|
||||
- SwiftLint
|
||||
- SWXMLHash
|
||||
- ZXingObjC
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
boost:
|
||||
@@ -2609,8 +2598,6 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/expo-asset/ios"
|
||||
ExpoBlur:
|
||||
:path: "../node_modules/expo-blur/ios"
|
||||
ExpoCamera:
|
||||
:path: "../node_modules/expo-camera/ios"
|
||||
ExpoClipboard:
|
||||
:path: "../node_modules/expo-clipboard/ios"
|
||||
ExpoDocumentPicker:
|
||||
@@ -2820,7 +2807,6 @@ SPEC CHECKSUMS:
|
||||
expo-dev-menu-interface: 609c35ae8b97479cdd4c9e23c8cf6adc44beea0e
|
||||
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
|
||||
ExpoBlur: 3c8885b9bf9eef4309041ec87adec48b5f1986a9
|
||||
ExpoCamera: e1879906d41184e84b57d7643119f8509414e318
|
||||
ExpoClipboard: 436f6de6971f14eb75ae160e076d9cb3b19eb795
|
||||
ExpoDocumentPicker: b263a279685b6640b8c8bc70d71c83067aeaae55
|
||||
ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63
|
||||
@@ -2925,7 +2911,6 @@ SPEC CHECKSUMS:
|
||||
SwiftLint: 3d48e2fb2a3468fdaccf049e5e755df22fb40c2c
|
||||
SWXMLHash: dd733a457e9c4fe93b1538654057aefae4acb382
|
||||
Yoga: dc7c21200195acacb62fa920c588e7c2106de45e
|
||||
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
|
||||
|
||||
PODFILE CHECKSUM: ac288e273086bafdd610cafff08ccca0d164f7c3
|
||||
|
||||
|
||||
240
apps/mobile-app/ios/VaultUI/QRScanner/QRScannerView.swift
Normal file
240
apps/mobile-app/ios/VaultUI/QRScanner/QRScannerView.swift
Normal file
@@ -0,0 +1,240 @@
|
||||
import SwiftUI
|
||||
import AVFoundation
|
||||
|
||||
private let locBundle = Bundle.vaultUI
|
||||
|
||||
/// SwiftUI view for scanning QR codes using AVFoundation
|
||||
public struct QRScannerView: View {
|
||||
let onCodeScanned: (String) -> Void
|
||||
let onCancel: () -> Void
|
||||
let prefixes: [String]?
|
||||
let statusText: String
|
||||
|
||||
@State private var hasScanned = false
|
||||
@State private var showFlash = false
|
||||
|
||||
public init(
|
||||
prefixes: [String]? = nil,
|
||||
statusText: String? = nil,
|
||||
onCodeScanned: @escaping (String) -> Void,
|
||||
onCancel: @escaping () -> Void
|
||||
) {
|
||||
self.prefixes = prefixes
|
||||
self.statusText = statusText?.isEmpty == false ? statusText! : "Scan QR code"
|
||||
self.onCodeScanned = onCodeScanned
|
||||
self.onCancel = onCancel
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
// Camera preview
|
||||
QRScannerRepresentable(
|
||||
prefixes: prefixes,
|
||||
onCodeScanned: { code in
|
||||
if !hasScanned {
|
||||
hasScanned = true
|
||||
showFlash = true
|
||||
|
||||
// Flash animation then callback
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
||||
onCodeScanned(code)
|
||||
}
|
||||
}
|
||||
},
|
||||
onCodeRejected: {
|
||||
// Reset hasScanned to allow scanning again
|
||||
hasScanned = false
|
||||
}
|
||||
)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
// Overlay with viewfinder
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
// Viewfinder frame
|
||||
Rectangle()
|
||||
.stroke(Color.white, lineWidth: 3)
|
||||
.frame(width: 280, height: 280)
|
||||
.overlay(
|
||||
// Flash effect
|
||||
Rectangle()
|
||||
.fill(Color.white)
|
||||
.opacity(showFlash ? 0.7 : 0)
|
||||
.animation(.easeInOut(duration: 0.2), value: showFlash)
|
||||
)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Status text
|
||||
Text(statusText)
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
.background(Color.black.opacity(0.7))
|
||||
.cornerRadius(10)
|
||||
.padding(.bottom, 50)
|
||||
}
|
||||
|
||||
// Cancel button
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: onCancel) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 32))
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.background(Color.black)
|
||||
}
|
||||
}
|
||||
|
||||
/// UIViewControllerRepresentable wrapper for AVFoundation camera
|
||||
struct QRScannerRepresentable: UIViewControllerRepresentable {
|
||||
let prefixes: [String]?
|
||||
let onCodeScanned: (String) -> Void
|
||||
let onCodeRejected: () -> Void
|
||||
|
||||
func makeUIViewController(context: Context) -> QRScannerViewController {
|
||||
let controller = QRScannerViewController()
|
||||
controller.prefixes = prefixes
|
||||
controller.onCodeScanned = onCodeScanned
|
||||
controller.onCodeRejected = onCodeRejected
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: QRScannerViewController, context: Context) {
|
||||
// No updates needed
|
||||
}
|
||||
}
|
||||
|
||||
/// UIViewController that handles AVFoundation QR code scanning
|
||||
class QRScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
|
||||
var captureSession: AVCaptureSession?
|
||||
var previewLayer: AVCaptureVideoPreviewLayer?
|
||||
var prefixes: [String]?
|
||||
var onCodeScanned: ((String) -> Void)?
|
||||
var onCodeRejected: (() -> Void)?
|
||||
private var rejectedQRCodes = Set<String>() // Track rejected QR codes to avoid repeated haptic feedback
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupCamera()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
if let session = captureSession, !session.isRunning {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
session.startRunning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
if let session = captureSession, session.isRunning {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
session.stopRunning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupCamera() {
|
||||
let session = AVCaptureSession()
|
||||
|
||||
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
|
||||
return
|
||||
}
|
||||
|
||||
let videoInput: AVCaptureDeviceInput
|
||||
|
||||
do {
|
||||
videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
if session.canAddInput(videoInput) {
|
||||
session.addInput(videoInput)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
let metadataOutput = AVCaptureMetadataOutput()
|
||||
|
||||
if session.canAddOutput(metadataOutput) {
|
||||
session.addOutput(metadataOutput)
|
||||
|
||||
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
|
||||
metadataOutput.metadataObjectTypes = [.qr]
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
|
||||
previewLayer.frame = view.layer.bounds
|
||||
previewLayer.videoGravity = .resizeAspectFill
|
||||
view.layer.addSublayer(previewLayer)
|
||||
|
||||
self.captureSession = session
|
||||
self.previewLayer = previewLayer
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
session.startRunning()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
previewLayer?.frame = view.layer.bounds
|
||||
}
|
||||
|
||||
func metadataOutput(
|
||||
_ output: AVCaptureMetadataOutput,
|
||||
didOutput metadataObjects: [AVMetadataObject],
|
||||
from connection: AVCaptureConnection
|
||||
) {
|
||||
if let metadataObject = metadataObjects.first,
|
||||
let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
|
||||
let stringValue = readableObject.stringValue {
|
||||
|
||||
// Check if prefixes filter is enabled
|
||||
if let prefixes = prefixes, !prefixes.isEmpty {
|
||||
// Check if the scanned code starts with any of the accepted prefixes
|
||||
let hasValidPrefix = prefixes.contains { prefix in
|
||||
stringValue.hasPrefix(prefix)
|
||||
}
|
||||
|
||||
if !hasValidPrefix {
|
||||
// Invalid QR code - only give haptic feedback once per unique code
|
||||
if !rejectedQRCodes.contains(stringValue) {
|
||||
let generator = UINotificationFeedbackGenerator()
|
||||
generator.notificationOccurred(.warning)
|
||||
rejectedQRCodes.insert(stringValue)
|
||||
}
|
||||
|
||||
// Notify that code was rejected (to reset UI state if needed)
|
||||
onCodeRejected?()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Valid QR code - stop scanning
|
||||
captureSession?.stopRunning()
|
||||
|
||||
// Success haptic feedback
|
||||
let generator = UINotificationFeedbackGenerator()
|
||||
generator.notificationOccurred(.success)
|
||||
|
||||
// Callback with scanned code
|
||||
onCodeScanned?(stringValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
85
apps/mobile-app/package-lock.json
generated
85
apps/mobile-app/package-lock.json
generated
@@ -17,10 +17,8 @@
|
||||
"@types/jsrsasign": "^10.5.15",
|
||||
"expo": "^53.0.22",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-camera": "^16.1.11",
|
||||
"expo-clipboard": "~7.1.5",
|
||||
"expo-constants": "~17.1.7",
|
||||
"expo-dev-client": "~5.1.8",
|
||||
"expo-document-picker": "~13.1.6",
|
||||
"expo-file-system": "~18.1.11",
|
||||
"expo-font": "~13.3.2",
|
||||
@@ -37,8 +35,7 @@
|
||||
"expo-web-browser": "~14.2.0",
|
||||
"fbemitter": "^3.0.0",
|
||||
"i18next": "^25.3.2",
|
||||
"jest": "~29.7.0",
|
||||
"jest-expo": "~53.0.10",
|
||||
"lodash": "^4.17.21",
|
||||
"otpauth": "^9.4.0",
|
||||
"react": "19.0.0",
|
||||
"react-hook-form": "^7.56.1",
|
||||
@@ -55,6 +52,7 @@
|
||||
"react-native-safe-area-context": "5.6.1",
|
||||
"react-native-screens": "~4.15.4",
|
||||
"react-native-svg": "15.11.2",
|
||||
"react-native-svg-transformer": "^1.5.0",
|
||||
"react-native-toast-message": "^2.2.1",
|
||||
"react-native-webview": "13.13.5",
|
||||
"secure-remote-password": "github:LinusU/secure-remote-password#73e5f732b6ca0cdbdc19da1a0c5f48cdbad2cbf0",
|
||||
@@ -77,10 +75,10 @@
|
||||
"eslint-config-expo": "~9.2.0",
|
||||
"eslint-plugin-jsdoc": "^55.2.0",
|
||||
"eslint-plugin-react-native": "^5.0.0",
|
||||
"expo-dev-client": "~5.1.8",
|
||||
"globals": "^16.3.0",
|
||||
"jest": "^29.2.1",
|
||||
"jest-expo": "~53.0.0",
|
||||
"react-native-svg-transformer": "^1.5.0",
|
||||
"typescript": "~5.8.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4223,7 +4221,6 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
|
||||
"integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -4240,7 +4237,6 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz",
|
||||
"integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -4257,7 +4253,6 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz",
|
||||
"integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -4274,7 +4269,6 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz",
|
||||
"integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -4291,7 +4285,6 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz",
|
||||
"integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -4308,7 +4301,6 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz",
|
||||
"integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -4325,7 +4317,6 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz",
|
||||
"integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -4342,7 +4333,6 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz",
|
||||
"integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -4359,7 +4349,6 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz",
|
||||
"integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@svgr/babel-plugin-add-jsx-attribute": "8.0.0",
|
||||
@@ -4386,7 +4375,6 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz",
|
||||
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
@@ -4407,7 +4395,6 @@
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -4420,7 +4407,6 @@
|
||||
"version": "8.3.6",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
|
||||
"integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"import-fresh": "^3.3.0",
|
||||
@@ -4447,7 +4433,6 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz",
|
||||
"integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.21.3",
|
||||
@@ -4465,7 +4450,6 @@
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
@@ -4478,7 +4462,6 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz",
|
||||
"integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
@@ -4501,7 +4484,6 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz",
|
||||
"integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cosmiconfig": "^8.1.3",
|
||||
@@ -4523,7 +4505,6 @@
|
||||
"version": "8.3.6",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
|
||||
"integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"import-fresh": "^3.3.0",
|
||||
@@ -4560,7 +4541,6 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
@@ -6707,7 +6687,6 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -7294,7 +7273,6 @@
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
|
||||
"integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-tree": "~2.2.0"
|
||||
@@ -7308,7 +7286,6 @@
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
|
||||
"integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.28",
|
||||
@@ -7323,7 +7300,6 @@
|
||||
"version": "2.0.28",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
|
||||
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/cssom": {
|
||||
@@ -7734,7 +7710,6 @@
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
|
||||
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"no-case": "^3.0.4",
|
||||
@@ -8772,26 +8747,6 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-camera": {
|
||||
"version": "16.1.11",
|
||||
"resolved": "https://registry.npmjs.org/expo-camera/-/expo-camera-16.1.11.tgz",
|
||||
"integrity": "sha512-etA5ZKoC6nPBnWWqiTmlX//zoFZ6cWQCCIdmpUHTGHAKd4qZNCkhPvBWbi8o32pDe57lix1V4+TPFgEcvPwsaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"invariant": "^2.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-web": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-native-web": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/expo-clipboard": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-7.1.5.tgz",
|
||||
@@ -8821,6 +8776,7 @@
|
||||
"version": "5.1.8",
|
||||
"resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-5.1.8.tgz",
|
||||
"integrity": "sha512-IopYPgBi3JflksO5ieTphbKsbYHy9iIVdT/d69It++y0iBMSm0oBIoDmUijrHKjE3fV6jnrwrm8luU13/mzIQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"expo-dev-launcher": "5.1.11",
|
||||
@@ -8837,6 +8793,7 @@
|
||||
"version": "5.1.11",
|
||||
"resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-5.1.11.tgz",
|
||||
"integrity": "sha512-bN0+nv5H038s8Gzf8i16hwCyD3sWDmHp7vb+QbL1i6B3XNnICCKS/H/3VH6H3PRMvCmoLGPlg+ODDqGlf0nu3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "8.11.0",
|
||||
@@ -8852,6 +8809,7 @@
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
|
||||
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
@@ -8868,12 +8826,14 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/expo-dev-menu": {
|
||||
"version": "6.1.10",
|
||||
"resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-6.1.10.tgz",
|
||||
"integrity": "sha512-LaI0Bw5zzw5XefjYSX6YaMydzk0YBysjqQoxzj6ufDyKgwAfPmFwOLkZ03DOSerc9naezGLNAGgTEN6QTgMmgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"expo-dev-menu-interface": "1.10.0"
|
||||
@@ -8886,6 +8846,7 @@
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-1.10.0.tgz",
|
||||
"integrity": "sha512-NxtM/qot5Rh2cY333iOE87dDg1S8CibW+Wu4WdLua3UMjy81pXYzAGCZGNOeY7k9GpNFqDPNDXWyBSlk9r2pBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
@@ -8936,6 +8897,7 @@
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz",
|
||||
"integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/expo-keep-awake": {
|
||||
@@ -9002,6 +8964,7 @@
|
||||
"version": "0.16.6",
|
||||
"resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.16.6.tgz",
|
||||
"integrity": "sha512-1A+do6/mLUWF9xd3uCrlXr9QFDbjbfqAYmUy8UDLOjof1lMrOhyeC4Yi6WexA/A8dhZEpIxSMCKfn7G4aHAh4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/config": "~11.0.12",
|
||||
@@ -9153,6 +9116,7 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-1.1.0.tgz",
|
||||
"integrity": "sha512-DeB+fRe0hUDPZhpJ4X4bFMAItatFBUPjw/TVSbJsaf3Exeami+2qbbJhWkcTMoYHOB73nOIcaYcWXYJnCJXO0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
@@ -10179,7 +10143,6 @@
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parent-module": "^1.0.0",
|
||||
@@ -10196,7 +10159,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
@@ -11968,7 +11930,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
@@ -12374,7 +12335,6 @@
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
@@ -12616,7 +12576,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
|
||||
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.3"
|
||||
@@ -13397,7 +13356,6 @@
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
||||
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lower-case": "^2.0.2",
|
||||
@@ -13457,9 +13415,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz",
|
||||
"integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==",
|
||||
"license": "(BSD-3-Clause OR GPL-2.0)",
|
||||
"engines": {
|
||||
"node": ">= 6.13.0"
|
||||
@@ -13931,7 +13889,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"callsites": "^3.0.0"
|
||||
@@ -13954,7 +13911,6 @@
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
@@ -14014,7 +13970,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
|
||||
"integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
@@ -14076,7 +14031,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -14949,7 +14903,6 @@
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-svg-transformer/-/react-native-svg-transformer-1.5.1.tgz",
|
||||
"integrity": "sha512-dFvBNR8A9VPum9KCfh+LE49YiJEF8zUSnEFciKQroR/bEOhlPoZA0SuQ0qNk7m2iZl2w59FYjdRe0pMHWMDl0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@svgr/core": "^8.1.0",
|
||||
@@ -16119,7 +16072,6 @@
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
|
||||
"integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dot-case": "^3.0.4",
|
||||
@@ -16690,14 +16642,12 @@
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz",
|
||||
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/svgo": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
|
||||
"integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@trysound/sax": "0.2.0",
|
||||
@@ -16723,7 +16673,6 @@
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
@@ -16733,7 +16682,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.30",
|
||||
@@ -16747,7 +16695,6 @@
|
||||
"version": "2.0.30",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
@@ -17147,7 +17094,6 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
@@ -17490,6 +17436,7 @@
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0"
|
||||
|
||||
@@ -38,10 +38,8 @@
|
||||
"@types/jsrsasign": "^10.5.15",
|
||||
"expo": "^53.0.22",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-camera": "^16.1.11",
|
||||
"expo-clipboard": "~7.1.5",
|
||||
"expo-constants": "~17.1.7",
|
||||
"expo-dev-client": "~5.1.8",
|
||||
"expo-document-picker": "~13.1.6",
|
||||
"expo-file-system": "~18.1.11",
|
||||
"expo-font": "~13.3.2",
|
||||
@@ -58,8 +56,7 @@
|
||||
"expo-web-browser": "~14.2.0",
|
||||
"fbemitter": "^3.0.0",
|
||||
"i18next": "^25.3.2",
|
||||
"jest": "~29.7.0",
|
||||
"jest-expo": "~53.0.10",
|
||||
"lodash": "^4.17.21",
|
||||
"otpauth": "^9.4.0",
|
||||
"react": "19.0.0",
|
||||
"react-hook-form": "^7.56.1",
|
||||
@@ -76,6 +73,7 @@
|
||||
"react-native-safe-area-context": "5.6.1",
|
||||
"react-native-screens": "~4.15.4",
|
||||
"react-native-svg": "15.11.2",
|
||||
"react-native-svg-transformer": "^1.5.0",
|
||||
"react-native-toast-message": "^2.2.1",
|
||||
"react-native-webview": "13.13.5",
|
||||
"secure-remote-password": "github:LinusU/secure-remote-password#73e5f732b6ca0cdbdc19da1a0c5f48cdbad2cbf0",
|
||||
@@ -98,10 +96,10 @@
|
||||
"eslint-config-expo": "~9.2.0",
|
||||
"eslint-plugin-jsdoc": "^55.2.0",
|
||||
"eslint-plugin-react-native": "^5.0.0",
|
||||
"expo-dev-client": "~5.1.8",
|
||||
"globals": "^16.3.0",
|
||||
"jest": "^29.2.1",
|
||||
"jest-expo": "~53.0.0",
|
||||
"react-native-svg-transformer": "^1.5.0",
|
||||
"typescript": "~5.8.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -105,6 +105,13 @@ export interface Spec extends TurboModule {
|
||||
// Re-authentication methods
|
||||
// Authenticate user with biometric or PIN. If title/subtitle are null/empty, defaults to "Unlock Vault" context.
|
||||
authenticateUser(title: string | null, subtitle: string | null): Promise<boolean>;
|
||||
|
||||
// QR code scanner
|
||||
// Scan a QR code and return the scanned data. Returns null if cancelled or failed.
|
||||
// If prefixes is provided, only QR codes starting with one of these prefixes will be accepted.
|
||||
// Scanner will keep scanning until a matching code is found or user cancels.
|
||||
// statusText is the message to display on the scanner screen (defaults to "Scan QR code" if null/empty).
|
||||
scanQRCode(prefixes: string[] | null, statusText: string | null): Promise<string | null>;
|
||||
}
|
||||
|
||||
export default TurboModuleRegistry.getEnforcing<Spec>('NativeVaultManager');
|
||||
|
||||
@@ -8,7 +8,7 @@ export class AppInfo {
|
||||
/**
|
||||
* The current mobile app version. This should be updated with each release of the mobile app.
|
||||
*/
|
||||
public static readonly VERSION = '0.25.0';
|
||||
public static readonly VERSION = '0.25.1';
|
||||
|
||||
/**
|
||||
* The API version to send to the server (base semver without stage suffixes).
|
||||
|
||||
@@ -36,6 +36,20 @@ declare class PasswordGenerator {
|
||||
private readonly uppercaseChars;
|
||||
private readonly numberChars;
|
||||
private readonly specialChars;
|
||||
/**
|
||||
* Ambiguous characters that look similar and are easy to confuse when typing:
|
||||
* - I, l, 1, | (pipe) - all look like vertical lines
|
||||
* - O, 0, o - all look like circles
|
||||
* - Z, 2 - similar appearance
|
||||
* - S, 5 - similar appearance
|
||||
* - B, 8 - similar appearance
|
||||
* - G, 6 - similar appearance
|
||||
* - Brackets, braces, parentheses: [], {}, ()
|
||||
* - Quotes: ', ", `
|
||||
* - Punctuation pairs: ;:, .,
|
||||
* - Dashes: -, _
|
||||
* - Angle brackets: <>
|
||||
*/
|
||||
private readonly ambiguousChars;
|
||||
private length;
|
||||
private useLowercase;
|
||||
|
||||
@@ -39,7 +39,21 @@ var PasswordGenerator = class {
|
||||
this.uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
this.numberChars = "0123456789";
|
||||
this.specialChars = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||||
this.ambiguousChars = "Il1O0o";
|
||||
/**
|
||||
* Ambiguous characters that look similar and are easy to confuse when typing:
|
||||
* - I, l, 1, | (pipe) - all look like vertical lines
|
||||
* - O, 0, o - all look like circles
|
||||
* - Z, 2 - similar appearance
|
||||
* - S, 5 - similar appearance
|
||||
* - B, 8 - similar appearance
|
||||
* - G, 6 - similar appearance
|
||||
* - Brackets, braces, parentheses: [], {}, ()
|
||||
* - Quotes: ', ", `
|
||||
* - Punctuation pairs: ;:, .,
|
||||
* - Dashes: -, _
|
||||
* - Angle brackets: <>
|
||||
*/
|
||||
this.ambiguousChars = "Il1O0oZzSsBbGg2568|[]{}()<>;:,.`'\"_-";
|
||||
this.length = 18;
|
||||
this.useLowercase = true;
|
||||
this.useUppercase = true;
|
||||
|
||||
@@ -13,7 +13,21 @@ var PasswordGenerator = class {
|
||||
this.uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
this.numberChars = "0123456789";
|
||||
this.specialChars = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||||
this.ambiguousChars = "Il1O0o";
|
||||
/**
|
||||
* Ambiguous characters that look similar and are easy to confuse when typing:
|
||||
* - I, l, 1, | (pipe) - all look like vertical lines
|
||||
* - O, 0, o - all look like circles
|
||||
* - Z, 2 - similar appearance
|
||||
* - S, 5 - similar appearance
|
||||
* - B, 8 - similar appearance
|
||||
* - G, 6 - similar appearance
|
||||
* - Brackets, braces, parentheses: [], {}, ()
|
||||
* - Quotes: ', ", `
|
||||
* - Punctuation pairs: ;:, .,
|
||||
* - Dashes: -, _
|
||||
* - Angle brackets: <>
|
||||
*/
|
||||
this.ambiguousChars = "Il1O0oZzSsBbGg2568|[]{}()<>;:,.`'\"_-";
|
||||
this.length = 18;
|
||||
this.useLowercase = true;
|
||||
this.useUppercase = true;
|
||||
|
||||
@@ -218,11 +218,7 @@ else
|
||||
v.RevisionNumber,
|
||||
CredentialCount = v.CredentialsCount,
|
||||
}),
|
||||
EmailClaims = u.EmailClaims.Select(ec => new
|
||||
{
|
||||
ec.CreatedAt,
|
||||
ec.Address
|
||||
}),
|
||||
EmailClaimCount = u.EmailClaims.Count(),
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -247,7 +243,7 @@ else
|
||||
IsInactive = isInactive,
|
||||
VaultCount = user.Vaults.Count(),
|
||||
CredentialCount = user.Vaults.OrderByDescending(x => x.RevisionNumber).First().CredentialCount,
|
||||
EmailClaimCount = user.EmailClaims.Count(),
|
||||
EmailClaimCount = user.EmailClaimCount,
|
||||
VaultStorageInKb = user.Vaults.Sum(x => x.FileSize),
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
@@ -15,6 +15,8 @@ else if (RelatedUsers.Any())
|
||||
</SortableTableColumn>
|
||||
<SortableTableColumn>@relatedUser.SharedIpAddresses.ToString("N0")</SortableTableColumn>
|
||||
<SortableTableColumn>@relatedUser.MostRecentSharedIp</SortableTableColumn>
|
||||
<SortableTableColumn>@relatedUser.CredentialsCount.ToString("N0")</SortableTableColumn>
|
||||
<SortableTableColumn>@relatedUser.EmailClaimsCount.ToString("N0")</SortableTableColumn>
|
||||
<SortableTableColumn>@relatedUser.RegistrationDate.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
|
||||
<SortableTableColumn><StatusPill Enabled="!relatedUser.IsBlocked" TextTrue="Active" TextFalse="Blocked" /></SortableTableColumn>
|
||||
</SortableTableRow>
|
||||
@@ -65,6 +67,8 @@ else
|
||||
new TableColumn { Title = "Username", PropertyName = "Username" },
|
||||
new TableColumn { Title = "Shared IPs", PropertyName = "SharedIpAddresses" },
|
||||
new TableColumn { Title = "Most Recent IP", PropertyName = "MostRecentSharedIp" },
|
||||
new TableColumn { Title = "Credentials", PropertyName = "CredentialsCount" },
|
||||
new TableColumn { Title = "Email Claims", PropertyName = "EmailClaimsCount" },
|
||||
new TableColumn { Title = "Registered", PropertyName = "RegistrationDate" },
|
||||
new TableColumn { Title = "Status", PropertyName = "IsBlocked" },
|
||||
];
|
||||
@@ -152,15 +156,32 @@ else
|
||||
|
||||
TotalRelatedUsers = users.Count;
|
||||
|
||||
// Get vault statistics for all related users (from their latest vault)
|
||||
var userIds = users.Select(u => u.Id).ToList();
|
||||
var vaultStats = await dbContext.Vaults
|
||||
.Where(v => userIds.Contains(v.UserId))
|
||||
.GroupBy(v => v.UserId)
|
||||
.Select(g => new
|
||||
{
|
||||
UserId = g.Key,
|
||||
CredentialsCount = g.OrderByDescending(v => v.RevisionNumber).First().CredentialsCount,
|
||||
EmailClaimsCount = g.OrderByDescending(v => v.RevisionNumber).First().EmailClaimsCount
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
// Combine the data
|
||||
RelatedUsers = (from user in users
|
||||
join data in relatedUserData on user.UserName equals data.Username
|
||||
join vaultStat in vaultStats on user.Id equals vaultStat.UserId into vaultGroup
|
||||
from vault in vaultGroup.DefaultIfEmpty()
|
||||
select new RelatedUserInfo
|
||||
{
|
||||
UserId = user.Id,
|
||||
Username = user.UserName ?? "Unknown",
|
||||
SharedIpAddresses = data.SharedIps,
|
||||
MostRecentSharedIp = data.MostRecentSharedIp ?? "-",
|
||||
CredentialsCount = vault?.CredentialsCount ?? 0,
|
||||
EmailClaimsCount = vault?.EmailClaimsCount ?? 0,
|
||||
RegistrationDate = user.CreatedAt,
|
||||
IsBlocked = user.Blocked
|
||||
})
|
||||
@@ -186,6 +207,8 @@ else
|
||||
"Username" => SortableTable.SortListByProperty(relatedUsers, r => r.Username, sortDirection),
|
||||
"SharedIpAddresses" => SortableTable.SortListByProperty(relatedUsers, r => r.SharedIpAddresses, sortDirection),
|
||||
"MostRecentSharedIp" => SortableTable.SortListByProperty(relatedUsers, r => r.MostRecentSharedIp, sortDirection),
|
||||
"CredentialsCount" => SortableTable.SortListByProperty(relatedUsers, r => r.CredentialsCount, sortDirection),
|
||||
"EmailClaimsCount" => SortableTable.SortListByProperty(relatedUsers, r => r.EmailClaimsCount, sortDirection),
|
||||
"RegistrationDate" => SortableTable.SortListByProperty(relatedUsers, r => r.RegistrationDate, sortDirection),
|
||||
"IsBlocked" => SortableTable.SortListByProperty(relatedUsers, r => r.IsBlocked, sortDirection),
|
||||
_ => relatedUsers
|
||||
@@ -206,6 +229,8 @@ else
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public int SharedIpAddresses { get; set; }
|
||||
public string MostRecentSharedIp { get; set; } = string.Empty;
|
||||
public int CredentialsCount { get; set; }
|
||||
public int EmailClaimsCount { get; set; }
|
||||
public DateTime RegistrationDate { get; set; }
|
||||
public bool IsBlocked { get; set; }
|
||||
}
|
||||
|
||||
@@ -332,6 +332,18 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await base.OnParametersSetAsync();
|
||||
|
||||
// Refresh data when navigating to a different user (e.g., clicking related user links)
|
||||
if (!IsLoading && User?.Id != Id)
|
||||
{
|
||||
await RefreshData();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshData()
|
||||
{
|
||||
IsLoading = true;
|
||||
|
||||
@@ -311,7 +311,18 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM
|
||||
return BadRequest(ApiErrorCodeHelper.CreateErrorResponse(ApiErrorCode.REFRESH_TOKEN_REQUIRED, 400));
|
||||
}
|
||||
|
||||
var principal = GetPrincipalFromToken(tokenModel.Token);
|
||||
ClaimsPrincipal principal;
|
||||
try
|
||||
{
|
||||
principal = GetPrincipalFromToken(tokenModel.Token);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// If token validation fails (expired, malformed, or invalid signature),
|
||||
// return unauthorized as we cannot identify the user from the access token.
|
||||
return Unauthorized(ApiErrorCodeHelper.CreateErrorResponse(ApiErrorCode.INVALID_REFRESH_TOKEN, 401));
|
||||
}
|
||||
|
||||
if (principal.FindFirst(ClaimTypes.NameIdentifier)?.Value == null)
|
||||
{
|
||||
return Unauthorized(ApiErrorCodeHelper.CreateErrorResponse(ApiErrorCode.USER_NOT_FOUND, 401));
|
||||
@@ -354,31 +365,24 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM
|
||||
{
|
||||
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
// If the token is not provided, return bad request.
|
||||
// If the refresh token is not provided, return bad request.
|
||||
if (string.IsNullOrWhiteSpace(model.RefreshToken))
|
||||
{
|
||||
return BadRequest(ApiErrorCodeHelper.CreateErrorResponse(ApiErrorCode.REFRESH_TOKEN_REQUIRED, 400));
|
||||
}
|
||||
|
||||
var principal = GetPrincipalFromToken(model.Token);
|
||||
if (principal.FindFirst(ClaimTypes.NameIdentifier)?.Value == null)
|
||||
// Look up the refresh token directly - we don't need to validate the access token
|
||||
// since the refresh token itself contains the user information we need.
|
||||
var refreshTokenEntry = await context.AliasVaultUserRefreshTokens.Include(t => t.User).FirstOrDefaultAsync(t => t.Value == model.RefreshToken);
|
||||
|
||||
if (refreshTokenEntry == null)
|
||||
{
|
||||
return Unauthorized(ApiErrorCodeHelper.CreateErrorResponse(ApiErrorCode.USER_NOT_FOUND, 401));
|
||||
// Token doesn't exist - could already be revoked or never existed.
|
||||
// Return success to avoid leaking information about token validity.
|
||||
return Ok();
|
||||
}
|
||||
|
||||
var user = await userManager.FindByIdAsync(principal.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty);
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized(ApiErrorCodeHelper.CreateErrorResponse(ApiErrorCode.USER_NOT_FOUND, 401));
|
||||
}
|
||||
|
||||
// Check if the refresh token is valid.
|
||||
var providedTokenExists = await context.AliasVaultUserRefreshTokens.AnyAsync(t => t.UserId == user.Id && t.Value == model.RefreshToken);
|
||||
if (!providedTokenExists)
|
||||
{
|
||||
await authLoggingService.LogAuthEventFailAsync(user.UserName!, AuthEventType.Logout, AuthFailureReason.InvalidRefreshToken);
|
||||
return Unauthorized(ApiErrorCodeHelper.CreateErrorResponse(ApiErrorCode.INVALID_REFRESH_TOKEN, 401));
|
||||
}
|
||||
var user = refreshTokenEntry.User;
|
||||
|
||||
// Remove the provided refresh token and any other existing refresh tokens that are issued to the current device ID.
|
||||
// This to make sure all tokens are revoked for this device that user is "logging out" from.
|
||||
|
||||
@@ -481,9 +481,6 @@ else
|
||||
|
||||
private async Task GenerateRandomAlias()
|
||||
{
|
||||
GlobalLoadingSpinner.Show();
|
||||
StateHasChanged();
|
||||
|
||||
// Store current values BEFORE generating, as the service might modify them
|
||||
string currentUsername = Obj.Username ?? string.Empty;
|
||||
string currentPassword = Obj.Password.Value ?? string.Empty;
|
||||
@@ -528,7 +525,6 @@ else
|
||||
LastGeneratedEmail = generatedObj.Alias.Email;
|
||||
}
|
||||
|
||||
GlobalLoadingSpinner.Hide();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
|
||||
@@ -71,11 +71,11 @@
|
||||
</data>
|
||||
<!-- Alias Settings Section -->
|
||||
<data name="AliasSettingsTitle">
|
||||
<value>Identity Generator Settings</value>
|
||||
<value>Henkilöllisyysgeneraattorin asetukset</value>
|
||||
<comment>Title for identity generator settings section</comment>
|
||||
</data>
|
||||
<data name="AliasGenerationLanguageLabel">
|
||||
<value>Language</value>
|
||||
<value>Kieli</value>
|
||||
<comment>Label for alias generation language setting</comment>
|
||||
</data>
|
||||
<data name="AliasGenerationLanguageDescription">
|
||||
@@ -83,7 +83,7 @@
|
||||
<comment>Description for alias generation language setting</comment>
|
||||
</data>
|
||||
<data name="AliasGenerationGenderLabel">
|
||||
<value>Gender</value>
|
||||
<value>Sukupuoli</value>
|
||||
<comment>Label for alias generation gender setting</comment>
|
||||
</data>
|
||||
<data name="AliasGenerationGenderDescription">
|
||||
@@ -103,7 +103,7 @@
|
||||
<comment>Female gender option</comment>
|
||||
</data>
|
||||
<data name="AliasGenerationAgeRangeLabel">
|
||||
<value>Age range</value>
|
||||
<value>Ikähaarukka</value>
|
||||
<comment>Label for alias generation age range setting</comment>
|
||||
</data>
|
||||
<data name="AliasGenerationAgeRangeDescription">
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
<comment>Generic error message</comment>
|
||||
</data>
|
||||
<data name="ErrorUnknown" xml:space="preserve">
|
||||
<value>An unknown error occurred. Please try again.</value>
|
||||
<value>Une erreur inconnue s'est produite. Merci de réessayer.</value>
|
||||
<comment>Generic unknown error message</comment>
|
||||
</data>
|
||||
<data name="ErrorValidation" xml:space="preserve">
|
||||
@@ -289,7 +289,7 @@
|
||||
</data>
|
||||
<!-- General UI text -->
|
||||
<data name="Or" xml:space="preserve">
|
||||
<value>or</value>
|
||||
<value>ou</value>
|
||||
<comment>Divider text between options</comment>
|
||||
</data>
|
||||
<data name="LockVault" xml:space="preserve">
|
||||
|
||||
@@ -36,6 +36,20 @@ declare class PasswordGenerator {
|
||||
private readonly uppercaseChars;
|
||||
private readonly numberChars;
|
||||
private readonly specialChars;
|
||||
/**
|
||||
* Ambiguous characters that look similar and are easy to confuse when typing:
|
||||
* - I, l, 1, | (pipe) - all look like vertical lines
|
||||
* - O, 0, o - all look like circles
|
||||
* - Z, 2 - similar appearance
|
||||
* - S, 5 - similar appearance
|
||||
* - B, 8 - similar appearance
|
||||
* - G, 6 - similar appearance
|
||||
* - Brackets, braces, parentheses: [], {}, ()
|
||||
* - Quotes: ', ", `
|
||||
* - Punctuation pairs: ;:, .,
|
||||
* - Dashes: -, _
|
||||
* - Angle brackets: <>
|
||||
*/
|
||||
private readonly ambiguousChars;
|
||||
private length;
|
||||
private useLowercase;
|
||||
|
||||
@@ -39,7 +39,21 @@ var PasswordGenerator = class {
|
||||
this.uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
this.numberChars = "0123456789";
|
||||
this.specialChars = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||||
this.ambiguousChars = "Il1O0o";
|
||||
/**
|
||||
* Ambiguous characters that look similar and are easy to confuse when typing:
|
||||
* - I, l, 1, | (pipe) - all look like vertical lines
|
||||
* - O, 0, o - all look like circles
|
||||
* - Z, 2 - similar appearance
|
||||
* - S, 5 - similar appearance
|
||||
* - B, 8 - similar appearance
|
||||
* - G, 6 - similar appearance
|
||||
* - Brackets, braces, parentheses: [], {}, ()
|
||||
* - Quotes: ', ", `
|
||||
* - Punctuation pairs: ;:, .,
|
||||
* - Dashes: -, _
|
||||
* - Angle brackets: <>
|
||||
*/
|
||||
this.ambiguousChars = "Il1O0oZzSsBbGg2568|[]{}()<>;:,.`'\"_-";
|
||||
this.length = 18;
|
||||
this.useLowercase = true;
|
||||
this.useUppercase = true;
|
||||
|
||||
@@ -13,7 +13,21 @@ var PasswordGenerator = class {
|
||||
this.uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
this.numberChars = "0123456789";
|
||||
this.specialChars = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||||
this.ambiguousChars = "Il1O0o";
|
||||
/**
|
||||
* Ambiguous characters that look similar and are easy to confuse when typing:
|
||||
* - I, l, 1, | (pipe) - all look like vertical lines
|
||||
* - O, 0, o - all look like circles
|
||||
* - Z, 2 - similar appearance
|
||||
* - S, 5 - similar appearance
|
||||
* - B, 8 - similar appearance
|
||||
* - G, 6 - similar appearance
|
||||
* - Brackets, braces, parentheses: [], {}, ()
|
||||
* - Quotes: ', ", `
|
||||
* - Punctuation pairs: ;:, .,
|
||||
* - Dashes: -, _
|
||||
* - Angle brackets: <>
|
||||
*/
|
||||
this.ambiguousChars = "Il1O0oZzSsBbGg2568|[]{}()<>;:,.`'\"_-";
|
||||
this.length = 18;
|
||||
this.useLowercase = true;
|
||||
this.useUppercase = true;
|
||||
|
||||
@@ -273,5 +273,12 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
|
||||
.WithMany(c => c.EncryptionKeys)
|
||||
.HasForeignKey(l => l.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Configure MobileLoginRequest - AliasVaultUser relationship
|
||||
modelBuilder.Entity<MobileLoginRequest>()
|
||||
.HasOne(m => m.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(m => m.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,985 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using AliasServerDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations
|
||||
{
|
||||
[DbContext(typeof(AliasServerDbContext))]
|
||||
[Migration("20251130152655_ConfigureMobileLoginRequestCascadeDelete")]
|
||||
partial class ConfigureMobileLoginRequestCascadeDelete
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.4")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true)
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AdminRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AdminRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AdminUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime?>("LastPasswordChanged")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AdminUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AliasVaultRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("Blocked")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("EmailsReceived")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("LastActivityDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("MaxEmailAgeDays")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("MaxEmails")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("PasswordChangedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AliasVaultUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DeviceIdentifier")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("ExpireDate")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("character varying(45)");
|
||||
|
||||
b.Property<string>("PreviousTokenValue")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AliasVaultUserRefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AuthLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AdditionalInfo")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Browser")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Country")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("DeviceType")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("EventType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("FailureReason")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<bool>("IsSuccess")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsSuspiciousActivity")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("OperatingSystem")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("RequestPath")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex(new[] { "EventType" }, "IX_EventType");
|
||||
|
||||
b.HasIndex(new[] { "IpAddress" }, "IX_IpAddress");
|
||||
|
||||
b.HasIndex(new[] { "Timestamp" }, "IX_Timestamp");
|
||||
|
||||
b.HasIndex(new[] { "Username", "IsSuccess", "Timestamp" }, "IX_Username_IsSuccess_Timestamp")
|
||||
.IsDescending(false, false, true);
|
||||
|
||||
b.HasIndex(new[] { "Username", "Timestamp" }, "IX_Username_Timestamp")
|
||||
.IsDescending(false, true);
|
||||
|
||||
b.ToTable("AuthLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("DateSystem")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("EncryptedSymmetricKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("From")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("FromDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("FromLocal")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MessageHtml")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MessagePlain")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MessagePreview")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MessageSource")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PushNotificationSent")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("To")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ToDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ToLocal")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserEncryptionKeyId")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Visible")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Date");
|
||||
|
||||
b.HasIndex("DateSystem");
|
||||
|
||||
b.HasIndex("PushNotificationSent");
|
||||
|
||||
b.HasIndex("ToLocal");
|
||||
|
||||
b.HasIndex("UserEncryptionKeyId");
|
||||
|
||||
b.HasIndex("Visible");
|
||||
|
||||
b.ToTable("Emails");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<byte[]>("Bytes")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("EmailId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Filesize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EmailId");
|
||||
|
||||
b.ToTable("EmailAttachments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Log", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Application")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Exception")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Level")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("LogEvent")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("LogEvent");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MessageTemplate")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SourceContext")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("TimeStamp")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Application");
|
||||
|
||||
b.HasIndex("TimeStamp");
|
||||
|
||||
b.ToTable("Logs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.MobileLoginRequest", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("ClearedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ClientIpAddress")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClientPublicKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("EncryptedDecryptionKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("FulfilledAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("MobileIpAddress")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("RetrievedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex(new[] { "ClientIpAddress" }, "IX_ClientIpAddress");
|
||||
|
||||
b.HasIndex(new[] { "CreatedAt" }, "IX_CreatedAt");
|
||||
|
||||
b.HasIndex(new[] { "MobileIpAddress" }, "IX_MobileIpAddress");
|
||||
|
||||
b.HasIndex(new[] { "RetrievedAt", "ClearedAt", "FulfilledAt" }, "IX_RetrievedAt_ClearedAt_FulfilledAt");
|
||||
|
||||
b.HasIndex(new[] { "UserId" }, "IX_UserId");
|
||||
|
||||
b.ToTable("MobileLoginRequests");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.ServerSetting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ServerSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.TaskRunnerJob", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<TimeOnly?>("EndTime")
|
||||
.HasColumnType("time without time zone");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsOnDemand")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime>("RunDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<TimeOnly>("StartTime")
|
||||
.HasColumnType("time without time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TaskRunnerJobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("AddressDomain")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("AddressLocal")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("Disabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Address")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId", "Disabled");
|
||||
|
||||
b.ToTable("UserEmailClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PublicKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserEncryptionKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("CredentialsCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("EmailClaimsCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("EncryptionSettings")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("EncryptionType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("FileSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long>("RevisionNumber")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Salt")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("VaultBlob")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Verifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Vaults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasVault.WorkerStatus.Database.WorkerServiceStatus", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("CurrentStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("DesiredStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime>("Heartbeat")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ServiceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("WorkerServiceStatuses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("FriendlyName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Xml")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("DataProtectionKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("RoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("UserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.ToTable("UserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.ToTable("UserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("UserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.UserEncryptionKey", "EncryptionKey")
|
||||
.WithMany("Emails")
|
||||
.HasForeignKey("UserEncryptionKeyId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("EncryptionKey");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.Email", "Email")
|
||||
.WithMany("Attachments")
|
||||
.HasForeignKey("EmailId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Email");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.MobileLoginRequest", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany("EmailClaims")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany("EncryptionKeys")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany("Vaults")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
|
||||
{
|
||||
b.Navigation("EmailClaims");
|
||||
|
||||
b.Navigation("EncryptionKeys");
|
||||
|
||||
b.Navigation("Vaults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.Navigation("Attachments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
|
||||
{
|
||||
b.Navigation("Emails");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ConfigureMobileLoginRequestCascadeDelete : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_MobileLoginRequests_AliasVaultUsers_UserId",
|
||||
table: "MobileLoginRequests");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_MobileLoginRequests_AliasVaultUsers_UserId",
|
||||
table: "MobileLoginRequests",
|
||||
column: "UserId",
|
||||
principalTable: "AliasVaultUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_MobileLoginRequests_AliasVaultUsers_UserId",
|
||||
table: "MobileLoginRequests");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_MobileLoginRequests_AliasVaultUsers_UserId",
|
||||
table: "MobileLoginRequests",
|
||||
column: "UserId",
|
||||
principalTable: "AliasVaultUsers",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -920,7 +920,8 @@ namespace AliasServerDb.Migrations
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId");
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ public static class AppInfo
|
||||
/// <summary>
|
||||
/// Gets the patch version number.
|
||||
/// </summary>
|
||||
public const int VersionPatch = 0;
|
||||
public const int VersionPatch = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version stage (e.g., "", "-alpha", "-beta", "-rc").
|
||||
|
||||
2
fastlane/metadata/android/en-US/changelogs/2501900.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/2501900.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Update QR code scanner
|
||||
- Improve password generator "non-ambigious characters" option
|
||||
2
fastlane/metadata/android/nl-NL/changelogs/2501900.txt
Normal file
2
fastlane/metadata/android/nl-NL/changelogs/2501900.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Update QR code scanner
|
||||
- Wachtwoordgenerator "onduidelijke tekens vermijden" optie verbeterd
|
||||
@@ -0,0 +1 @@
|
||||
- Improve password generator "non-ambigious characters" option
|
||||
@@ -0,0 +1 @@
|
||||
- Improve autofill performance
|
||||
2
fastlane/metadata/ios/en-US/changelogs/2501900.txt
Normal file
2
fastlane/metadata/ios/en-US/changelogs/2501900.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Update QR code scanner
|
||||
- Improve password generator "non-ambigious characters" option
|
||||
2
fastlane/metadata/ios/nl-NL/changelogs/2501900.txt
Normal file
2
fastlane/metadata/ios/nl-NL/changelogs/2501900.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Update QR code scanner
|
||||
- Wachtwoordgenerator "onduidelijke tekens vermijden" optie verbeterd
|
||||
130
install.sh
130
install.sh
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# @version 0.25.0
|
||||
# @version 0.25.2
|
||||
|
||||
# Repository information used for downloading files and images from GitHub
|
||||
REPO_OWNER="aliasvault"
|
||||
@@ -72,9 +72,10 @@ show_usage() {
|
||||
printf " configure-dev-db Enable/disable development database (for local development only)\n"
|
||||
printf "\n"
|
||||
printf "Options:\n"
|
||||
printf " --verbose Show detailed output\n"
|
||||
printf " -y, --yes Automatic yes to prompts\n"
|
||||
printf " --dev Target development database for db import/export operations"
|
||||
printf " --verbose Show detailed output\n"
|
||||
printf " -y, --yes Automatic yes to prompts\n"
|
||||
printf " --dev Target development database for db import/export operations\n"
|
||||
printf " --parallel=N Use pigz with N threads for faster compression (default: off, max: 32)\n"
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
@@ -98,6 +99,7 @@ parse_args() {
|
||||
FORCE_YES=false
|
||||
COMMAND_ARG=""
|
||||
DEV_DB=false
|
||||
PARALLEL_JOBS=0 # 0 = use standard gzip, >0 = use pigz with N threads
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
show_usage
|
||||
@@ -228,6 +230,14 @@ parse_args() {
|
||||
DEV_DB=true
|
||||
shift
|
||||
;;
|
||||
--parallel=*)
|
||||
PARALLEL_JOBS="${1#*=}"
|
||||
if ! [[ "$PARALLEL_JOBS" =~ ^[0-9]+$ ]] || [ "$PARALLEL_JOBS" -lt 1 ] || [ "$PARALLEL_JOBS" -gt 32 ]; then
|
||||
echo "Error: Invalid --parallel value '$PARALLEL_JOBS'. Must be a number between 1 and 32"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
show_usage
|
||||
@@ -2816,18 +2826,30 @@ handle_db_export() {
|
||||
|
||||
# Check if output redirection is present
|
||||
if [ -t 1 ]; then
|
||||
printf "Usage: ./install.sh db-export [--dev] > backup.sql.gz\n" >&2
|
||||
printf "Usage: ./install.sh db-export [OPTIONS] > backup.sql.gz\n" >&2
|
||||
printf "\n" >&2
|
||||
printf "Options:\n" >&2
|
||||
printf " --dev Export from development database\n" >&2
|
||||
printf " --dev Export from development database\n" >&2
|
||||
printf " --parallel=N Use pigz with N threads for parallel compression (max: 32)\n" >&2
|
||||
printf "\n" >&2
|
||||
printf "Examples:\n" >&2
|
||||
printf " ./install.sh db-export > my_backup_$(date +%Y%m%d).sql.gz\n" >&2
|
||||
printf " ./install.sh db-export --dev > my_dev_backup_$(date +%Y%m%d).sql.gz\n" >&2
|
||||
printf "Compression:\n" >&2
|
||||
printf " Default (no --parallel) Uses standard gzip (slowest, lowest CPU usage)\n" >&2
|
||||
printf " --parallel=X Uses pigz with X threads (~2x faster, good for production)\n" >&2
|
||||
printf "\n" >&2
|
||||
printf "Note: Parallel compression runs at lowest priority (nice/ionice) to minimize\n" >&2
|
||||
printf " impact on production.\n" >&2
|
||||
printf "\n" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create temporary file for export
|
||||
temp_export_file=$(mktemp)
|
||||
trap 'rm -f "$temp_export_file"' EXIT INT TERM
|
||||
|
||||
# Start timing
|
||||
export_start_time=$(date +%s)
|
||||
|
||||
# Determine docker compose command based on dev/prod
|
||||
if [ "$DEV_DB" = true ]; then
|
||||
# Check if dev containers are running
|
||||
if ! docker compose -f dockerfiles/docker-compose.dev.yml -p aliasvault-dev ps postgres-dev --quiet 2>/dev/null | grep -q .; then
|
||||
@@ -2841,8 +2863,8 @@ handle_db_export() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "${CYAN}> Exporting development database...${NC}\n" >&2
|
||||
docker compose -f dockerfiles/docker-compose.dev.yml -p aliasvault-dev exec postgres-dev pg_dump -U aliasvault aliasvault | gzip
|
||||
DOCKER_CMD="docker compose -f dockerfiles/docker-compose.dev.yml -p aliasvault-dev exec -T postgres-dev"
|
||||
DB_TYPE="development"
|
||||
else
|
||||
# Production database export logic
|
||||
if ! docker compose ps --quiet 2>/dev/null | grep -q .; then
|
||||
@@ -2855,12 +2877,67 @@ handle_db_export() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "${CYAN}> Exporting production database...${NC}\n" >&2
|
||||
docker compose exec postgres pg_dump -U aliasvault aliasvault | gzip
|
||||
DOCKER_CMD="docker compose exec -T postgres"
|
||||
DB_TYPE="production"
|
||||
fi
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
# Execute export based on parallel setting
|
||||
if [ "$PARALLEL_JOBS" -gt 0 ]; then
|
||||
printf "${CYAN}> Exporting ${DB_TYPE} database (with ${PARALLEL_JOBS}-thread parallel compression)...${NC}\n" >&2
|
||||
# Use pigz for parallel compression
|
||||
$DOCKER_CMD bash -c "
|
||||
# Install pigz if not available (for parallel gzip)
|
||||
if ! command -v pigz >/dev/null 2>&1; then
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
apk add --no-cache pigz >/dev/null 2>&1 || true
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update >/dev/null 2>&1 && apt-get install -y pigz >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Dump with pigz parallel compression (or fallback to gzip -1 if pigz install failed)
|
||||
# Use nice (lowest CPU priority) and ionice (lowest I/O priority) to minimize impact
|
||||
if command -v pigz >/dev/null 2>&1; then
|
||||
ionice -c 3 nice -n 19 pg_dump -U aliasvault aliasvault | ionice -c 3 nice -n 19 pigz -1 -p ${PARALLEL_JOBS} 2>/dev/null || \
|
||||
nice -n 19 pg_dump -U aliasvault aliasvault | nice -n 19 pigz -1 -p ${PARALLEL_JOBS}
|
||||
else
|
||||
ionice -c 3 nice -n 19 pg_dump -U aliasvault aliasvault | ionice -c 3 nice -n 19 gzip -1 2>/dev/null || \
|
||||
nice -n 19 pg_dump -U aliasvault aliasvault | nice -n 19 gzip -1
|
||||
fi
|
||||
" > "$temp_export_file" 2>/dev/null
|
||||
export_status=$?
|
||||
else
|
||||
# Default: standard gzip (backwards compatible)
|
||||
printf "${CYAN}> Exporting ${DB_TYPE} database (standard compression)...${NC}\n" >&2
|
||||
$DOCKER_CMD nice -n 19 pg_dump -U aliasvault aliasvault | gzip -1 > "$temp_export_file"
|
||||
export_status=$?
|
||||
fi
|
||||
|
||||
# End timing
|
||||
export_end_time=$(date +%s)
|
||||
export_duration=$((export_end_time - export_start_time))
|
||||
|
||||
# Get filesize
|
||||
if [ -f "$temp_export_file" ]; then
|
||||
export_filesize=$(wc -c < "$temp_export_file")
|
||||
export_filesize_mb=$(awk "BEGIN {printf \"%.2f\", $export_filesize/1024/1024}")
|
||||
fi
|
||||
|
||||
if [ $export_status -eq 0 ]; then
|
||||
# Output the file to stdout
|
||||
cat "$temp_export_file"
|
||||
|
||||
printf "${GREEN}> Database exported successfully.${NC}\n" >&2
|
||||
printf "${CYAN}> Export format: SQL (.sql.gz)${NC}\n" >&2
|
||||
if [ "$PARALLEL_JOBS" -gt 0 ]; then
|
||||
printf "${CYAN}> Compression: pigz with ${PARALLEL_JOBS} threads${NC}\n" >&2
|
||||
else
|
||||
printf "${CYAN}> Compression: gzip (standard)${NC}\n" >&2
|
||||
fi
|
||||
printf "${CYAN}> Export duration: ${export_duration}s${NC}\n" >&2
|
||||
if [ -n "$export_filesize_mb" ]; then
|
||||
printf "${CYAN}> Export filesize: ${export_filesize_mb} MB (compressed)${NC}\n" >&2
|
||||
fi
|
||||
else
|
||||
printf "${RED}> Failed to export database.${NC}\n" >&2
|
||||
exit 1
|
||||
@@ -2892,9 +2969,9 @@ handle_db_import() {
|
||||
printf " --dev Import to development database\n"
|
||||
printf "\n"
|
||||
printf "Examples:\n"
|
||||
printf " ./install.sh db-import < backup.sql.gz # Import gzipped backup\n"
|
||||
printf " ./install.sh db-import < backup.sql # Import plain SQL backup\n"
|
||||
printf " ./install.sh db-import --dev < backup.sql # Import to dev database\n"
|
||||
printf " ./install.sh db-import < backup.sql.gz # Import gzipped SQL (standard)\n"
|
||||
printf " ./install.sh db-import < backup.sql # Import plain SQL\n"
|
||||
printf " ./install.sh db-import --dev < backup.sql.gz # Import to dev database\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -2951,8 +3028,16 @@ handle_db_import() {
|
||||
cat <&3 > "$temp_file" # Read from fd 3 instead of stdin
|
||||
exec 3<&- # Close fd 3
|
||||
|
||||
# Detect if the file is gzipped or plain SQL
|
||||
# Get input filesize
|
||||
if [ -f "$temp_file" ]; then
|
||||
import_filesize=$(wc -c < "$temp_file")
|
||||
import_filesize_mb=$(awk "BEGIN {printf \"%.2f\", $import_filesize/1024/1024}")
|
||||
printf "${CYAN}> Input file size: ${import_filesize_mb} MB${NC}\n"
|
||||
fi
|
||||
|
||||
# Detect file format
|
||||
is_gzipped=false
|
||||
|
||||
if gzip -t "$temp_file" 2>/dev/null; then
|
||||
is_gzipped=true
|
||||
printf "${CYAN}> Detected gzipped SQL backup${NC}\n"
|
||||
@@ -2966,6 +3051,9 @@ handle_db_import() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Start timing
|
||||
import_start_time=$(date +%s)
|
||||
|
||||
if [ "$DEV_DB" = true ]; then
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
docker compose -f dockerfiles/docker-compose.dev.yml -p aliasvault-dev exec -T postgres-dev psql -U aliasvault postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'aliasvault' AND pid <> pg_backend_pid();" && \
|
||||
@@ -3009,10 +3097,16 @@ handle_db_import() {
|
||||
fi
|
||||
|
||||
import_status=$?
|
||||
|
||||
# End timing
|
||||
import_end_time=$(date +%s)
|
||||
import_duration=$((import_end_time - import_start_time))
|
||||
|
||||
rm "$temp_file"
|
||||
|
||||
if [ $import_status -eq 0 ]; then
|
||||
printf "${GREEN}> Database imported successfully.${NC}\n"
|
||||
printf "${CYAN}> Import duration: ${import_duration}s${NC}\n"
|
||||
if [ "$DEV_DB" != true ]; then
|
||||
printf "${CYAN}> Starting services...${NC}\n"
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
|
||||
@@ -103,4 +103,69 @@ describe('PasswordGenerator', () => {
|
||||
|
||||
expect(result).toBe(generator);
|
||||
});
|
||||
|
||||
it('removes ambiguous characters when useNonAmbiguousCharacters is enabled', () => {
|
||||
/**
|
||||
* Generate 50 passwords to ensure statistical confidence
|
||||
* Check that none of the ambiguous characters appear:
|
||||
* - I, l, 1, | - vertical line lookalikes
|
||||
* - O, 0, o - circle lookalikes
|
||||
* - Z, z, 2 - similar appearance
|
||||
* - S, s, 5 - similar appearance
|
||||
* - B, b, 8 - similar appearance
|
||||
* - G, g, 6 - similar appearance
|
||||
* - Brackets, braces, parentheses
|
||||
* - Quotes
|
||||
* - Punctuation pairs
|
||||
* - Dashes
|
||||
*/
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const password = generator
|
||||
.setLength(32)
|
||||
.useNonAmbiguousCharacters(true)
|
||||
.generateRandomPassword();
|
||||
|
||||
expect(password).not.toMatch(/[Il1|]/);
|
||||
expect(password).not.toMatch(/[O0o]/);
|
||||
expect(password).not.toMatch(/[Zz2]/);
|
||||
expect(password).not.toMatch(/[Ss5]/);
|
||||
expect(password).not.toMatch(/[Bb8]/);
|
||||
expect(password).not.toMatch(/[Gg6]/);
|
||||
expect(password).not.toMatch(/[\\[\\]{}()<>]/);
|
||||
expect(password).not.toMatch(/['"`]/);
|
||||
expect(password).not.toMatch(/[;:,.]/);
|
||||
expect(password).not.toMatch(/[_-]/);
|
||||
}
|
||||
});
|
||||
|
||||
it('still generates valid passwords with non-ambiguous characters enabled', () => {
|
||||
const password = generator
|
||||
.setLength(20)
|
||||
.useNonAmbiguousCharacters(true)
|
||||
.generateRandomPassword();
|
||||
|
||||
expect(password.length).toBe(20);
|
||||
// Should still contain allowed characters
|
||||
expect(password.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('includes ambiguous characters when useNonAmbiguousCharacters is disabled', () => {
|
||||
// Generate multiple passwords and check if at least one contains ambiguous characters
|
||||
let foundAmbiguous = false;
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const password = generator
|
||||
.setLength(32)
|
||||
.useNonAmbiguousCharacters(false)
|
||||
.generateRandomPassword();
|
||||
|
||||
// Check if any ambiguous characters are present
|
||||
if (/[Il1O0oZzSsBbGg2568|[\]{}()<>;:,.`'"_-]/.test(password)) {
|
||||
foundAmbiguous = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(foundAmbiguous).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,21 @@ export class PasswordGenerator {
|
||||
private readonly uppercaseChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
private readonly numberChars = '0123456789';
|
||||
private readonly specialChars = '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||
private readonly ambiguousChars = 'Il1O0o';
|
||||
/**
|
||||
* Ambiguous characters that look similar and are easy to confuse when typing:
|
||||
* - I, l, 1, | (pipe) - all look like vertical lines
|
||||
* - O, 0, o - all look like circles
|
||||
* - Z, 2 - similar appearance
|
||||
* - S, 5 - similar appearance
|
||||
* - B, 8 - similar appearance
|
||||
* - G, 6 - similar appearance
|
||||
* - Brackets, braces, parentheses: [], {}, ()
|
||||
* - Quotes: ', ", `
|
||||
* - Punctuation pairs: ;:, .,
|
||||
* - Dashes: -, _
|
||||
* - Angle brackets: <>
|
||||
*/
|
||||
private readonly ambiguousChars = 'Il1O0oZzSsBbGg2568|[]{}()<>;:,.`\'"_-';
|
||||
|
||||
private length: number = 18;
|
||||
private useLowercase: boolean = true;
|
||||
|
||||
Reference in New Issue
Block a user