mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-30 01:27:52 -05:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab82a63a0a | ||
|
|
82376b696c | ||
|
|
0c8fc191a6 | ||
|
|
b71f0dd2c3 | ||
|
|
3617c551e3 | ||
|
|
901caa896b | ||
|
|
89534bf78e | ||
|
|
e82595162f | ||
|
|
93c439e852 | ||
|
|
ff08fae579 | ||
|
|
5fdcee50d5 | ||
|
|
8526172ec7 | ||
|
|
5156988319 | ||
|
|
18d92ecced | ||
|
|
0a0bec99b1 | ||
|
|
791f8a758b | ||
|
|
3f11e29787 | ||
|
|
046d09453a | ||
|
|
1d77d05e7c | ||
|
|
22d2e09982 | ||
|
|
8b835a4a77 | ||
|
|
a435305093 | ||
|
|
e4f3de927f | ||
|
|
1d5c288514 | ||
|
|
5d3ad60dee | ||
|
|
c5244b31ec | ||
|
|
a6c7c54592 | ||
|
|
bf46c155bd | ||
|
|
d4e5b724ff | ||
|
|
e51219d513 | ||
|
|
800f015947 | ||
|
|
5f3c36263d | ||
|
|
4617d5efc4 | ||
|
|
1401982e2c | ||
|
|
ebdbf41208 | ||
|
|
ed4b82e125 | ||
|
|
1976255e98 | ||
|
|
e817326162 | ||
|
|
9d2a397317 | ||
|
|
8f42ebdfa4 | ||
|
|
3aab43b17a | ||
|
|
6e922237c0 | ||
|
|
ebac252162 | ||
|
|
9df76ffb43 |
@@ -7,6 +7,10 @@ on:
|
||||
branches: [ "main" ]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-chrome-extension:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
4
.github/workflows/docker-compose-build.yml
vendored
4
.github/workflows/docker-compose-build.yml
vendored
@@ -6,6 +6,10 @@ on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
4
.github/workflows/docker-compose-pull.yml
vendored
4
.github/workflows/docker-compose-pull.yml
vendored
@@ -7,6 +7,10 @@ on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
4
.github/workflows/dotnet-e2e-admin-tests.yml
vendored
4
.github/workflows/dotnet-e2e-admin-tests.yml
vendored
@@ -7,6 +7,10 @@ on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
admin-tests:
|
||||
timeout-minutes: 60
|
||||
|
||||
@@ -7,6 +7,10 @@ on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
client-tests:
|
||||
timeout-minutes: 60
|
||||
|
||||
@@ -7,6 +7,10 @@ on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
|
||||
4
.github/workflows/dotnet-unit-tests.yml
vendored
4
.github/workflows/dotnet-unit-tests.yml
vendored
@@ -7,6 +7,10 @@ on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -9,6 +9,7 @@ on:
|
||||
- main
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and analyze
|
||||
|
||||
102
browser-extension/package-lock.json
generated
102
browser-extension/package-lock.json
generated
@@ -16,7 +16,7 @@
|
||||
"otpauth": "^9.3.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.4",
|
||||
"react-router-dom": "^7.5.2",
|
||||
"secure-remote-password": "github:LinusU/secure-remote-password#73e5f732b6ca0cdbdc19da1a0c5f48cdbad2cbf0",
|
||||
"sql.js": "^1.12.0",
|
||||
"vitest": "^3.0.8",
|
||||
@@ -1973,12 +1973,6 @@
|
||||
"@types/har-format": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/emscripten": {
|
||||
"version": "1.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.40.0.tgz",
|
||||
@@ -10292,12 +10286,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.2.0.tgz",
|
||||
"integrity": "sha512-fXyqzPgCPZbqhrk7k3hPcCpYIlQ2ugIXDboHUzhJISFVy2DEPsmHgN588MyGmkIOv3jDgNfUE3kJi83L28s/LQ==",
|
||||
"version": "7.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.2.tgz",
|
||||
"integrity": "sha512-9Rw8r199klMnlGZ8VAsV/I8WrIF6IyJ90JQUdboupx1cdkgYqwnrYjH+I/nY/7cA1X5zia4mDJqH36npP7sxGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.6.0",
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"turbo-stream": "2.4.0"
|
||||
@@ -10316,12 +10309,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.2.0.tgz",
|
||||
"integrity": "sha512-cU7lTxETGtQRQbafJubvZKHEn5izNABxZhBY0Jlzdv0gqQhCPQt2J8aN5ZPjS6mQOXn5NnirWNh+FpE8TTYN0Q==",
|
||||
"version": "7.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.2.tgz",
|
||||
"integrity": "sha512-yk1XW8Fj7gK7flpYBXF3yzd2NbX6P7Kxjvs2b5nu1M04rb5pg/Zc4fGdBNTeT4eDYL2bvzWNyKaIMJX/RKHTTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.2.0"
|
||||
"react-router": "7.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -11898,6 +11891,48 @@
|
||||
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
||||
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/fdir": {
|
||||
"version": "6.4.4",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tinypool": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
|
||||
@@ -12449,14 +12484,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz",
|
||||
"integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==",
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
|
||||
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2",
|
||||
"postcss": "^8.5.3",
|
||||
"rollup": "^4.30.1"
|
||||
"rollup": "^4.34.9",
|
||||
"tinyglobby": "^0.2.13"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@@ -12599,6 +12637,32 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.4.4",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.8.tgz",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"otpauth": "^9.3.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.4",
|
||||
"react-router-dom": "^7.5.2",
|
||||
"secure-remote-password": "github:LinusU/secure-remote-password#73e5f732b6ca0cdbdc19da1a0c5f48cdbad2cbf0",
|
||||
"sql.js": "^1.12.0",
|
||||
"vitest": "^3.0.8",
|
||||
|
||||
@@ -515,7 +515,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
CURRENT_PROJECT_VERSION = 15;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -530,7 +530,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.16.0;
|
||||
MARKETING_VERSION = 0.16.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -554,7 +554,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
CURRENT_PROJECT_VERSION = 15;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -569,7 +569,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.16.0;
|
||||
MARKETING_VERSION = 0.16.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import './contentScript/style.css';
|
||||
import { FormDetector } from '../utils/formDetector/FormDetector';
|
||||
import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup } from './contentScript/Popup';
|
||||
import { injectIcon, popupDebounceTimeHasPassed } from './contentScript/Form';
|
||||
import { injectIcon, popupDebounceTimeHasPassed, validateInputField } from './contentScript/Form';
|
||||
import { onMessage } from "webext-bridge/content-script";
|
||||
import { BoolResponse as messageBoolResponse } from '../utils/types/messaging/BoolResponse';
|
||||
import { defineContentScript } from 'wxt/sandbox';
|
||||
@@ -25,7 +25,9 @@ export default defineContentScript({
|
||||
// Create a shadow root UI for isolation
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'aliasvault-ui',
|
||||
position: 'inline',
|
||||
position: 'overlay',
|
||||
alignment: 'top-left',
|
||||
zIndex: 2147483646,
|
||||
anchor: 'html',
|
||||
/**
|
||||
* Handle mount.
|
||||
@@ -40,25 +42,23 @@ export default defineContentScript({
|
||||
}
|
||||
|
||||
// Check if element itself, html or body has av-disable attribute like av-disable="true"
|
||||
const avDisable = (e.target as HTMLElement).getAttribute('av-disable') ?? document.body?.getAttribute('av-disable') ?? document.documentElement.getAttribute('av-disable');
|
||||
if (avDisable === 'true') {
|
||||
const avDisable = ((e.target as HTMLElement).getAttribute('av-disable') ?? document.body?.getAttribute('av-disable') ?? document.documentElement.getAttribute('av-disable')) === 'true';
|
||||
if (avDisable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e.target as HTMLInputElement;
|
||||
const textInputTypes = ['text', 'email', 'tel', 'password', 'search', 'url'];
|
||||
|
||||
if (target.tagName === 'INPUT' && textInputTypes.includes(target.type) && !target.dataset.aliasvaultIgnore) {
|
||||
const formDetector = new FormDetector(document, target);
|
||||
const { isValid, inputElement } = validateInputField(e.target as Element);
|
||||
if (isValid && inputElement) {
|
||||
const formDetector = new FormDetector(document, inputElement);
|
||||
if (!formDetector.containsLoginForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
injectIcon(target, container);
|
||||
injectIcon(inputElement, container);
|
||||
|
||||
// Only show popup if its enabled and debounce time has passed.
|
||||
if (await isAutoShowPopupEnabled() && popupDebounceTimeHasPassed()) {
|
||||
openAutofillPopup(target, container);
|
||||
openAutofillPopup(inputElement, container);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -85,19 +85,19 @@ export default defineContentScript({
|
||||
}
|
||||
|
||||
const target = document.getElementById(elementIdentifier) ?? document.getElementsByName(elementIdentifier)[0];
|
||||
const { isValid, inputElement } = validateInputField(target);
|
||||
|
||||
if (!(target instanceof HTMLInputElement)) {
|
||||
return { success: false, error: 'Target element is not an input field' };
|
||||
if (!isValid || !inputElement) {
|
||||
return { success: false, error: 'Target element is not a supported input field' };
|
||||
}
|
||||
|
||||
const formDetector = new FormDetector(document, target);
|
||||
|
||||
const formDetector = new FormDetector(document, inputElement);
|
||||
if (!formDetector.containsLoginForm()) {
|
||||
return { success: false, error: 'No form found' };
|
||||
}
|
||||
|
||||
injectIcon(target, container);
|
||||
openAutofillPopup(target, container);
|
||||
injectIcon(inputElement, container);
|
||||
openAutofillPopup(inputElement, container);
|
||||
return { success: true };
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,63 +1,108 @@
|
||||
import { CombinedStopWords } from "@/utils/formDetector/FieldPatterns";
|
||||
import { Credential } from "../../utils/types/Credential";
|
||||
|
||||
type CredentialWithPriority = Credential & {
|
||||
priority: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter credentials based on current URL and page context to determine which credentials to show
|
||||
* in the autofill popup.
|
||||
* in the autofill popup. Credentials are sorted by priority:
|
||||
* 1. Exact URL match (highest priority)
|
||||
* 2. Base URL match AND page title word match
|
||||
* 3. Base URL match only
|
||||
* 4. Page title word match only (lowest priority)
|
||||
*/
|
||||
export function filterCredentials(credentials: Credential[], currentUrl: string, pageTitle: string): Credential[] {
|
||||
const urlObject = new URL(currentUrl);
|
||||
const baseUrl = `${urlObject.protocol}//${urlObject.hostname}`;
|
||||
const filtered: CredentialWithPriority[] = [];
|
||||
|
||||
// 1. Exact URL match
|
||||
let filtered = credentials.filter(cred =>
|
||||
cred.ServiceUrl?.toLowerCase() === currentUrl.toLowerCase()
|
||||
);
|
||||
const sanitizedCurrentUrl = currentUrl.toLowerCase().replace('www.', '');
|
||||
|
||||
// 2. Base URL match with fuzzy domain comparison if no exact matches
|
||||
filtered = filtered.concat(credentials.filter(cred => {
|
||||
if (!cred.ServiceUrl) {
|
||||
return false;
|
||||
// 1. Exact URL match (priority 1)
|
||||
credentials.forEach(cred => {
|
||||
if (!cred.ServiceUrl || cred.ServiceUrl.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedCredUrl = cred.ServiceUrl.toLowerCase().replace('www.', '');
|
||||
|
||||
if (sanitizedCurrentUrl.startsWith(sanitizedCredUrl)) {
|
||||
filtered.push({ ...cred, priority: 1 });
|
||||
}
|
||||
});
|
||||
|
||||
// If we have one or more exact matches, do not continue to other matches
|
||||
if (filtered.length > 0) {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// Prepare page title words for matching
|
||||
const titleWords = pageTitle.length > 0
|
||||
? pageTitle.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(word =>
|
||||
word.length > 3 &&
|
||||
!CombinedStopWords.has(word.toLowerCase())
|
||||
)
|
||||
: [];
|
||||
|
||||
// Check for base URL matches and page title matches
|
||||
credentials.forEach(cred => {
|
||||
if (!cred.ServiceUrl || filtered.some(f => f.Id === cred.Id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hasBaseUrlMatch = false;
|
||||
let hasTitleMatch = false;
|
||||
|
||||
// Check base URL match
|
||||
try {
|
||||
const credUrlObject = new URL(cred.ServiceUrl);
|
||||
const currentUrlObject = new URL(baseUrl);
|
||||
|
||||
// Extract root domains by splitting on dots and taking last two parts
|
||||
const credDomainParts = credUrlObject.hostname.toLowerCase().split('.');
|
||||
const currentDomainParts = currentUrlObject.hostname.toLowerCase().split('.');
|
||||
|
||||
// Get root domain (last two parts, e.g., 'aliasvault.net')
|
||||
const credRootDomain = credDomainParts.slice(-2).join('.');
|
||||
const currentRootDomain = currentDomainParts.slice(-2).join('.');
|
||||
|
||||
// Compare protocols and root domains
|
||||
return credUrlObject.protocol === currentUrlObject.protocol &&
|
||||
credRootDomain === currentRootDomain;
|
||||
if (credUrlObject.protocol === currentUrlObject.protocol &&
|
||||
credRootDomain === currentRootDomain) {
|
||||
hasBaseUrlMatch = true;
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
// Invalid URL, skip
|
||||
}
|
||||
}));
|
||||
|
||||
// 3. Page title word match if still no matches
|
||||
if (filtered.length === 0 && pageTitle.length > 0) {
|
||||
const titleWords = pageTitle.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(word =>
|
||||
word.length > 3 && // Filter out words shorter than 4 characters
|
||||
!CombinedStopWords.has(word.toLowerCase()) // Filter out generic words
|
||||
// Check page title match
|
||||
if (titleWords.length > 0) {
|
||||
const credNameWords = cred.ServiceName.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(word => word.length > 3 && !CombinedStopWords.has(word));
|
||||
hasTitleMatch = titleWords.some(word =>
|
||||
credNameWords.some(credWord => credWord.includes(word))
|
||||
);
|
||||
}
|
||||
|
||||
filtered = credentials.filter(cred =>
|
||||
titleWords.some(word =>
|
||||
cred.ServiceName.toLowerCase().includes(word)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure we have unique credentials
|
||||
const uniqueCredentials = Array.from(new Map(filtered.map(cred => [cred.Id, cred])).values());
|
||||
// Assign priority based on matches
|
||||
if (hasBaseUrlMatch && hasTitleMatch) {
|
||||
filtered.push({ ...cred, priority: 2 });
|
||||
} else if (hasBaseUrlMatch) {
|
||||
filtered.push({ ...cred, priority: 3 });
|
||||
} else if (hasTitleMatch) {
|
||||
filtered.push({ ...cred, priority: 4 });
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by priority and then take unique credentials
|
||||
const uniqueCredentials = Array.from(
|
||||
new Map(filtered
|
||||
.sort((a, b) => a.priority - b.priority)
|
||||
.map(cred => [cred.Id, cred]))
|
||||
.values()
|
||||
);
|
||||
// Show max 3 results
|
||||
return uniqueCredentials.slice(0, 3);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,34 @@ export function hidePopupFor(ms: number) : void {
|
||||
popupDebounceTime = Date.now() + ms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if an element is a supported input field that can be processed for autofill.
|
||||
* @param element The element to validate
|
||||
* @returns An object containing validation result and the element cast as HTMLInputElement if valid
|
||||
*/
|
||||
export function validateInputField(element: Element | null): { isValid: boolean; inputElement?: HTMLInputElement } {
|
||||
if (!element) {
|
||||
return { isValid: false };
|
||||
}
|
||||
|
||||
const textInputTypes = ['text', 'email', 'tel', 'password', 'search', 'url', 'number'];
|
||||
const elementType = element.getAttribute('type');
|
||||
const isInputElement = element.tagName.toLowerCase() === 'input';
|
||||
|
||||
// Check if it's a valid input field we should process
|
||||
const isValid = (
|
||||
// Case 1: It's an input element (with either explicit type or defaulting to "text")
|
||||
(isInputElement && (!elementType || textInputTypes.includes(elementType?.toLowerCase() ?? ''))) ||
|
||||
// Case 2: Non-input element but has valid type attribute
|
||||
(!isInputElement && elementType && textInputTypes.includes(elementType.toLowerCase()))
|
||||
) as boolean;
|
||||
|
||||
return {
|
||||
isValid,
|
||||
inputElement: isValid ? (element as HTMLInputElement) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill credential into current form.
|
||||
*
|
||||
@@ -51,10 +79,44 @@ export function fillCredential(credential: Credential, input: HTMLInputElement)
|
||||
formFiller.fillFields(credential);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the actual visible input element, either the element itself or a child input.
|
||||
* Certain websites use custom input element wrappers that not only contain the input but
|
||||
* also other elements like labels, icons, etc. As we want to position the icon relative to the actual
|
||||
* input, we try to find the actual input element. If there is no actual input element, we fallback
|
||||
* to the provided element.
|
||||
*
|
||||
* This method is optional, but it improves the AliasVault icon positioning on certain websites.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
* @returns The actual input element to use for positioning.
|
||||
*/
|
||||
function findActualInput(element: HTMLElement): HTMLInputElement {
|
||||
// If it's already an input, return it
|
||||
if (element.tagName.toLowerCase() === 'input') {
|
||||
return element as HTMLInputElement;
|
||||
}
|
||||
|
||||
// Try to find a visible child input
|
||||
const childInput = element.querySelector('input');
|
||||
if (childInput) {
|
||||
const style = window.getComputedStyle(childInput);
|
||||
if (style.display !== 'none' && style.visibility !== 'hidden') {
|
||||
return childInput;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to the provided element if no child input found
|
||||
return element as HTMLInputElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject icon for a focused input element
|
||||
*/
|
||||
export function injectIcon(input: HTMLInputElement, container: HTMLElement): void {
|
||||
// Find the actual input element to use for positioning
|
||||
const actualInput = findActualInput(input);
|
||||
|
||||
const aliasvaultIconSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg enable-background="new 0 0 500 500" version="1.1" viewBox="0 0 500 500" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m459.87 294.95c0.016205 5.4005 0.03241 10.801-0.35022 16.873-1.111 6.3392-1.1941 12.173-2.6351 17.649-10.922 41.508-36.731 69.481-77.351 83.408-7.2157 2.4739-14.972 3.3702-22.479 4.995-23.629 0.042205-47.257 0.11453-70.886 0.12027-46.762 0.011322-93.523-0.01416-140.95-0.43411-8.59-2.0024-16.766-2.8352-24.398-5.3326-21.595-7.0666-39.523-19.656-53.708-37.552-10.227-12.903-17.579-27.17-21.28-43.221-1.475-6.3967-2.4711-12.904-3.6852-19.361-0.051849-5.747-0.1037-11.494 0.26915-17.886 4.159-42.973 27.68-71.638 63.562-92.153 0-0.70761-0.001961-1.6988 3.12e-4 -2.69 0.022484-9.8293-1.3071-19.894 0.35664-29.438 3.2391-18.579 11.08-35.272 23.763-49.773 12.098-13.832 26.457-23.989 43.609-30.029 7.813-2.7512 16.14-4.0417 24.234-5.9948 7.392-0.025734 14.784-0.05146 22.835 0.32253 4.1959 0.95392 7.7946 1.2538 11.258 2.1053 17.16 4.2192 32.287 12.176 45.469 24.104 2.2558 2.0411 4.372 6.6241 9.621 3.868 16.839-8.8419 34.718-11.597 53.603-8.594 16.791 2.6699 31.602 9.4308 44.236 20.636 11.531 10.227 19.84 22.841 25.393 37.236 6.3436 16.445 10.389 33.163 6.0798 49.389 7.9587 8.9321 15.807 16.704 22.421 25.414 9.162 12.065 15.33 25.746 18.144 40.776 0.97046 5.1848 1.9111 10.375 2.8654 15.563m-71.597 71.012c5.5615-5.2284 12.002-9.7986 16.508-15.817 10.474-13.992 14.333-29.916 11.288-47.446-2.2496-12.95-8.1973-24.076-17.243-33.063-12.746-12.663-28.865-18.614-46.786-18.569-69.912 0.17712-139.82 0.56831-209.74 0.96176-15.922 0.089599-29.168 7.4209-39.685 18.296-14.45 14.944-20.408 33.343-16.655 54.368 2.2763 12.754 8.2167 23.748 17.158 32.66 13.299 13.255 30.097 18.653 48.728 18.651 59.321-0.005188 118.64 0.042358 177.96-0.046601 9.5912-0.014374 19.181-0.86588 28.773-0.88855 10.649-0.025146 19.978-3.825 29.687-9.1074z" fill="#EEC170"/>
|
||||
@@ -71,8 +133,8 @@ export function injectIcon(input: HTMLInputElement, container: HTMLElement): voi
|
||||
`;
|
||||
|
||||
// Generate unique ID if input doesn't have one
|
||||
if (!input.id) {
|
||||
input.id = `aliasvault-input-${Math.random().toString(36).substring(2, 11)}`;
|
||||
if (!actualInput.id) {
|
||||
actualInput.id = `aliasvault-input-${Math.random().toString(36).substring(2, 11)}`;
|
||||
}
|
||||
|
||||
// Create an overlay container at document level if it doesn't exist
|
||||
@@ -88,19 +150,26 @@ export function injectIcon(input: HTMLInputElement, container: HTMLElement): voi
|
||||
const iconContainer = document.createElement('div');
|
||||
iconContainer.innerHTML = ICON_HTML;
|
||||
const icon = iconContainer.firstElementChild as HTMLElement;
|
||||
icon.setAttribute('data-icon-for', input.id);
|
||||
icon.setAttribute('data-icon-for', actualInput.id);
|
||||
|
||||
// Enable pointer events just for the icon
|
||||
icon.style.pointerEvents = 'auto';
|
||||
|
||||
/**
|
||||
* Update position of the icon.
|
||||
* Positions icon relative to right edge, moving it left by any existing padding.
|
||||
*/
|
||||
const updateIconPosition = () : void => {
|
||||
const rect = input.getBoundingClientRect();
|
||||
const rect = actualInput.getBoundingClientRect();
|
||||
const computedStyle = window.getComputedStyle(actualInput);
|
||||
const paddingRight = parseInt(computedStyle.paddingLeft + computedStyle.paddingRight);
|
||||
|
||||
// Default offset is 32px, add any padding to move it further left
|
||||
const rightOffset = 24 + paddingRight;
|
||||
|
||||
icon.style.position = 'fixed';
|
||||
icon.style.top = `${rect.top + (rect.height - 24) / 2}px`;
|
||||
icon.style.left = `${rect.right - 32}px`;
|
||||
icon.style.left = `${(rect.left + rect.width) - rightOffset}px`;
|
||||
};
|
||||
|
||||
// Update position initially and on relevant events
|
||||
@@ -112,8 +181,8 @@ export function injectIcon(input: HTMLInputElement, container: HTMLElement): voi
|
||||
icon.addEventListener('click', (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setTimeout(() => input.focus(), 0);
|
||||
openAutofillPopup(input, container);
|
||||
setTimeout(() => actualInput.focus(), 0);
|
||||
openAutofillPopup(actualInput, container);
|
||||
});
|
||||
|
||||
// Append the icon to the overlay container
|
||||
@@ -131,8 +200,8 @@ export function injectIcon(input: HTMLInputElement, container: HTMLElement): voi
|
||||
icon.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
icon.remove();
|
||||
input.removeEventListener('blur', handleBlur);
|
||||
input.removeEventListener('keydown', handleKeyPress);
|
||||
actualInput.removeEventListener('blur', handleBlur);
|
||||
actualInput.removeEventListener('keydown', handleKeyPress);
|
||||
window.removeEventListener('scroll', updateIconPosition, true);
|
||||
window.removeEventListener('resize', updateIconPosition);
|
||||
|
||||
@@ -153,8 +222,8 @@ export function injectIcon(input: HTMLInputElement, container: HTMLElement): voi
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('blur', handleBlur);
|
||||
input.addEventListener('keydown', handleKeyPress);
|
||||
actualInput.addEventListener('blur', handleBlur);
|
||||
actualInput.addEventListener('keydown', handleKeyPress);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,11 +6,12 @@ import { PasswordGenerator } from '../../utils/generators/Password/PasswordGener
|
||||
import { storage } from "wxt/storage";
|
||||
import { sendMessage } from "webext-bridge/content-script";
|
||||
import { CredentialsResponse } from '@/utils/types/messaging/CredentialsResponse';
|
||||
import { CombinedStopWords } from '../../utils/formDetector/FieldPatterns';
|
||||
import { PasswordSettingsResponse } from '@/utils/types/messaging/PasswordSettingsResponse';
|
||||
import SqliteClient from '../../utils/SqliteClient';
|
||||
import { BaseIdentityGenerator } from '@/utils/generators/Identity/implementations/base/BaseIdentityGenerator';
|
||||
import { StringResponse } from '@/utils/types/messaging/StringResponse';
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { Credential } from '@/utils/types/Credential';
|
||||
|
||||
// TODO: store generic setting constants somewhere else.
|
||||
export const DISABLED_SITES_KEY = 'local:aliasvault_disabled_sites';
|
||||
@@ -159,7 +160,7 @@ export function removeExistingPopup(container: HTMLElement) : void {
|
||||
/**
|
||||
* Create auto-fill popup
|
||||
*/
|
||||
export function createAutofillPopup(input: HTMLInputElement, credentials: Credential[] | undefined, rootContainer: HTMLElement) : void {
|
||||
export function createAutofillPopup(input: HTMLInputElement, credentials: Credential[] | undefined, rootContainer: HTMLElement) : void {
|
||||
// Disable browser's native autocomplete to avoid conflicts with AliasVault's autocomplete.
|
||||
input.setAttribute('autocomplete', 'false');
|
||||
const popup = createBasePopup(input, rootContainer);
|
||||
@@ -211,8 +212,8 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
const suggestedName = getSuggestedServiceName(document, window.location);
|
||||
const result = await createAliasCreationPopup(suggestedName, rootContainer);
|
||||
const suggestedNames = FormDetector.getSuggestedServiceName(document, window.location);
|
||||
const result = await createAliasCreationPopup(suggestedNames, rootContainer);
|
||||
|
||||
if (!result) {
|
||||
// User cancelled
|
||||
@@ -265,7 +266,7 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
|
||||
|
||||
// Get password settings from background
|
||||
const passwordSettingsResponse = await sendMessage('GET_PASSWORD_SETTINGS', {}, 'background') as PasswordSettingsResponse;
|
||||
|
||||
|
||||
// Initialize password generator with the retrieved settings
|
||||
const passwordGenerator = new PasswordGenerator(passwordSettingsResponse.settings);
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
@@ -315,32 +316,12 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
|
||||
};
|
||||
|
||||
// Add click listener with capture and prevent removal.
|
||||
createButton.addEventListener('click', handleCreateClick, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
|
||||
// Backup click handling using mousedown/mouseup if needed.
|
||||
let isMouseDown = false;
|
||||
createButton.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
isMouseDown = true;
|
||||
}, { capture: true });
|
||||
|
||||
createButton.addEventListener('mouseup', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isMouseDown) {
|
||||
handleCreateClick(e);
|
||||
}
|
||||
isMouseDown = false;
|
||||
}, { capture: true });
|
||||
addReliableClickHandler(createButton, handleCreateClick);
|
||||
|
||||
// Create search input.
|
||||
const searchInput = document.createElement('input');
|
||||
searchInput.type = 'text';
|
||||
searchInput.dataset.aliasvaultIgnore = 'true';
|
||||
searchInput.dataset.avDisable = 'true';
|
||||
searchInput.placeholder = 'Search vault...';
|
||||
searchInput.className = 'av-search-input';
|
||||
|
||||
@@ -358,10 +339,18 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
|
||||
</svg>
|
||||
`;
|
||||
|
||||
closeButton.addEventListener('click', async () => {
|
||||
/**
|
||||
* Handle close button click
|
||||
*/
|
||||
const handleCloseClick = async (e: Event) : Promise<void> => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
await disableAutoShowPopup();
|
||||
removeExistingPopup(rootContainer);
|
||||
});
|
||||
};
|
||||
|
||||
addReliableClickHandler(closeButton, handleCloseClick);
|
||||
|
||||
actionContainer.appendChild(searchInput);
|
||||
actionContainer.appendChild(createButton);
|
||||
@@ -517,7 +506,7 @@ function handleSearchInput(searchInput: HTMLInputElement, credentials: Credentia
|
||||
filteredCredentials = uniqueCredentials.filter(cred => {
|
||||
const searchableFields = [
|
||||
cred.ServiceName?.toLowerCase(),
|
||||
cred.Username?.toLowerCase(),
|
||||
cred.Username?.toLowerCase(),
|
||||
cred.Alias?.Email?.toLowerCase(),
|
||||
cred.ServiceUrl?.toLowerCase()
|
||||
];
|
||||
@@ -601,7 +590,7 @@ function createCredentialList(credentials: Credential[], input: HTMLInputElement
|
||||
`;
|
||||
|
||||
// Handle popout click
|
||||
popoutIcon.addEventListener('click', (e) => {
|
||||
addReliableClickHandler(popoutIcon, (e) => {
|
||||
e.stopPropagation(); // Prevent credential fill
|
||||
sendMessage('OPEN_POPUP_WITH_CREDENTIAL', { credentialId: cred.Id }, 'background');
|
||||
removeExistingPopup(rootContainer);
|
||||
@@ -611,7 +600,7 @@ function createCredentialList(credentials: Credential[], input: HTMLInputElement
|
||||
item.appendChild(popoutIcon);
|
||||
|
||||
// Update click handler to only trigger on credentialInfo
|
||||
credentialInfo.addEventListener('click', () => {
|
||||
addReliableClickHandler(credentialInfo, () => {
|
||||
fillCredential(cred, input);
|
||||
removeExistingPopup(rootContainer);
|
||||
});
|
||||
@@ -671,7 +660,7 @@ export async function disableAutoShowPopup(): Promise<void> {
|
||||
/**
|
||||
* Create alias creation popup where user can choose between random alias and custom alias.
|
||||
*/
|
||||
export async function createAliasCreationPopup(defaultName: string, rootContainer: HTMLElement): Promise<{ serviceName: string | null, isCustomCredential: boolean, customEmail?: string, customUsername?: string, customPassword?: string } | null> {
|
||||
export async function createAliasCreationPopup(suggestedNames: string[], rootContainer: HTMLElement): Promise<{ serviceName: string | null, isCustomCredential: boolean, customEmail?: string, customUsername?: string, customPassword?: string } | null> {
|
||||
// Close existing popup
|
||||
removeExistingPopup(rootContainer);
|
||||
|
||||
@@ -755,17 +744,23 @@ export async function createAliasCreationPopup(defaultName: string, rootContaine
|
||||
|
||||
<div class="av-create-popup-help-text">${randomIdentitySubtext}</div>
|
||||
|
||||
<div class="av-create-popup-field-group">
|
||||
<label for="service-name-input">Service name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="service-name-input"
|
||||
value="${suggestedNames[0] ?? ''}"
|
||||
class="av-create-popup-input"
|
||||
placeholder="Enter service name"
|
||||
>
|
||||
${suggestedNames.length > 1 ? `
|
||||
<div class="av-suggested-names">
|
||||
${getSuggestedNamesHtml(suggestedNames, suggestedNames[0] ?? '')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="av-create-popup-mode av-create-popup-random-mode">
|
||||
<div class="av-create-popup-field-group">
|
||||
<label for="service-name-input">Service name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="service-name-input"
|
||||
value="${defaultName}"
|
||||
class="av-create-popup-input"
|
||||
placeholder="Enter service name"
|
||||
>
|
||||
</div>
|
||||
<div class="av-create-popup-actions">
|
||||
<button id="cancel-btn" class="av-create-popup-cancel">Cancel</button>
|
||||
<button id="save-btn" class="av-create-popup-save">Create and save alias</button>
|
||||
@@ -773,16 +768,6 @@ export async function createAliasCreationPopup(defaultName: string, rootContaine
|
||||
</div>
|
||||
|
||||
<div class="av-create-popup-mode av-create-popup-custom-mode" style="display: none;">
|
||||
<div class="av-create-popup-field-group">
|
||||
<label for="custom-service-name">Service name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="custom-service-name"
|
||||
value="${defaultName}"
|
||||
class="av-create-popup-input"
|
||||
placeholder="Enter service name"
|
||||
>
|
||||
</div>
|
||||
<div class="av-create-popup-field-group">
|
||||
<label for="custom-email">Email</label>
|
||||
<input
|
||||
@@ -810,8 +795,15 @@ export async function createAliasCreationPopup(defaultName: string, rootContaine
|
||||
type="text"
|
||||
id="password-preview"
|
||||
class="av-create-popup-input"
|
||||
data-is-generated="true"
|
||||
>
|
||||
<button id="regenerate-password" class="av-create-popup-regenerate-btn">
|
||||
<button id="toggle-password-visibility" class="av-create-popup-visibility-btn" title="Toggle password visibility">
|
||||
<svg class="av-icon" viewBox="0 0 24 24">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="regenerate-password" class="av-create-popup-regenerate-btn" title="Generate new password">
|
||||
<svg class="av-icon" viewBox="0 0 24 24">
|
||||
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
|
||||
<path d="M3 3v5h5"></path>
|
||||
@@ -843,12 +835,12 @@ export async function createAliasCreationPopup(defaultName: string, rootContaine
|
||||
const customCancelBtn = popup.querySelector('#custom-cancel-btn') as HTMLButtonElement;
|
||||
const saveBtn = popup.querySelector('#save-btn') as HTMLButtonElement;
|
||||
const customSaveBtn = popup.querySelector('#custom-save-btn') as HTMLButtonElement;
|
||||
const input = popup.querySelector('#service-name-input') as HTMLInputElement;
|
||||
const customInput = popup.querySelector('#custom-service-name') as HTMLInputElement;
|
||||
const inputServiceName = popup.querySelector('#service-name-input') as HTMLInputElement;
|
||||
const customEmail = popup.querySelector('#custom-email') as HTMLInputElement;
|
||||
const customUsername = popup.querySelector('#custom-username') as HTMLInputElement;
|
||||
const passwordPreview = popup.querySelector('#password-preview') as HTMLInputElement;
|
||||
const regenerateBtn = popup.querySelector('#regenerate-password') as HTMLButtonElement;
|
||||
const toggleVisibilityBtn = popup.querySelector('#toggle-password-visibility') as HTMLButtonElement;
|
||||
|
||||
/**
|
||||
* Setup default value for input with placeholder styling.
|
||||
@@ -891,7 +883,14 @@ export async function createAliasCreationPopup(defaultName: string, rootContaine
|
||||
* Generate and set password.
|
||||
*/
|
||||
const generatePassword = () : void => {
|
||||
if (!passwordGenerator) {
|
||||
return;
|
||||
}
|
||||
|
||||
passwordPreview.value = passwordGenerator.generateRandomPassword();
|
||||
passwordPreview.type = 'text';
|
||||
passwordPreview.dataset.isGenerated = 'true';
|
||||
updateVisibilityIcon(true);
|
||||
};
|
||||
|
||||
// Get password settings from background
|
||||
@@ -906,6 +905,65 @@ export async function createAliasCreationPopup(defaultName: string, rootContaine
|
||||
// Handle regenerate button click
|
||||
regenerateBtn.addEventListener('click', generatePassword);
|
||||
|
||||
// Add password visibility toggle functionality
|
||||
const passwordInput = popup.querySelector('#password-preview') as HTMLInputElement;
|
||||
|
||||
/**
|
||||
* Toggle password visibility icon
|
||||
*/
|
||||
const updateVisibilityIcon = (isVisible: boolean): void => {
|
||||
toggleVisibilityBtn.innerHTML = isVisible ? `
|
||||
<svg class="av-icon" viewBox="0 0 24 24">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>
|
||||
` : `
|
||||
<svg class="av-icon" viewBox="0 0 24 24">
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
|
||||
<line x1="1" y1="1" x2="23" y2="23"></line>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle password visibility
|
||||
*/
|
||||
const togglePasswordVisibility = (): void => {
|
||||
const isVisible = passwordInput.type === 'text';
|
||||
passwordInput.type = isVisible ? 'password' : 'text';
|
||||
updateVisibilityIcon(!isVisible);
|
||||
};
|
||||
|
||||
toggleVisibilityBtn.addEventListener('click', togglePasswordVisibility);
|
||||
|
||||
/**
|
||||
* Handle password input changes
|
||||
*/
|
||||
const handlePasswordChange = (e: Event): void => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const isGenerated = target.dataset.isGenerated === 'true';
|
||||
const isEmpty = target.value.trim().length <= 1;
|
||||
|
||||
// If manually cleared (empty or single char) and was previously generated, switch to password type
|
||||
if (isEmpty && isGenerated) {
|
||||
target.type = 'password';
|
||||
target.dataset.isGenerated = 'false';
|
||||
updateVisibilityIcon(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle paste events
|
||||
*/
|
||||
const handlePasswordPaste = (): void => {
|
||||
passwordInput.dataset.isGenerated = 'false';
|
||||
passwordInput.type = 'password';
|
||||
updateVisibilityIcon(false);
|
||||
};
|
||||
|
||||
passwordInput.addEventListener('input', handlePasswordChange);
|
||||
passwordInput.addEventListener('paste', handlePasswordPaste);
|
||||
|
||||
/**
|
||||
* Toggle dropdown visibility.
|
||||
*/
|
||||
@@ -965,7 +1023,7 @@ export async function createAliasCreationPopup(defaultName: string, rootContaine
|
||||
|
||||
// Handle save buttons
|
||||
saveBtn.addEventListener('click', () => {
|
||||
const serviceName = input.value.trim();
|
||||
const serviceName = inputServiceName.value.trim();
|
||||
if (serviceName) {
|
||||
closePopup({
|
||||
serviceName,
|
||||
@@ -978,7 +1036,7 @@ export async function createAliasCreationPopup(defaultName: string, rootContaine
|
||||
* Handle custom save button click.
|
||||
*/
|
||||
const handleCustomSave = () : void => {
|
||||
const serviceName = customInput.value.trim();
|
||||
const serviceName = inputServiceName.value.trim();
|
||||
if (serviceName) {
|
||||
const email = customEmail.value.trim();
|
||||
const username = customUsername.value.trim();
|
||||
@@ -993,25 +1051,25 @@ export async function createAliasCreationPopup(defaultName: string, rootContaine
|
||||
// Add error styling to fields
|
||||
customEmail.classList.add('av-create-popup-input-error');
|
||||
customUsername.classList.add('av-create-popup-input-error');
|
||||
|
||||
|
||||
// Add error messages after labels
|
||||
const emailLabel = customEmail.previousElementSibling as HTMLLabelElement;
|
||||
const usernameLabel = customUsername.previousElementSibling as HTMLLabelElement;
|
||||
|
||||
|
||||
if (!emailLabel.querySelector('.av-create-popup-error-text')) {
|
||||
const emailError = document.createElement('span');
|
||||
emailError.className = 'av-create-popup-error-text';
|
||||
emailError.textContent = 'Enter email and/or username';
|
||||
emailLabel.appendChild(emailError);
|
||||
}
|
||||
|
||||
|
||||
if (!usernameLabel.querySelector('.av-create-popup-error-text')) {
|
||||
const usernameError = document.createElement('span');
|
||||
usernameError.className = 'av-create-popup-error-text';
|
||||
usernameError.textContent = 'Enter email and/or username';
|
||||
usernameLabel.appendChild(usernameError);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove error styling.
|
||||
*/
|
||||
@@ -1027,10 +1085,10 @@ export async function createAliasCreationPopup(defaultName: string, rootContaine
|
||||
usernameError.remove();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
customEmail.addEventListener('input', removeError, { once: true });
|
||||
customUsername.addEventListener('input', removeError, { once: true });
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1055,8 +1113,8 @@ export async function createAliasCreationPopup(defaultName: string, rootContaine
|
||||
}
|
||||
};
|
||||
|
||||
customInput.addEventListener('keyup', handleCustomEnter);
|
||||
customEmail.addEventListener('keyup', handleCustomEnter);
|
||||
inputServiceName.addEventListener('keyup', handleCustomEnter);
|
||||
customEmail.addEventListener('keyup', handleCustomEnter);
|
||||
customUsername.addEventListener('keyup', handleCustomEnter);
|
||||
passwordPreview.addEventListener('keyup', handleCustomEnter);
|
||||
|
||||
@@ -1070,9 +1128,9 @@ export async function createAliasCreationPopup(defaultName: string, rootContaine
|
||||
});
|
||||
|
||||
// Handle Enter key
|
||||
input.addEventListener('keyup', (e) => {
|
||||
inputServiceName.addEventListener('keyup', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const serviceName = input.value.trim();
|
||||
const serviceName = inputServiceName.value.trim();
|
||||
if (serviceName) {
|
||||
closePopup({
|
||||
serviceName,
|
||||
@@ -1095,11 +1153,51 @@ export async function createAliasCreationPopup(defaultName: string, rootContaine
|
||||
// Use mousedown instead of click to prevent closing when dragging text
|
||||
overlay.addEventListener('mousedown', handleClickOutside);
|
||||
|
||||
/**
|
||||
* Handle suggested name click.
|
||||
*/
|
||||
const handleSuggestedNameClick = (e: Event) : void => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.classList.contains('av-suggested-name')) {
|
||||
const name = target.dataset.name;
|
||||
if (name) {
|
||||
// Update input with clicked name
|
||||
inputServiceName.value = name;
|
||||
customUsername.value = name;
|
||||
|
||||
// Update the suggested names section
|
||||
const suggestedNamesContainer = target.closest('.av-suggested-names');
|
||||
if (suggestedNamesContainer) {
|
||||
// Update the suggestions HTML using the helper function
|
||||
suggestedNamesContainer.innerHTML = getSuggestedNamesHtml(suggestedNames, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
popup.addEventListener('click', handleSuggestedNameClick);
|
||||
|
||||
// Focus the input field
|
||||
input.select();
|
||||
inputServiceName.select();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get suggested names HTML with current input value excluded
|
||||
*/
|
||||
function getSuggestedNamesHtml(suggestedNames: string[], currentValue: string): string {
|
||||
// Filter out the current value and create unique set of remaining suggestions
|
||||
const filteredSuggestions = [...new Set(suggestedNames.filter(n => n !== currentValue))];
|
||||
|
||||
if (filteredSuggestions.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `or ${filteredSuggestions.map((name, index) =>
|
||||
`<span class="av-suggested-name" data-name="${name}">${name}</span>${index < filteredSuggestions.length - 1 ? ', ' : ''}`
|
||||
).join('')}?`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get favicon bytes from page and resize if necessary.
|
||||
*/
|
||||
@@ -1108,7 +1206,7 @@ async function getFaviconBytes(document: Document): Promise<Uint8Array | null> {
|
||||
const TARGET_WIDTH = 96; // Resize target width
|
||||
|
||||
const faviconLinks = [
|
||||
...Array.from(document.querySelectorAll('link[rel="icon"][type="image/svg+xml"]')),
|
||||
...Array.from(document.querySelectorAll('link[rel="icon"][type="image/svg+xml"]')),
|
||||
...Array.from(document.querySelectorAll('link[rel="icon"][sizes="96x96"]')),
|
||||
...Array.from(document.querySelectorAll('link[rel="icon"][sizes="128x128"]')),
|
||||
...Array.from(document.querySelectorAll('link[rel="icon"][sizes="48x48"]')),
|
||||
@@ -1225,57 +1323,6 @@ export async function dismissVaultLockedPopup(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a suggested service name from the page title and URL.
|
||||
* Attempts to extract meaningful parts while maintaining original capitalization.
|
||||
*/
|
||||
function getSuggestedServiceName(document: Document, location: Location): string {
|
||||
const title = document.title;
|
||||
|
||||
/**
|
||||
* Filter out common words and keep meaningful parts of the title
|
||||
*/
|
||||
const getMeaningfulTitleParts = (title: string): string[] => {
|
||||
return title
|
||||
.toLowerCase()
|
||||
.split(/[\s|\-—/\\]+/) // Split on spaces and common dividers
|
||||
.filter(word =>
|
||||
word.length > 1 && // Filter out single characters
|
||||
!CombinedStopWords.has(word.toLowerCase()) // Filter out common words
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get original case version of meaningful words
|
||||
*/
|
||||
const getOriginalCase = (text: string, meaningfulParts: string[]): string => {
|
||||
return text
|
||||
.split(/[\s|\-—/\\]+/)
|
||||
.filter(word => meaningfulParts.includes(word.toLowerCase()))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
// First try to extract meaningful parts after the last divider
|
||||
const dividerRegex = /[|\-—/\\][^|\-—/\\]*$/;
|
||||
const dividerMatch = dividerRegex.exec(title);
|
||||
if (dividerMatch) {
|
||||
const meaningfulParts = getMeaningfulTitleParts(dividerMatch[0]);
|
||||
if (meaningfulParts.length > 0) {
|
||||
return getOriginalCase(dividerMatch[0].trim(), meaningfulParts);
|
||||
}
|
||||
}
|
||||
|
||||
// If no meaningful parts found after divider, try the full title
|
||||
const meaningfulParts = getMeaningfulTitleParts(title);
|
||||
if (meaningfulParts.length > 0) {
|
||||
return getOriginalCase(title, meaningfulParts);
|
||||
}
|
||||
|
||||
// Fall back to domain name if no meaningful parts found
|
||||
const domainParts = location.hostname.replace(/^www\./, '').split('.');
|
||||
return domainParts.slice(-2).join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a valid service URL from the current page.
|
||||
*/
|
||||
@@ -1304,3 +1351,35 @@ function getValidServiceUrl(): string {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add click handler with mousedown/mouseup backup for better click reliability in shadow DOM.
|
||||
*
|
||||
* Some websites due to their design cause the AliasVault autofill to re-trigger when clicking
|
||||
* outside of the input field, which causes the AliasVault popup to close before the click event
|
||||
* is registered. This is a workaround to ensure the click event is always registered.
|
||||
*/
|
||||
function addReliableClickHandler(element: HTMLElement, handler: (e: Event) => void): void {
|
||||
// Add primary click listener with capture and prevent removal
|
||||
element.addEventListener('click', handler, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
|
||||
// Backup click handling using mousedown/mouseup if needed
|
||||
let isMouseDown = false;
|
||||
element.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
isMouseDown = true;
|
||||
}, { capture: true });
|
||||
|
||||
element.addEventListener('mouseup', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isMouseDown) {
|
||||
handler(e);
|
||||
}
|
||||
isMouseDown = false;
|
||||
}, { capture: true });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* AliasVault Content Script Styles */
|
||||
body {
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Base Popup Styles */
|
||||
@@ -111,6 +113,20 @@ body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-suggested-names {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #acacac;
|
||||
}
|
||||
.av-suggested-name {
|
||||
color: #bababa;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.av-suggested-name:hover {
|
||||
color: #d68338;
|
||||
}
|
||||
|
||||
.av-service-details {
|
||||
font-size: 0.85em;
|
||||
white-space: nowrap;
|
||||
@@ -439,7 +455,7 @@ body {
|
||||
}
|
||||
|
||||
.av-create-popup-field-group {
|
||||
margin-bottom: 24px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.av-create-popup-field-group label {
|
||||
@@ -474,7 +490,8 @@ body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.av-create-popup-regenerate-btn {
|
||||
.av-create-popup-regenerate-btn,
|
||||
.av-create-popup-visibility-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -488,7 +505,8 @@ body {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.av-create-popup-regenerate-btn:hover {
|
||||
.av-create-popup-regenerate-btn:hover,
|
||||
.av-create-popup-visibility-btn:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
@@ -501,6 +519,14 @@ body {
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.av-create-popup-visibility-btn .av-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.5;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.av-create-popup-error {
|
||||
margin-top: 16px;
|
||||
padding: 8px 12px;
|
||||
@@ -631,10 +657,6 @@ body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.av-create-popup-mode {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.av-create-popup-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -21,6 +21,28 @@ const CredentialsList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
|
||||
|
||||
/**
|
||||
* Get the display text for a credential, showing username by default,
|
||||
* falling back to email only if username is null/undefined
|
||||
*/
|
||||
const getCredentialDisplayText = (cred: Credential): string => {
|
||||
const username = cred.Username ?? '';
|
||||
|
||||
// Show username if available.
|
||||
if (username.length > 0) {
|
||||
return username;
|
||||
}
|
||||
|
||||
// Show email if username is not available.
|
||||
const email = cred.Alias?.Email ?? '';
|
||||
if (email.length > 0) {
|
||||
return email;
|
||||
}
|
||||
|
||||
// Show empty string if neither username nor email is available.
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Loading state with minimum duration for more fluid UX.
|
||||
*/
|
||||
@@ -173,7 +195,9 @@ const CredentialsList: React.FC = () => {
|
||||
/>
|
||||
<div className="text-left">
|
||||
<p className="font-medium text-gray-900 dark:text-white">{cred.ServiceName}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{cred.Username}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{getCredentialDisplayText(cred)}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -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.16.0';
|
||||
public static readonly VERSION = '0.16.2';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault server (API) version. If the server version is below this, the
|
||||
|
||||
@@ -86,7 +86,7 @@ export const EnglishStopWords = new Set([
|
||||
|
||||
// Marketing/Promotional
|
||||
'free', 'create', 'new', 'your', 'special', 'offer',
|
||||
'deal', 'discount', 'promotion',
|
||||
'deal', 'discount', 'promotion', 'newsletter',
|
||||
|
||||
// Common website sections
|
||||
'help', 'support', 'contact', 'about', 'faq', 'terms',
|
||||
@@ -95,14 +95,17 @@ export const EnglishStopWords = new Set([
|
||||
|
||||
// Generic descriptors
|
||||
'online', 'web', 'digital', 'mobile', 'my', 'personal',
|
||||
'private', 'general', 'default', 'standard',
|
||||
'private', 'general', 'default', 'standard', 'website',
|
||||
|
||||
// System/Technical
|
||||
'system', 'admin', 'administrator', 'platform', 'portal',
|
||||
'gateway', 'api', 'interface', 'console',
|
||||
|
||||
// Time-related
|
||||
'today', 'now', 'current', 'latest', 'newest', 'recent'
|
||||
'today', 'now', 'current', 'latest', 'newest', 'recent',
|
||||
|
||||
// General
|
||||
'the', 'and', 'or', 'but', 'to', 'up'
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -174,7 +177,10 @@ export const DutchStopWords = new Set([
|
||||
'interface', 'console',
|
||||
|
||||
// Time-related
|
||||
'vandaag', 'nu', 'huidig', 'recent', 'nieuwste'
|
||||
'vandaag', 'nu', 'huidig', 'recent', 'nieuwste',
|
||||
|
||||
// General
|
||||
'je', 'in', 'op', 'de', 'van', 'ons', 'allemaal'
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormFields } from "./types/FormFields";
|
||||
import { CombinedFieldPatterns, CombinedGenderOptionPatterns } from "./FieldPatterns";
|
||||
import { CombinedFieldPatterns, CombinedGenderOptionPatterns, CombinedStopWords } from "./FieldPatterns";
|
||||
|
||||
/**
|
||||
* Form detector.
|
||||
@@ -18,6 +18,150 @@ export class FormDetector {
|
||||
this.visibilityCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect login forms on the page based on the clicked element.
|
||||
*/
|
||||
public containsLoginForm(): boolean {
|
||||
let formWrapper = this.clickedElement?.closest('form, [role="dialog"]') as HTMLElement | null;
|
||||
if (formWrapper?.getAttribute('role') === 'dialog') {
|
||||
// If we hit a dialog, search for form only within the dialog
|
||||
formWrapper = formWrapper.querySelector('form') as HTMLElement | null ?? formWrapper;
|
||||
}
|
||||
|
||||
if (!formWrapper) {
|
||||
// If no form or dialog found, fallback to document.body
|
||||
formWrapper = this.document.body as HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanity check: if form contains more than 150 inputs, don't process as this is likely not a login form.
|
||||
* This is a simple way to prevent processing large forms that are not login forms and making the browser page unresponsive.
|
||||
*/
|
||||
const inputCount = formWrapper.querySelectorAll('input').length;
|
||||
if (inputCount > 200) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the wrapper contains a password or likely username field before processing.
|
||||
if (this.containsPasswordField(formWrapper) || this.containsLikelyUsernameOrEmailField(formWrapper)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect login forms on the page based on the clicked element.
|
||||
*/
|
||||
public getForm(): FormFields | null {
|
||||
if (!this.clickedElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formWrapper = this.clickedElement.closest('form') ?? this.document.body;
|
||||
return this.detectFormFields(formWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get suggested service names from the page title and URL.
|
||||
* Returns an array with two suggestions: the primary name and the domain name as an alternative.
|
||||
*/
|
||||
public static getSuggestedServiceName(document: Document, location: Location): string[] {
|
||||
const title = document.title;
|
||||
const maxWords = 4;
|
||||
const maxLength = 50;
|
||||
|
||||
/**
|
||||
* We apply a limit to the length and word count of the title to prevent
|
||||
* the service name from being too long or containing too many words which
|
||||
* is not likely to be a good service name.
|
||||
*/
|
||||
const validLength = (text: string): boolean => {
|
||||
const validLength = text.length >= 3 && text.length <= maxLength;
|
||||
const validWordCount = text.split(/[\s|\-—/\\]+/).length <= maxWords;
|
||||
return validLength && validWordCount;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter out common words from prefix/suffix until no more matches found
|
||||
*/
|
||||
const getMeaningfulTitleParts = (title: string): string[] => {
|
||||
const words = title.toLowerCase().split(' ').map(word => word.toLowerCase());
|
||||
|
||||
// Strip stopwords from start until no more matches
|
||||
let startIndex = 0;
|
||||
while (startIndex < words.length && CombinedStopWords.has(words[startIndex].toLowerCase())) {
|
||||
startIndex++;
|
||||
}
|
||||
|
||||
// Strip stopwords from end until no more matches
|
||||
let endIndex = words.length - 1;
|
||||
while (endIndex > startIndex && CombinedStopWords.has(words[endIndex].toLowerCase())) {
|
||||
endIndex--;
|
||||
}
|
||||
|
||||
// Return remaining words
|
||||
return words.slice(startIndex, endIndex + 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get original case version of meaningful words
|
||||
*/
|
||||
const getOriginalCase = (text: string, meaningfulParts: string[]): string => {
|
||||
return text
|
||||
.split(/[\s|]+/)
|
||||
.filter(word => meaningfulParts.includes(word.toLowerCase()))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
// Domain name suggestion (always included as fallback or first suggestion)
|
||||
const domainSuggestion = location.hostname.replace(/^www\./, '');
|
||||
|
||||
// First try to extract meaningful parts based on the divider
|
||||
const dividerRegex = /[|\-—/\\:]/;
|
||||
const dividerMatch = dividerRegex.exec(title);
|
||||
if (dividerMatch) {
|
||||
const dividerIndex = dividerMatch.index;
|
||||
const beforeDivider = title.substring(0, dividerIndex).trim();
|
||||
const afterDivider = title.substring(dividerIndex + 1).trim();
|
||||
|
||||
// Count meaningful words on each side
|
||||
const beforeWords = getMeaningfulTitleParts(beforeDivider);
|
||||
const afterWords = getMeaningfulTitleParts(afterDivider);
|
||||
|
||||
// Get both parts in original case
|
||||
const beforePart = getOriginalCase(beforeDivider, beforeWords);
|
||||
const afterPart = getOriginalCase(afterDivider, afterWords);
|
||||
|
||||
// Check if both parts are valid
|
||||
const beforeValid = validLength(beforePart);
|
||||
const afterValid = validLength(afterPart);
|
||||
|
||||
// If both parts are valid, return both as suggestions
|
||||
if (beforeValid && afterValid) {
|
||||
return [beforePart, afterPart, domainSuggestion];
|
||||
}
|
||||
|
||||
// If only one part is valid, return it
|
||||
if (beforeValid) {
|
||||
return [beforePart, domainSuggestion];
|
||||
}
|
||||
if (afterValid) {
|
||||
return [afterPart, domainSuggestion];
|
||||
}
|
||||
}
|
||||
|
||||
// If no meaningful parts found after divider, try the full title
|
||||
const meaningfulParts = getMeaningfulTitleParts(title);
|
||||
const serviceName = getOriginalCase(title, meaningfulParts);
|
||||
if (validLength(serviceName)) {
|
||||
return [serviceName, domainSuggestion];
|
||||
}
|
||||
|
||||
// Fall back to domain name
|
||||
return [domainSuggestion];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an element and all its parents are visible.
|
||||
* This checks for display:none, visibility:hidden, and opacity:0
|
||||
@@ -46,7 +190,7 @@ export class FormDetector {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check for display:none
|
||||
if (style.display === 'none') {
|
||||
// Cache and return false for this element and all its parents
|
||||
@@ -57,7 +201,7 @@ export class FormDetector {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Check for visibility:hidden
|
||||
if (style.visibility === 'hidden') {
|
||||
// Cache and return false for this element and all its parents
|
||||
@@ -68,7 +212,7 @@ export class FormDetector {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Check for opacity:0
|
||||
if (parseFloat(style.opacity) === 0) {
|
||||
// Cache and return false for this element and all its parents
|
||||
@@ -97,41 +241,6 @@ export class FormDetector {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect login forms on the page based on the clicked element.
|
||||
*/
|
||||
public containsLoginForm(): boolean {
|
||||
const formWrapper = this.clickedElement?.closest('form') ?? this.document.body;
|
||||
|
||||
/**
|
||||
* Sanity check: if form contains more than 150 inputs, don't process as this is likely not a login form.
|
||||
* This is a simple way to prevent processing large forms that are not login forms and making the browser page unresponsive.
|
||||
*/
|
||||
const inputCount = formWrapper.querySelectorAll('input').length;
|
||||
if (inputCount > 200) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the wrapper contains a password or likely username field before processing.
|
||||
if (this.containsPasswordField(formWrapper) || this.containsLikelyUsernameOrEmailField(formWrapper)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect login forms on the page based on the clicked element.
|
||||
*/
|
||||
public getForm(): FormFields | null {
|
||||
if (!this.clickedElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formWrapper = this.clickedElement.closest('form') ?? this.document.body;
|
||||
return this.detectFormFields(formWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an input field based on common patterns in its attributes.
|
||||
*/
|
||||
@@ -186,6 +295,17 @@ export class FormDetector {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for sibling elements with class containing "label"
|
||||
const parent = input.parentElement;
|
||||
if (parent) {
|
||||
const siblings = Array.from(parent.children);
|
||||
for (const sibling of siblings) {
|
||||
if (sibling !== input && Array.from(sibling.classList).some(c => c.toLowerCase().includes('label'))) {
|
||||
attributes.push(sibling.textContent?.toLowerCase() ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for parent label and table cell structure
|
||||
let currentElement = input;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
|
||||
@@ -76,4 +76,11 @@ describe('FormDetector English tests', () => {
|
||||
|
||||
testField(FormField.Email, 'resolving_input', htmlFile);
|
||||
});
|
||||
|
||||
describe('English login form 2 detection', () => {
|
||||
const htmlFile = 'en-login-form2.html';
|
||||
|
||||
testField(FormField.Email, 'account_name_text_field', htmlFile);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createTestDocument } from './TestUtils';
|
||||
import { FormDetector } from '../FormDetector';
|
||||
|
||||
describe('FormDetector.getSuggestedServiceName (English)', () => {
|
||||
it('should extract service name from title with divider and include domain', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
'Welcome to MyBank - Online Banking Platform For You',
|
||||
'https://www.mybank.com'
|
||||
);
|
||||
const suggestions = FormDetector.getSuggestedServiceName(document, location);
|
||||
expect(suggestions).toEqual(['MyBank', 'Banking Platform For You', 'mybank.com']);
|
||||
});
|
||||
|
||||
it('should extract service name from title without divider and include domain', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
'GitHub: Let\'s build from here',
|
||||
'https://github.com'
|
||||
);
|
||||
const suggestions = FormDetector.getSuggestedServiceName(document, location);
|
||||
expect(suggestions).toEqual(['GitHub', 'Let\'s build from here', 'github.com']);
|
||||
});
|
||||
|
||||
it('should handle titles with multiple meaningful words and include domain', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
'Amazon Shopping Cart',
|
||||
'https://www.amazon.com'
|
||||
);
|
||||
const suggestions = FormDetector.getSuggestedServiceName(document, location);
|
||||
expect(suggestions).toEqual(['Amazon Shopping', 'amazon.com']);
|
||||
});
|
||||
|
||||
it('should return only domain name when title has no meaningful words', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
'Home | Welcome',
|
||||
'https://www.example.com'
|
||||
);
|
||||
const suggestions = FormDetector.getSuggestedServiceName(document, location);
|
||||
expect(suggestions).toEqual(['example.com']);
|
||||
});
|
||||
|
||||
it('should handle titles with special characters and include domain', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
'Netflix - Watch TV Shows Online, Watch Movies Online',
|
||||
'https://www.netflix.com'
|
||||
);
|
||||
const suggestions = FormDetector.getSuggestedServiceName(document, location);
|
||||
expect(suggestions).toEqual(['Netflix', 'netflix.com']);
|
||||
});
|
||||
|
||||
it('should handle titles with multiple dividers and include domain', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
'Twitter / X - Social Media Platform',
|
||||
'https://twitter.com'
|
||||
);
|
||||
const suggestions = FormDetector.getSuggestedServiceName(document, location);
|
||||
expect(suggestions).toEqual(['Twitter', 'X - Social Media', 'twitter.com']);
|
||||
});
|
||||
|
||||
it('should handle empty titles by returning only domain', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
'',
|
||||
'https://www.example.com'
|
||||
);
|
||||
const suggestions = FormDetector.getSuggestedServiceName(document, location);
|
||||
expect(suggestions).toEqual(['example.com']);
|
||||
});
|
||||
|
||||
it('should handle titles with only stop words by returning only domain', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
'The and or but',
|
||||
'https://www.example.com'
|
||||
);
|
||||
const suggestions = FormDetector.getSuggestedServiceName(document, location);
|
||||
expect(suggestions).toEqual(['example.com']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createTestDocument } from './TestUtils';
|
||||
import { FormDetector } from '../FormDetector';
|
||||
|
||||
describe('FormDetector.getSuggestedServiceName (Dutch)', () => {
|
||||
it('should extract service name from title with divider and include domain', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
'ING - Online Bankieren',
|
||||
'https://www.ing.nl'
|
||||
);
|
||||
const suggestions = FormDetector.getSuggestedServiceName(document, location);
|
||||
expect(suggestions).toEqual(['ING', 'Bankieren', 'ing.nl']);
|
||||
});
|
||||
|
||||
it('should extract service name from title without divider and include domain', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
'Bol.com | De winkel van ons allemaal',
|
||||
'https://www.bol.com'
|
||||
);
|
||||
const suggestions = FormDetector.getSuggestedServiceName(document, location);
|
||||
expect(suggestions).toEqual(['Bol.com', 'bol.com']);
|
||||
});
|
||||
|
||||
it('should handle titles with multiple meaningful words and include domain', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
'Albert Heijn Online Boodschappen',
|
||||
'https://www.ah.nl'
|
||||
);
|
||||
const suggestions = FormDetector.getSuggestedServiceName(document, location);
|
||||
expect(suggestions).toEqual(['Albert Heijn Online Boodschappen', 'ah.nl']);
|
||||
});
|
||||
|
||||
it('should return only domain name when title has no meaningful words', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
'Home | Welkom',
|
||||
'https://www.voorbeeld.nl'
|
||||
);
|
||||
const suggestions = FormDetector.getSuggestedServiceName(document, location);
|
||||
expect(suggestions).toEqual(['voorbeeld.nl']);
|
||||
});
|
||||
|
||||
it('should handle titles with special characters and include domain', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
'NS - Nederlandse Spoorwegen',
|
||||
'https://www.ns.nl'
|
||||
);
|
||||
const suggestions = FormDetector.getSuggestedServiceName(document, location);
|
||||
expect(suggestions).toEqual(['Nederlandse Spoorwegen', 'ns.nl']);
|
||||
});
|
||||
|
||||
it('should handle titles with multiple dividers and include domain', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
'KPN / Internet & TV',
|
||||
'https://www.kpn.nl'
|
||||
);
|
||||
const suggestions = FormDetector.getSuggestedServiceName(document, location);
|
||||
expect(suggestions).toEqual(['KPN', 'Internet & TV', 'kpn.nl']);
|
||||
});
|
||||
|
||||
it('should handle empty titles by returning only domain', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
'',
|
||||
'https://www.voorbeeld.nl'
|
||||
);
|
||||
const suggestions = FormDetector.getSuggestedServiceName(document, location);
|
||||
expect(suggestions).toEqual(['voorbeeld.nl']);
|
||||
});
|
||||
|
||||
it('should handle titles with only Dutch stop words by returning only domain', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
'Je in op de',
|
||||
'https://www.voorbeeld.nl'
|
||||
);
|
||||
const suggestions = FormDetector.getSuggestedServiceName(document, location);
|
||||
expect(suggestions).toEqual(['voorbeeld.nl']);
|
||||
});
|
||||
});
|
||||
@@ -40,6 +40,34 @@ export const createTestDom = (htmlFile: string) : JSDOM => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a test document with the specified title and URL.
|
||||
* This is used for testing service name extraction.
|
||||
*/
|
||||
export const createTestDocument = (title: string, url: string) : { document: Document, location: Location } => {
|
||||
const dom = createTestDom('empty.html');
|
||||
const document = dom.window.document;
|
||||
|
||||
// Set the title
|
||||
document.title = title;
|
||||
|
||||
// Create a proper Location object
|
||||
const location = {
|
||||
href: url,
|
||||
origin: new URL(url).origin,
|
||||
protocol: new URL(url).protocol,
|
||||
host: new URL(url).host,
|
||||
hostname: new URL(url).hostname,
|
||||
port: new URL(url).port,
|
||||
pathname: new URL(url).pathname,
|
||||
search: new URL(url).search,
|
||||
hash: new URL(url).hash,
|
||||
ancestorOrigins: {} as DOMStringList,
|
||||
} as Location;
|
||||
|
||||
return { document, location };
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to test field detection
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<!--
|
||||
Apple ID login form where the label is a sibling element of the input field without proper label[for] attribute.
|
||||
The element here should be detected as an email field.
|
||||
-->
|
||||
<div>
|
||||
<div class=" form-cell-wrapper form-textbox ">
|
||||
<input type="text" id="account_name_text_field" can-field="accountName" aria-labelledby="apple_id_field_label" autocorrect="off" autocapitalize="off" aria-required="true" required="required" spellcheck="false" ($focus)="appleIdFocusHandler($element)" ($blur)="appleIdBlurHandler()" class="force-ltr form-textbox-input form-textbox-entered " autocomplete="false" aria-invalid="false">
|
||||
<span aria-hidden="true" id="apple_id_field_label" class=" form-textbox-label form-label-flyout">
|
||||
Email or Phone Number
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -7,7 +7,7 @@ export default defineConfig({
|
||||
manifest: {
|
||||
name: "AliasVault",
|
||||
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
|
||||
version: "0.16.0",
|
||||
version: "0.16.2",
|
||||
content_security_policy: {
|
||||
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
|
||||
},
|
||||
|
||||
@@ -239,9 +239,9 @@ GEM
|
||||
minitest (5.25.1)
|
||||
net-http (0.5.0)
|
||||
uri
|
||||
nokogiri (1.18.4-x86_64-linux-gnu)
|
||||
nokogiri (1.18.8-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.4-x86_64-linux-musl)
|
||||
nokogiri (1.18.8-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
octokit (4.25.1)
|
||||
faraday (>= 1, < 3)
|
||||
|
||||
@@ -105,9 +105,11 @@ The following websites have been known to cause issues in the past (but should b
|
||||
|
||||
| Website | Reason |
|
||||
| --- | --- |
|
||||
| https://www.paprika-shopping.nl/nieuwsbrief/newsletter-register-landing.html | Popup CSS style conflicts |
|
||||
| https://bloshing.com/inschrijven-nieuwsbrief | Popup CSS style conflicts |
|
||||
| https://gamefaqs.gamespot.com/user | Popup buttons not working |
|
||||
| https://news.ycombinator.com/login?goto=news | Popup and client favicon not showing due to SVG format |
|
||||
| https://vault.bitwarden.com/#/login | Autofill password not detected (input not long enough), manually typing in works |
|
||||
| https://login.microsoftonline.com/ | Password gets reset after autofill |
|
||||
| [Paprika Shopping](https://www.paprika-shopping.nl/nieuwsbrief/newsletter-register-landing.html) | Popup CSS style conflicts |
|
||||
| [Bloshing](https://bloshing.com/inschrijven-nieuwsbrief) | Popup CSS style conflicts |
|
||||
| [GameFAQs](https://gamefaqs.gamespot.com/user) | Popup buttons not working |
|
||||
| [Hacker News](https://news.ycombinator.com/login?goto=news) | Popup and client favicon not showing due to SVG format |
|
||||
| [Bitwarden](https://vault.bitwarden.com/#/login) | Autofill password not detected (input not long enough), manually typing in works |
|
||||
| [Microsoft Online](https://login.microsoftonline.com/) | Password gets reset after autofill |
|
||||
| [ING Bank](https://mijn.ing.nl/login/) | Autofill doesn't detect input fields and AliasVault autofill icon placement is off |
|
||||
| [GitHub Issues](https://github.com/lanedirt/AliasVault/issues) | The "New issue -> Blank Issue" title field causes the autofill to trigger because of a parent form (outside of the role=modal div) |
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazor-ApexCharts" Version="5.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.3">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<DataAnnotationsValidator/>
|
||||
<div>
|
||||
<label asp-for="Input.UserName" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your username</label>
|
||||
<InputTextField id="username" @bind-Value="Input.UserName" placeholder="username" />
|
||||
<InputTextField id="username" @bind-Value="Input.UserName" type="text" placeholder="username" />
|
||||
<ValidationMessage For="() => Input.UserName"/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -49,11 +49,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.3" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.4" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.4" />
|
||||
<PackageReference Include="SpamOK.PasswordGenerator" Version="1.1.0" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -82,13 +82,13 @@ else
|
||||
</h2>
|
||||
|
||||
<FullScreenLoadingIndicator @ref="_loadingIndicator"/>
|
||||
<ServerValidationErrors @ref="_serverValidationErrors"/>
|
||||
|
||||
<EditForm Model="_loginModel" OnValidSubmit="HandleLogin" class="mt-8 space-y-6">
|
||||
<EditForm Model="_loginModel" OnValidSubmit="HandleLogin" class="mt-4 space-y-6">
|
||||
<ServerValidationErrors @ref="_serverValidationErrors"/>
|
||||
<DataAnnotationsValidator/>
|
||||
<div>
|
||||
<label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your username or email</label>
|
||||
<InputTextField id="email" @bind-Value="_loginModel.Username" placeholder="name / name@company.com"/>
|
||||
<InputTextField id="email" @bind-Value="_loginModel.Username" type="text" placeholder="name / name@company.com"/>
|
||||
<ValidationMessage For="() => _loginModel.Username"/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<DataAnnotationsValidator/>
|
||||
<div>
|
||||
<label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your username or email</label>
|
||||
<InputTextField id="email" @bind-Value="_registerModel.Username" placeholder="name / name@company.com" />
|
||||
<InputTextField id="email" @bind-Value="_registerModel.Username" type="text" placeholder="name / name@company.com" />
|
||||
<ValidationMessage For="() => _registerModel.Username"/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
else if (IsWebAuthnLoading) {
|
||||
<BoldLoadingIndicator />
|
||||
<p class="text-center font-normal text-gray-500 dark:text-gray-400">
|
||||
<p class="mt-6 text-center font-normal text-gray-500 dark:text-gray-400">
|
||||
Logging in with WebAuthn...
|
||||
</p>
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
}
|
||||
</td>
|
||||
<td class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400">
|
||||
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@mail.DateSystem</span>
|
||||
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@mail.DateSystem.ToString("yyyy-MM-dd")</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -125,7 +125,7 @@
|
||||
public void OnVisibilityChange(bool isVisible)
|
||||
{
|
||||
_isPageVisible = isVisible;
|
||||
|
||||
|
||||
if (isVisible && DbService.Settings.AutoEmailRefresh)
|
||||
{
|
||||
// Start polling if visible and auto-refresh is enabled
|
||||
@@ -136,7 +136,7 @@
|
||||
// Stop polling if hidden
|
||||
StopPolling();
|
||||
}
|
||||
|
||||
|
||||
// If becoming visible, do an immediate refresh
|
||||
if (isVisible)
|
||||
{
|
||||
@@ -150,13 +150,13 @@
|
||||
if (_pollingCts != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_pollingCts = new CancellationTokenSource();
|
||||
|
||||
|
||||
// Start polling task
|
||||
_ = PollForEmails(_pollingCts.Token);
|
||||
}
|
||||
|
||||
|
||||
private void StopPolling()
|
||||
{
|
||||
if (_pollingCts != null)
|
||||
@@ -166,7 +166,7 @@
|
||||
_pollingCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task PollForEmails(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
@@ -217,7 +217,7 @@
|
||||
{
|
||||
// Stop polling
|
||||
StopPolling();
|
||||
|
||||
|
||||
// Unregister the visibility callback using the same reference
|
||||
if (_dotNetRef != null)
|
||||
{
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
<p>@DuplicateCredentialsCount duplicate credential(s) were found and will not be imported.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@if (ImportedCredentials.Count == 0)
|
||||
{
|
||||
<div class="p-4 mb-4 text-amber-700 bg-amber-100 rounded-lg dark:bg-amber-800/30 dark:text-amber-300" role="alert">
|
||||
@@ -369,7 +369,7 @@
|
||||
ImportError = null;
|
||||
ImportSuccessMessage = null;
|
||||
StateHasChanged();
|
||||
|
||||
|
||||
// Let UI update to start showing the loading indicator
|
||||
await Task.Delay(50);
|
||||
}
|
||||
@@ -479,6 +479,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.File.Name.EndsWith(".zip"))
|
||||
{
|
||||
ImportError = $"Please unzip the {ServiceName} export file before importing, please read the instructions below for more information.";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IsImporting = true;
|
||||
@@ -500,10 +506,10 @@
|
||||
await Task.WhenAll(processingTask, delayTask);
|
||||
|
||||
ImportedCredentials = await processingTask;
|
||||
|
||||
|
||||
// Detect and remove duplicates before showing the preview
|
||||
await DetectAndRemoveDuplicates();
|
||||
|
||||
|
||||
CurrentStep = ImportStep.Preview;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -532,7 +538,7 @@
|
||||
)).ToList();
|
||||
|
||||
DuplicateCredentialsCount = duplicates.Count;
|
||||
|
||||
|
||||
// Remove duplicates from the import list
|
||||
ImportedCredentials = ImportedCredentials.Except(duplicates).ToList();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
@using AliasVault.ImportExport.Models
|
||||
@using AliasVault.ImportExport.Importers
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject GlobalNotificationService GlobalNotificationService
|
||||
@inject ILogger<ImportServiceBitwarden> Logger
|
||||
|
||||
<ImportServiceCard
|
||||
ServiceName="Dashlane"
|
||||
Description="Import passwords from your Dashlane account"
|
||||
LogoUrl="img/importers/dashlane.svg"
|
||||
ProcessFileCallback="ProcessFile">
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">In order to import your Dashlane passwords, you need to export it as a CSV file. You can do this by logging into your Dashlane account, going to the 'Account' > 'Settings' menu and selecting 'Export to CSV'.</p>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">Note: the .zip file you download will contain a "credentials.csv" file. You need to unzip the archive first, and then upload the "credentials.csv" CSV file below.</p>
|
||||
</ImportServiceCard>
|
||||
|
||||
@code {
|
||||
private static async Task<List<ImportedCredential>> ProcessFile(string fileContents)
|
||||
{
|
||||
return await DashlaneImporter.ImportFromCsvAsync(fileContents);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
@using AliasVault.ImportExport.Models
|
||||
@using AliasVault.ImportExport.Importers
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject GlobalNotificationService GlobalNotificationService
|
||||
@inject ILogger<ImportServiceProtonPass> Logger
|
||||
|
||||
<ImportServiceCard
|
||||
ServiceName="Proton Pass"
|
||||
Description="Import passwords from Proton Pass"
|
||||
LogoUrl="img/importers/protonpass.svg"
|
||||
ProcessFileCallback="ProcessFile">
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">In order to import your Proton Pass passwords, you need to export it as a CSV file. You can do this by logging into Proton Pass (web), clicking on the 'Settings' menu > 'Export' > 'File format: CSV'. Then click on 'Export'.</p>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">Once you have exported the file, you can upload it below.</p>
|
||||
</ImportServiceCard>
|
||||
|
||||
@code {
|
||||
private static async Task<List<ImportedCredential>> ProcessFile(string fileContents)
|
||||
{
|
||||
return await ProtonPassImporter.ImportFromCsvAsync(fileContents);
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,11 @@
|
||||
<ImportService1Password />
|
||||
<ImportServiceBitwarden />
|
||||
<ImportServiceChrome />
|
||||
<ImportServiceDashlane />
|
||||
<ImportServiceFirefox />
|
||||
<ImportServiceKeePass />
|
||||
<ImportServiceKeePassXC />
|
||||
<ImportServiceProtonPass />
|
||||
<ImportServiceStrongbox />
|
||||
<ImportServiceAliasVault />
|
||||
</div>
|
||||
|
||||
@@ -104,30 +104,39 @@ else
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
// Check on server if 2FA is enabled
|
||||
// Get the current password ephemeral and salt from the server
|
||||
// which is required to confirm the current password.
|
||||
if (firstRender)
|
||||
{
|
||||
// Get the QR code and secret for the authenticator app.
|
||||
var response = await Http.GetFromJsonAsync<PasswordChangeInitiateResponse>("v1/Auth/change-password/initiate");
|
||||
|
||||
if (response == null)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("Failed to initiate the password change process.", true);
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentServerEphemeral = response.ServerEphemeral;
|
||||
CurrentSalt = response.Salt;
|
||||
CurrentEncryptionType = response.EncryptionType;
|
||||
CurrentEncryptionSettings = response.EncryptionSettings;
|
||||
await GetCurrentPasswordEphemeralAndSalt();
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current password ephemeral and salt from the server which
|
||||
/// is required to confirm the current password.
|
||||
/// </summary>
|
||||
private async Task GetCurrentPasswordEphemeralAndSalt()
|
||||
{
|
||||
var response = await Http.GetFromJsonAsync<PasswordChangeInitiateResponse>("v1/Auth/change-password/initiate");
|
||||
|
||||
if (response == null)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("Failed to initiate the password change process.", true);
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentServerEphemeral = response.ServerEphemeral;
|
||||
CurrentSalt = response.Salt;
|
||||
CurrentEncryptionType = response.EncryptionType;
|
||||
CurrentEncryptionSettings = response.EncryptionSettings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initiates the password change process.
|
||||
/// </summary>
|
||||
@@ -215,6 +224,10 @@ else
|
||||
// Set success message.
|
||||
GlobalNotificationService.AddSuccessMessage("Password changed successfully.", true);
|
||||
|
||||
// Get the new password ephemeral and salt from the server, which is required if the usre
|
||||
// wants to change the password again.
|
||||
await GetCurrentPasswordEphemeralAndSalt();
|
||||
|
||||
GlobalLoadingSpinner.Hide();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
5
src/AliasVault.Client/wwwroot/img/importers/dashlane.svg
Normal file
5
src/AliasVault.Client/wwwroot/img/importers/dashlane.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="512" cy="512" r="512" style="fill:#10353e"/>
|
||||
<path d="m544.7 458.9 53.8 19.8c8.7 3.1 19.3-1 19.3-7.5V334.9c0-3.1-2.6-6-6.8-7.5l-53.8-19.8c-8.7-3.1-19.3 1-19.3 7.5v136.4c0 3.1 2.6 5.9 6.8 7.4m0 244.8 53.8 19.8c8.7 3.1 19.3-1 19.3-7.5V579.7c0-3.1-2.6-6-6.8-7.5l-53.8-19.8c-8.7-3.1-19.3 1-19.3 7.5v136.4c0 3.1 2.6 6 6.8 7.4M445 413.3l53.8 19.8c8.7 3.1 19.3-1 19.3-7.5V277.3c0-3.1-2.6-6-6.8-7.5L457.5 250c-8.7-3.1-19.3 1-19.3 7.5v148.4c0 3.1 2.6 5.9 6.8 7.4m0 326.9 53.8 19.8c8.7 3.1 19.3-1 19.3-7.5v-148c0-3.1-2.6-6-6.8-7.5l-53.8-19.8c-8.7-3.1-19.3 1-19.3 7.5v148c0 3.1 2.6 6 6.8 7.5m-26.6-457.4c0-3.1-2.6-6-6.8-7.5l-53.8-19.8c-8.7-3.1-19.3 1-19.3 7.5v464.1c0 3.1 2.6 6 6.8 7.5l53.8 19.8c8.7 3.1 19.3-1 19.3-7.5V282.8zM710.7 406l-53.8-19.8c-8.7-3.1-19.3 1-19.3 7.5v224c0 3.1 2.6 6 6.8 7.5l53.8 19.8c8.7 3.1 19.3-1 19.3-7.5v-224c0-3.1-2.6-6-6.8-7.5" style="fill:#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
68
src/AliasVault.Client/wwwroot/img/importers/protonpass.svg
Normal file
68
src/AliasVault.Client/wwwroot/img/importers/protonpass.svg
Normal file
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Слой_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="500px" height="500px" viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{clip-path:url(#SVGID_00000160880367675937928300000013114190071515040655_);}
|
||||
.st1{fill:url(#SVGID_00000158020781677298841590000014386279406831169436_);}
|
||||
.st2{fill:url(#SVGID_00000038389245457700614160000015930090567954731184_);}
|
||||
.st3{fill:url(#SVGID_00000074402669088272121800000007567021010918974869_);}
|
||||
</style>
|
||||
<g>
|
||||
<defs>
|
||||
<rect id="SVGID_1_" width="500" height="500"/>
|
||||
</defs>
|
||||
<clipPath id="SVGID_00000097476224578929584770000017210037084984045718_">
|
||||
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
|
||||
</clipPath>
|
||||
<g style="clip-path:url(#SVGID_00000097476224578929584770000017210037084984045718_);">
|
||||
|
||||
<radialGradient id="SVGID_00000047755579261974386440000006872451468367864226_" cx="148.4036" cy="350.2411" r="4.717" gradientTransform="matrix(46.7033 -75.1155 -117.4926 -73.0513 34370.6797 37242.4727)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#FFD580"/>
|
||||
<stop offset="9.375000e-02" style="stop-color:#F6C592"/>
|
||||
<stop offset="0.205" style="stop-color:#EBB6A2"/>
|
||||
<stop offset="0.3245" style="stop-color:#DFA5AF"/>
|
||||
<stop offset="0.4288" style="stop-color:#D397BE"/>
|
||||
<stop offset="0.5337" style="stop-color:#C486CB"/>
|
||||
<stop offset="0.6488" style="stop-color:#B578D9"/>
|
||||
<stop offset="0.7713" style="stop-color:#A166E5"/>
|
||||
<stop offset="0.8913" style="stop-color:#8B57F2"/>
|
||||
<stop offset="1" style="stop-color:#704CFF"/>
|
||||
</radialGradient>
|
||||
<path style="fill:url(#SVGID_00000047755579261974386440000006872451468367864226_);" d="M150.4,63.1
|
||||
c34.9-34.9,52.3-52.3,72.4-58.8c17.7-5.7,36.7-5.7,54.4,0c20.1,6.5,37.5,24,72.4,58.8l87.2,87.1c34.9,34.9,52.3,52.3,58.9,72.4
|
||||
c5.8,17.7,5.8,36.7,0,54.4c-6.5,20.1-24,37.5-58.9,72.4l-87.2,87.1c-34.9,34.9-52.3,52.3-72.4,58.8c-17.7,5.7-36.7,5.7-54.4,0
|
||||
c-20.1-6.5-37.5-24-72.4-58.8L134,418.2c-9.9-11.1-14.9-16.7-18.4-23c-3.1-5.6-5.4-11.6-6.8-17.9c-1.6-7.1-1.6-14.5-1.6-29.4
|
||||
V151.8c0-14.9,0-22.3,1.6-29.4c1.4-6.3,3.7-12.3,6.8-17.9c3.5-6.3,8.5-11.9,18.4-23L150.4,63.1z"/>
|
||||
|
||||
<linearGradient id="SVGID_00000017511077749203986220000003166735930388103090_" gradientUnits="userSpaceOnUse" x1="234.6024" y1="617.7536" x2="331.7387" y2="24.506" gradientTransform="matrix(1 0 0 -1 0 502)">
|
||||
<stop offset="0" style="stop-color:#6D4AFF"/>
|
||||
<stop offset="0.392" style="stop-color:#B39FFB;stop-opacity:0.978"/>
|
||||
<stop offset="1" style="stop-color:#FFE8DB;stop-opacity:0.8"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000017511077749203986220000003166735930388103090_);" d="M150.4,63.1
|
||||
c34.9-34.9,52.3-52.3,72.4-58.8c17.7-5.7,36.7-5.7,54.4,0c20.1,6.5,37.5,24,72.4,58.8l87.2,87.1c34.9,34.9,52.3,52.3,58.9,72.4
|
||||
c5.8,17.7,5.8,36.7,0,54.4c-6.5,20.1-24,37.5-58.9,72.4l-87.2,87.1c-34.9,34.9-52.3,52.3-72.4,58.8c-17.7,5.7-36.7,5.7-54.4,0
|
||||
c-20.1-6.5-37.5-24-72.4-58.8L134,418.2c-9.9-11.1-14.9-16.7-18.4-23c-3.1-5.6-5.4-11.6-6.8-17.9c-1.6-7.1-1.6-14.5-1.6-29.4
|
||||
V151.8c0-14.9,0-22.3,1.6-29.4c1.4-6.3,3.7-12.3,6.8-17.9c3.5-6.3,8.5-11.9,18.4-23L150.4,63.1z"/>
|
||||
|
||||
<radialGradient id="SVGID_00000089555210451034675070000005649055941905357209_" cx="148.0355" cy="350.4669" r="4.717" gradientTransform="matrix(37.5657 -60.419 -94.5046 -58.7585 27673.916 29995.748)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#FFD580"/>
|
||||
<stop offset="9.375000e-02" style="stop-color:#F6C592"/>
|
||||
<stop offset="0.205" style="stop-color:#EBB6A2"/>
|
||||
<stop offset="0.3245" style="stop-color:#DFA5AF"/>
|
||||
<stop offset="0.4288" style="stop-color:#D397BE"/>
|
||||
<stop offset="0.5337" style="stop-color:#C486CB"/>
|
||||
<stop offset="0.6488" style="stop-color:#B578D9"/>
|
||||
<stop offset="0.7713" style="stop-color:#A166E5"/>
|
||||
<stop offset="0.8913" style="stop-color:#8B57F2"/>
|
||||
<stop offset="1" style="stop-color:#704CFF"/>
|
||||
</radialGradient>
|
||||
<path style="fill:url(#SVGID_00000089555210451034675070000005649055941905357209_);" d="M144.1,69.4
|
||||
c17.4-17.4,26.2-26.1,36.2-29.4c8.8-2.9,18.4-2.9,27.2,0c10.1,3.3,18.8,12,36.2,29.4l130.8,130.7c17.4,17.4,26.2,26.1,29.4,36.2
|
||||
c2.9,8.8,2.9,18.4,0,27.2c-3.3,10.1-12,18.8-29.4,36.2L243.8,430.4c-17.4,17.4-26.2,26.1-36.2,29.4c-8.8,2.9-18.4,2.9-27.2,0
|
||||
c-10.1-3.3-18.8-12-36.2-29.4l-81-80.9c-34.9-34.9-52.3-52.3-58.9-72.4c-5.7-17.7-5.7-36.7,0-54.4c6.5-20.1,24-37.5,58.9-72.4
|
||||
L144.1,69.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -18,16 +18,16 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
@@ -17,20 +17,20 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.2" />
|
||||
<PackageReference Include="MimeKit" Version="4.11.0" />
|
||||
<PackageReference Include="NUglify" Version="1.21.13" />
|
||||
<PackageReference Include="NUglify" Version="1.21.15" />
|
||||
<PackageReference Include="SmtpServer" Version="10.0.1" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.2" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.4" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -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 minimum supported AliasVault client version. Normally the minimum client version is the same
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -30,11 +30,11 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="4.3.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.6.0">
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.7.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<PackageReference Include="MailKit" Version="4.11.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="4.3.2"/>
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.6.0"/>
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.7.0"/>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
@@ -34,7 +34,7 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="4.3.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.6.0">
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.7.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
@@ -68,6 +68,8 @@
|
||||
<EmbeddedResource Include="TestData\Exports\keepass.csv" />
|
||||
<EmbeddedResource Include="TestData\Exports\keepassxc.csv" />
|
||||
<EmbeddedResource Include="TestData\Exports\1password_8.csv" />
|
||||
<EmbeddedResource Include="TestData\Exports\protonpass.csv" />
|
||||
<EmbeddedResource Include="TestData\Exports\dashlane.csv" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
username,username2,username3,title,password,note,url,category,otpUrl
|
||||
Test username,,,Test,password123,,https://Test,,
|
||||
googleuser,,,Google,googlepassword,,https://www.google.com,,
|
||||
testusername,testusernamealternative,,Local,testpassword,testnote,https://www.testwebsite.local,,
|
||||
|
@@ -0,0 +1,5 @@
|
||||
type,name,url,email,username,password,note,totp,createTime,modifyTime,vault
|
||||
login,Test proton 1,https://www.website.com/,,user1,pass1,,otpauth://totp/Strongbox?secret=PLW4SB3PQ7MKVXY2MXF4NEXS6Y&algorithm=SHA1&digits=6&period=30,1744362003,1744362003,Personal
|
||||
alias,Test alias,,testalias.gating981@passinbox.com,,,,,1744362031,1744362052,Personal
|
||||
login,Test proton2,,,testuser2,testpassword2,,,1744362088,1744362088,Personal
|
||||
login,testwithoutpass,,,testuser,,,,1744362100,1744362110,Personal
|
||||
|
@@ -355,4 +355,103 @@ public class ImportExportTests
|
||||
Assert.That(sampleEntry2.TwoFactorSecret, Is.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case for importing credentials from ProtonPass CSV and ensuring all values are present.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
public async Task ImportCredentialsFromProtonPassCsv()
|
||||
{
|
||||
// Arrange
|
||||
var fileContent = await ResourceReaderUtility.ReadEmbeddedResourceStringAsync("AliasVault.UnitTests.TestData.Exports.protonpass.csv");
|
||||
|
||||
// Act
|
||||
var importedCredentials = await ProtonPassImporter.ImportFromCsvAsync(fileContent);
|
||||
|
||||
// Assert
|
||||
Assert.That(importedCredentials, Has.Count.EqualTo(4));
|
||||
|
||||
// Test specific entries
|
||||
var testProton1Credential = importedCredentials.First(c => c.ServiceName == "Test proton 1");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(testProton1Credential.ServiceName, Is.EqualTo("Test proton 1"));
|
||||
Assert.That(testProton1Credential.ServiceUrl, Is.EqualTo("https://www.website.com/"));
|
||||
Assert.That(testProton1Credential.Username, Is.EqualTo("user1"));
|
||||
Assert.That(testProton1Credential.Password, Is.EqualTo("pass1"));
|
||||
Assert.That(testProton1Credential.TwoFactorSecret, Is.EqualTo("otpauth://totp/Strongbox?secret=PLW4SB3PQ7MKVXY2MXF4NEXS6Y&algorithm=SHA1&digits=6&period=30"));
|
||||
});
|
||||
|
||||
var testProton2Credential = importedCredentials.First(c => c.ServiceName == "Test proton2");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(testProton2Credential.ServiceName, Is.EqualTo("Test proton2"));
|
||||
Assert.That(testProton2Credential.Username, Is.EqualTo("testuser2"));
|
||||
Assert.That(testProton2Credential.Password, Is.EqualTo("testpassword2"));
|
||||
});
|
||||
|
||||
var testWithoutPassCredential = importedCredentials.First(c => c.ServiceName == "testwithoutpass");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(testWithoutPassCredential.ServiceName, Is.EqualTo("testwithoutpass"));
|
||||
Assert.That(testWithoutPassCredential.Username, Is.EqualTo("testuser"));
|
||||
Assert.That(testWithoutPassCredential.Password, Is.Empty);
|
||||
});
|
||||
|
||||
var testWithEmailCredential = importedCredentials.First(c => c.ServiceName == "Test alias");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(testWithEmailCredential.ServiceName, Is.EqualTo("Test alias"));
|
||||
Assert.That(testWithEmailCredential.Email, Is.EqualTo("testalias.gating981@passinbox.com"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case for importing credentials from Dashlane CSV and ensuring all values are present.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
public async Task ImportCredentialsFromDashlaneCsv()
|
||||
{
|
||||
// Arrange
|
||||
var fileContent = await ResourceReaderUtility.ReadEmbeddedResourceStringAsync("AliasVault.UnitTests.TestData.Exports.dashlane.csv");
|
||||
|
||||
// Act
|
||||
var importedCredentials = await DashlaneImporter.ImportFromCsvAsync(fileContent);
|
||||
|
||||
// Assert
|
||||
Assert.That(importedCredentials, Has.Count.EqualTo(3));
|
||||
|
||||
// Test specific entries
|
||||
var testCredential = importedCredentials.First(c => c.ServiceName == "Test");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(testCredential.ServiceName, Is.EqualTo("Test"));
|
||||
Assert.That(testCredential.ServiceUrl, Is.EqualTo("https://Test"));
|
||||
Assert.That(testCredential.Username, Is.EqualTo("Test username"));
|
||||
Assert.That(testCredential.Password, Is.EqualTo("password123"));
|
||||
Assert.That(testCredential.Notes, Is.Null);
|
||||
});
|
||||
|
||||
var googleCredential = importedCredentials.First(c => c.ServiceName == "Google");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(googleCredential.ServiceName, Is.EqualTo("Google"));
|
||||
Assert.That(googleCredential.ServiceUrl, Is.EqualTo("https://www.google.com"));
|
||||
Assert.That(googleCredential.Username, Is.EqualTo("googleuser"));
|
||||
Assert.That(googleCredential.Password, Is.EqualTo("googlepassword"));
|
||||
Assert.That(googleCredential.Notes, Is.Null);
|
||||
});
|
||||
|
||||
var localCredential = importedCredentials.First(c => c.ServiceName == "Local");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(localCredential.ServiceName, Is.EqualTo("Local"));
|
||||
Assert.That(localCredential.ServiceUrl, Is.EqualTo("https://www.testwebsite.local"));
|
||||
Assert.That(localCredential.Username, Is.EqualTo("testusername"));
|
||||
Assert.That(localCredential.Password, Is.EqualTo("testpassword"));
|
||||
Assert.That(localCredential.Notes, Is.EqualTo("testnote\nAlternative username 1: testusernamealternative"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
@@ -22,14 +22,14 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
|
||||
<PackageReference Include="SkiaSharp" Version="3.116.1" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -114,7 +114,7 @@ public static class FaviconExtractor
|
||||
var defaultFavicon = new HtmlNode(HtmlNodeType.Element, htmlDoc, 0);
|
||||
defaultFavicon.Attributes.Add("href", $"{uri.GetLeftPart(UriPartial.Authority)}/favicon.ico");
|
||||
|
||||
return
|
||||
HtmlNodeCollection?[] nodeArray =
|
||||
[
|
||||
htmlDoc.DocumentNode.SelectNodes("//link[@rel='icon' and @type='image/svg+xml']"),
|
||||
htmlDoc.DocumentNode.SelectNodes("//link[@rel='icon' and @sizes='96x96']"),
|
||||
@@ -126,6 +126,9 @@ public static class FaviconExtractor
|
||||
htmlDoc.DocumentNode.SelectNodes("//link[@rel='icon' or @rel='shortcut icon']"),
|
||||
new HtmlNodeCollection(htmlDoc.DocumentNode) { defaultFavicon },
|
||||
];
|
||||
|
||||
// Filter node array to only return non-null values and cast to non-nullable array
|
||||
return nodeArray.Where(x => x != null).Cast<HtmlNodeCollection>().ToArray();
|
||||
}
|
||||
|
||||
private static async Task<byte[]?> TryGetFaviconAsync(HttpClient client, Uri uri)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="DashlaneImporter.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.ImportExport.Importers;
|
||||
|
||||
using AliasVault.ImportExport.Models;
|
||||
using AliasVault.ImportExport.Models.Imports;
|
||||
using CsvHelper;
|
||||
using CsvHelper.Configuration;
|
||||
using System.Globalization;
|
||||
|
||||
/// <summary>
|
||||
/// Imports credentials from Dashlane.
|
||||
/// </summary>
|
||||
public static class DashlaneImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Imports Dashlane CSV file and converts contents to list of ImportedCredential model objects.
|
||||
/// </summary>
|
||||
/// <param name="fileContent">The content of the CSV file.</param>
|
||||
/// <returns>The imported list of ImportedCredential objects.</returns>
|
||||
public static async Task<List<ImportedCredential>> ImportFromCsvAsync(string fileContent)
|
||||
{
|
||||
using var reader = new StringReader(fileContent);
|
||||
using var csv = new CsvReader(reader, new CsvConfiguration(CultureInfo.InvariantCulture));
|
||||
|
||||
var credentials = new List<ImportedCredential>();
|
||||
await foreach (var record in csv.GetRecordsAsync<DashlaneCsvRecord>())
|
||||
{
|
||||
var credential = new ImportedCredential
|
||||
{
|
||||
ServiceName = record.Title,
|
||||
ServiceUrl = record.URL,
|
||||
Username = record.Username,
|
||||
Password = record.Password,
|
||||
TwoFactorSecret = record.OTPUrl,
|
||||
Notes = BuildNotes(record)
|
||||
};
|
||||
|
||||
credentials.Add(credential);
|
||||
}
|
||||
|
||||
if (credentials.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No records found in the CSV file.");
|
||||
}
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
private static string? BuildNotes(DashlaneCsvRecord record)
|
||||
{
|
||||
var notes = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(record.Note))
|
||||
{
|
||||
notes.Add(record.Note);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(record.Username2))
|
||||
{
|
||||
notes.Add($"Alternative username 1: {record.Username2}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(record.Username3))
|
||||
{
|
||||
notes.Add($"Alternative username 2: {record.Username3}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(record.Category))
|
||||
{
|
||||
notes.Add($"Category: {record.Category}");
|
||||
}
|
||||
|
||||
return notes.Count > 0 ? string.Join(Environment.NewLine, notes) : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ProtonPassImporter.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.ImportExport.Importers;
|
||||
|
||||
using AliasVault.ImportExport.Models;
|
||||
using AliasVault.ImportExport.Models.Imports;
|
||||
using CsvHelper;
|
||||
using CsvHelper.Configuration;
|
||||
using System.Globalization;
|
||||
|
||||
/// <summary>
|
||||
/// Imports credentials from ProtonPass.
|
||||
/// </summary>
|
||||
public static class ProtonPassImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Imports ProtonPass CSV file and converts contents to list of ImportedCredential model objects.
|
||||
/// </summary>
|
||||
/// <param name="fileContent">The content of the CSV file.</param>
|
||||
/// <returns>The imported list of ImportedCredential objects.</returns>
|
||||
public static async Task<List<ImportedCredential>> ImportFromCsvAsync(string fileContent)
|
||||
{
|
||||
using var reader = new StringReader(fileContent);
|
||||
using var csv = new CsvReader(reader, new CsvConfiguration(CultureInfo.InvariantCulture));
|
||||
|
||||
var credentials = new List<ImportedCredential>();
|
||||
await foreach (var record in csv.GetRecordsAsync<ProtonPassCsvRecord>())
|
||||
{
|
||||
var credential = new ImportedCredential
|
||||
{
|
||||
ServiceName = record.Name,
|
||||
ServiceUrl = record.Url,
|
||||
Email = record.Email,
|
||||
Username = record.Username,
|
||||
Password = record.Password,
|
||||
Notes = record.Note,
|
||||
TwoFactorSecret = record.Totp,
|
||||
};
|
||||
|
||||
credentials.Add(credential);
|
||||
}
|
||||
|
||||
if (credentials.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No records found in the CSV file.");
|
||||
}
|
||||
|
||||
return credentials;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="DashlaneCsvRecord.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
using AliasVault.ImportExport.Converters;
|
||||
using CsvHelper.Configuration.Attributes;
|
||||
|
||||
namespace AliasVault.ImportExport.Models.Imports;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Dashlane CSV record that is being imported from a Dashlane CSV export file.
|
||||
/// </summary>
|
||||
public class DashlaneCsvRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the primary username.
|
||||
/// </summary>
|
||||
[Name("username")]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the second username.
|
||||
/// </summary>
|
||||
[Name("username2")]
|
||||
public string? Username2 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the third username.
|
||||
/// </summary>
|
||||
[Name("username3")]
|
||||
public string? Username3 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title/service name.
|
||||
/// </summary>
|
||||
[Name("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
[Name("password")]
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets any additional notes.
|
||||
/// </summary>
|
||||
[Name("note")]
|
||||
public string? Note { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the service URL.
|
||||
/// </summary>
|
||||
[Name("url")]
|
||||
public string? URL { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the category.
|
||||
/// </summary>
|
||||
[Name("category")]
|
||||
public string? Category { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the OTP URL.
|
||||
/// </summary>
|
||||
[Name("otpUrl")]
|
||||
public string? OTPUrl { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ProtonPassCsvRecord.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.ImportExport.Models.Imports;
|
||||
|
||||
using CsvHelper.Configuration.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a ProtonPass CSV record that is being imported from a ProtonPass CSV export file.
|
||||
/// </summary>
|
||||
public class ProtonPassCsvRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the item (e.g., login, alias).
|
||||
/// </summary>
|
||||
[Name("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the item.
|
||||
/// </summary>
|
||||
[Name("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL of the item.
|
||||
/// </summary>
|
||||
[Name("url")]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email of the item.
|
||||
/// </summary>
|
||||
[Name("email")]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the username of the item.
|
||||
/// </summary>
|
||||
[Name("username")]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password of the item.
|
||||
/// </summary>
|
||||
[Name("password")]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets any additional notes.
|
||||
/// </summary>
|
||||
[Name("note")]
|
||||
public string? Note { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TOTP (Time-based One-Time Password) URI.
|
||||
/// </summary>
|
||||
[Name("totp")]
|
||||
public string? Totp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the creation time of the item.
|
||||
/// </summary>
|
||||
[Name("createTime")]
|
||||
public string CreateTime { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the modification time of the item.
|
||||
/// </summary>
|
||||
[Name("modifyTime")]
|
||||
public string ModifyTime { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vault name where the item is stored.
|
||||
/// </summary>
|
||||
[Name("vault")]
|
||||
public string Vault { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -18,13 +18,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
Reference in New Issue
Block a user