Compare commits

...

31 Commits

Author SHA1 Message Date
Leendert de Borst
0a70902d69 Bump version to 0.25.1 for mobile app (unaffected by 0.25.2 release) 2025-11-30 17:47:58 +01:00
Leendert de Borst
eee41df9a4 Bump version to 0.25.2 2025-11-30 17:30:32 +01:00
Leendert de Borst
d563d6d448 Improve browser extension vault cache (#1413) 2025-11-30 17:26:23 +01:00
Leendert de Borst
db1474397c Add cascade delete to MobileLoginRequests (#1415) 2025-11-30 15:38:49 +00:00
Leendert de Borst
e881f9486a Add parallel support to db-export command (#1415) 2025-11-30 15:12:55 +00:00
Leendert de Borst
645fd605e6 Update PasswordGenerator.test.ts (#1413) 2025-11-30 12:08:22 +00:00
Leendert de Borst
254f0a1212 Improve browser extension autofill suggestion performance (#1413) 2025-11-30 12:08:22 +00:00
Leendert de Borst
64d29ebcd4 Update admin users list to show correct amount of email claims (#1411) 2025-11-30 11:17:16 +00:00
Leendert de Borst
df0d74595f Bump version to 0.26.0-alpha 2025-11-28 20:16:39 +01:00
Leendert de Borst
2131e4922c Merge branch 'main' of https://github.com/aliasvault/aliasvault
* 'main' of https://github.com/aliasvault/aliasvault:
  New Crowdin updates (#1397)
  Tweak native QR code scanner to only react on AliasVault prefixes (#1405)
  Add native iOS QR code scanner (#1405)
  Update net.aliasvault.app.yml.template (#1405)
  Add native Android QR code scanner ZXing implementation (#1405)
  Update run.sh to generate net.aliasvault.app.yml with latest version and branch for proper F-Droid build (#1405)
  Update package.json (#1405)
  Update F-Droid local build scripts (#1405)
  Replace expo-camera which uses non-FOSS libs with react-native-vision-camera (#1405)
  Add expo-camera to scanignore to prevent it being deleted by F-Droid (#1405)
  Add sign-apk.sh helper script (#1405)
  Update F-Droid local build flow to capture APK outputs (#1405)
2025-11-28 18:50:54 +01:00
Leendert de Borst
d846825b84 Update FormFiller logic to improve browser extension autofill reliability 2025-11-28 18:50:40 +01:00
Leendert de Borst
2a902eeb97 Bump version to 0.25.1 2025-11-28 18:37:14 +01:00
Leendert de Borst
d9a6dfab03 New Crowdin updates (#1397)
* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations en.json (French)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]
2025-11-28 18:26:11 +01:00
Leendert de Borst
3da99ed4b1 Tweak native QR code scanner to only react on AliasVault prefixes (#1405) 2025-11-28 17:23:11 +00:00
Leendert de Borst
5414f40c98 Add native iOS QR code scanner (#1405) 2025-11-28 17:23:11 +00:00
Leendert de Borst
6c561e8ece Update net.aliasvault.app.yml.template (#1405) 2025-11-28 17:23:11 +00:00
Leendert de Borst
3654b12cd7 Add native Android QR code scanner ZXing implementation (#1405) 2025-11-28 17:23:11 +00:00
Leendert de Borst
266e7b36d4 Update run.sh to generate net.aliasvault.app.yml with latest version and branch for proper F-Droid build (#1405) 2025-11-28 17:23:11 +00:00
Leendert de Borst
cbe9978367 Update package.json (#1405) 2025-11-28 17:23:11 +00:00
Leendert de Borst
6b949bcb2f Update F-Droid local build scripts (#1405) 2025-11-28 17:23:11 +00:00
Leendert de Borst
6a4fbb9193 Replace expo-camera which uses non-FOSS libs with react-native-vision-camera (#1405) 2025-11-28 17:23:11 +00:00
Leendert de Borst
c459a48927 Add expo-camera to scanignore to prevent it being deleted by F-Droid (#1405) 2025-11-28 17:23:11 +00:00
Leendert de Borst
d3f132df63 Add sign-apk.sh helper script (#1405) 2025-11-28 17:23:11 +00:00
Leendert de Borst
b5edc6ef76 Update F-Droid local build flow to capture APK outputs (#1405) 2025-11-28 17:23:11 +00:00
Leendert de Borst
4e0db87bc3 Update password generator with non-ambigious char improvement (#1398) 2025-11-27 10:10:24 +01:00
Leendert de Borst
62cc0e7c2b Improve password generator non-ambigious chars option (#1398) 2025-11-27 09:08:23 +00:00
Leendert de Borst
dad3a6fa2c Make AuthController.cs more robust and do not log invalid tokens as server errors (#1408) 2025-11-27 09:08:07 +00:00
dependabot[bot]
9560d550e4 Bump the npm_and_yarn group across 2 directories with 1 update
Bumps the npm_and_yarn group with 1 update in the /apps/browser-extension directory: [node-forge](https://github.com/digitalbazaar/forge).
Bumps the npm_and_yarn group with 1 update in the /apps/mobile-app directory: [node-forge](https://github.com/digitalbazaar/forge).


Updates `node-forge` from 1.3.1 to 1.3.2
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.3.1...v1.3.2)

Updates `node-forge` from 1.3.1 to 1.3.2
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.3.1...v1.3.2)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-version: 1.3.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: node-forge
  dependency-version: 1.3.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-27 06:50:16 +00:00
Leendert de Borst
0930ae03cd Remove loading animation from web app generate random alias button (#1402) 2025-11-26 22:37:44 +00:00
Leendert de Borst
23c9bf2fc9 Fix related users navigation refresh in admin (#1400) 2025-11-26 10:20:12 +00:00
Leendert de Borst
6ebaf8e1b8 Bump working version to 0.26.0-alpha 2025-11-26 11:11:52 +01:00
78 changed files with 2615 additions and 572 deletions

View File

@@ -1 +1 @@
0
2

View File

@@ -1 +1 @@
0.25.0
0.25.2

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,

View File

@@ -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());

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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);
}
/**

View File

@@ -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';

View File

@@ -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';

View File

@@ -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";

View File

@@ -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";

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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).

View File

@@ -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[];

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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';"
},

View File

@@ -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'

View File

@@ -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"/>

View File

@@ -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)
}
}
}
}

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -1,2 +1,3 @@
fdroiddata
fdroidserver
net.aliasvault.app.yml

View File

@@ -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

View File

@@ -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/

View File

@@ -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!"

View File

@@ -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/

View 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}"

View File

@@ -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

View File

@@ -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",

View File

@@ -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>
);

View File

@@ -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",

View File

@@ -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.",

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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;

View File

@@ -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

View File

@@ -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?,

View File

@@ -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

View 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)
}
}
}

View File

@@ -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"

View File

@@ -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": {

View File

@@ -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');

View File

@@ -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).

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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; }
}

View File

@@ -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;

View File

@@ -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.

View File

@@ -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();
}

View File

@@ -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">

View File

@@ -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">

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -920,7 +920,8 @@ namespace AliasServerDb.Migrations
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany()
.HasForeignKey("UserId");
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("User");
});

View File

@@ -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").

View File

@@ -0,0 +1,2 @@
- Update QR code scanner
- Improve password generator "non-ambigious characters" option

View File

@@ -0,0 +1,2 @@
- Update QR code scanner
- Wachtwoordgenerator "onduidelijke tekens vermijden" optie verbeterd

View File

@@ -0,0 +1 @@
- Improve password generator "non-ambigious characters" option

View File

@@ -0,0 +1 @@
- Improve autofill performance

View File

@@ -0,0 +1,2 @@
- Update QR code scanner
- Improve password generator "non-ambigious characters" option

View File

@@ -0,0 +1,2 @@
- Update QR code scanner
- Wachtwoordgenerator "onduidelijke tekens vermijden" optie verbeterd

View File

@@ -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

View File

@@ -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);
});
});

View File

@@ -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;