Compare commits

...

44 Commits

Author SHA1 Message Date
Leendert de Borst
ab82a63a0a Bump version to 0.16.2 (#818) 2025-05-01 08:57:09 +02:00
dependabot[bot]
82376b696c Bump vite
Bumps the npm_and_yarn group with 1 update in the /browser-extension directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.2.6 to 6.3.4
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-01 08:45:37 +02:00
Leendert de Borst
0c8fc191a6 Update date format in RecentEmails.razor (#815) 2025-04-30 14:41:26 +02:00
Leendert de Borst
b71f0dd2c3 Tweak Login.razor margins (#809) 2025-04-28 18:44:15 +02:00
Leendert de Borst
3617c551e3 Refresh password salt and ephemeral after changing it (#809) 2025-04-28 18:44:15 +02:00
Leendert de Borst
901caa896b Add dashlane importer and unittest (#811) 2025-04-28 18:44:08 +02:00
dependabot[bot]
89534bf78e Bump the npm_and_yarn group across 1 directory with 2 updates
Bumps the npm_and_yarn group with 2 updates in the /browser-extension directory: [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) and [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom).


Updates `react-router` from 7.2.0 to 7.5.2
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.5.2/packages/react-router)

Updates `react-router-dom` from 7.2.0 to 7.5.2
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.5.2/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.5.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: react-router-dom
  dependency-version: 7.5.2
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-25 17:18:01 +02:00
dependabot[bot]
e82595162f Bump nokogiri in /docs in the bundler group across 1 directory
Bumps the bundler group with 1 update in the /docs directory: [nokogiri](https://github.com/sparklemotion/nokogiri).


Updates `nokogiri` from 1.18.4 to 1.18.8
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.18.4...v1.18.8)

---
updated-dependencies:
- dependency-name: nokogiri
  dependency-version: 1.18.8
  dependency-type: indirect
  dependency-group: bundler
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-22 12:37:19 +02:00
Leendert de Borst
93c439e852 Fix nullability warning in FaviconExtractor.cs (#805) 2025-04-21 16:01:22 +02:00
dependabot[bot]
ff08fae579 Bump HtmlAgilityPack from 1.12.0 to 1.12.1
Bumps [HtmlAgilityPack](https://github.com/zzzprojects/html-agility-pack) from 1.12.0 to 1.12.1.
- [Release notes](https://github.com/zzzprojects/html-agility-pack/releases)
- [Commits](https://github.com/zzzprojects/html-agility-pack/compare/v1.12.0...v1.12.1)

---
updated-dependencies:
- dependency-name: HtmlAgilityPack
  dependency-version: 1.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-21 16:01:22 +02:00
Leendert de Borst
5fdcee50d5 Bump version to 0.16.1 (#803) 2025-04-15 18:39:10 +02:00
Leendert de Borst
8526172ec7 Add form detector improvements and tests (#794) 2025-04-15 18:39:10 +02:00
Leendert de Borst
5156988319 Merge pull request #800 from lanedirt/798-browser-extension-make-service-name-extraction-more-accurate
Browser extension make service name extraction more accurate
2025-04-15 18:02:03 +02:00
Leendert de Borst
18d92ecced Add reliable click handler for all autofill popup elements (#797) 2025-04-15 17:07:27 +02:00
Leendert de Borst
0a0bec99b1 Merge branch 'main' into 798-browser-extension-make-service-name-extraction-more-accurate 2025-04-15 17:02:46 +02:00
Leendert de Borst
791f8a758b Update Filter.ts (#801) 2025-04-15 17:00:22 +02:00
Leendert de Borst
3f11e29787 Fix autofill popup z-index visibility (#801) 2025-04-15 17:00:22 +02:00
Leendert de Borst
046d09453a Show email in credential list if username is empty (#801) 2025-04-15 17:00:22 +02:00
Leendert de Borst
1d77d05e7c Improve autofill matching (#801) 2025-04-15 17:00:22 +02:00
Leendert de Borst
22d2e09982 Make browser extension autofill dismiss button more reliable (#797) 2025-04-15 16:59:50 +02:00
Leendert de Borst
8b835a4a77 Remove cancel for sonarcloud runner as it uses pull_request_target 2025-04-15 16:58:28 +02:00
Leendert de Borst
a435305093 Simplify service name to a single input for both modes (#798) 2025-04-15 15:51:18 +02:00
Leendert de Borst
e4f3de927f Show service name suggestions (#798) 2025-04-15 15:34:48 +02:00
Leendert de Borst
1d5c288514 Add service name extraction unit tests (#798) 2025-04-15 12:57:04 +02:00
Leendert de Borst
5d3ad60dee Improve browser extension service name extractor (#798) 2025-04-15 12:56:55 +02:00
Leendert de Borst
c5244b31ec Cancel already running CI jobs on newer commit 2025-04-15 11:34:19 +02:00
Leendert de Borst
a6c7c54592 Add password visibility toggle to browser extension credential create (#793) 2025-04-15 11:24:59 +02:00
Leendert de Borst
bf46c155bd Fix browser extension autofill from causing scrollbars to appear (#794) 2025-04-15 11:24:51 +02:00
Leendert de Borst
d4e5b724ff Make autofill work with more input element variations (#794) 2025-04-15 11:24:51 +02:00
Leendert de Borst
e51219d513 Add explicit type=text for accessibility improvements (#794) 2025-04-15 11:24:51 +02:00
Leendert de Borst
800f015947 Update all .NET dependencies to 9.0.4 (#791) 2025-04-14 20:55:36 +02:00
dependabot[bot]
5f3c36263d Bump Microsoft.AspNetCore.Authorization and Microsoft.AspNetCore.Components.Web
Bumps [Microsoft.AspNetCore.Authorization](https://github.com/dotnet/aspnetcore) and [Microsoft.AspNetCore.Components.Web](https://github.com/dotnet/aspnetcore). These dependencies needed to be updated together.

Updates `Microsoft.AspNetCore.Authorization` from 9.0.3 to 9.0.4
- [Release notes](https://github.com/dotnet/aspnetcore/releases)
- [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md)
- [Commits](https://github.com/dotnet/aspnetcore/compare/v9.0.3...v9.0.4)

Updates `Microsoft.AspNetCore.Components.Web` from 9.0.3 to 9.0.4
- [Release notes](https://github.com/dotnet/aspnetcore/releases)
- [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md)
- [Commits](https://github.com/dotnet/aspnetcore/compare/v9.0.3...v9.0.4)

---
updated-dependencies:
- dependency-name: Microsoft.AspNetCore.Authorization
  dependency-version: 9.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Microsoft.AspNetCore.Components.Web
  dependency-version: 9.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 17:00:32 +02:00
dependabot[bot]
4617d5efc4 Bump Microsoft.AspNetCore.Components.WebAssembly.DevServer
Bumps [Microsoft.AspNetCore.Components.WebAssembly.DevServer](https://github.com/dotnet/aspnetcore) from 9.0.3 to 9.0.4.
- [Release notes](https://github.com/dotnet/aspnetcore/releases)
- [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md)
- [Commits](https://github.com/dotnet/aspnetcore/compare/v9.0.3...v9.0.4)

---
updated-dependencies:
- dependency-name: Microsoft.AspNetCore.Components.WebAssembly.DevServer
  dependency-version: 9.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 17:00:22 +02:00
dependabot[bot]
1401982e2c Bump Microsoft.EntityFrameworkCore and Microsoft.EntityFrameworkCore.SqlServer
Bumps [Microsoft.EntityFrameworkCore](https://github.com/dotnet/efcore) and [Microsoft.EntityFrameworkCore.SqlServer](https://github.com/dotnet/efcore). These dependencies needed to be updated together.

Updates `Microsoft.EntityFrameworkCore` from 9.0.3 to 9.0.4
- [Release notes](https://github.com/dotnet/efcore/releases)
- [Commits](https://github.com/dotnet/efcore/compare/v9.0.3...v9.0.4)

Updates `Microsoft.EntityFrameworkCore.SqlServer` from 9.0.3 to 9.0.4
- [Release notes](https://github.com/dotnet/efcore/releases)
- [Commits](https://github.com/dotnet/efcore/compare/v9.0.3...v9.0.4)

---
updated-dependencies:
- dependency-name: Microsoft.EntityFrameworkCore
  dependency-version: 9.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Microsoft.EntityFrameworkCore.SqlServer
  dependency-version: 9.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 17:00:15 +02:00
Leendert de Borst
ebdbf41208 Merge pull request #778 from lanedirt/775-add-spacing-in-webauthn-login-message-ui
Update webauthn unlock animation margin
2025-04-13 21:09:36 +02:00
Leendert de Borst
ed4b82e125 Update webauthn unlock animation margin (#775) 2025-04-12 16:06:12 +02:00
dependabot[bot]
1976255e98 Bump vite
Bumps the npm_and_yarn group with 1 update in the /browser-extension directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.2.5 to 6.2.6
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.2.6
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-12 16:03:32 +02:00
Leendert de Borst
e817326162 Merge pull request #774 from lanedirt/772-feature-request-add-proton-pass-import
Add Proton Pass importer
2025-04-12 16:03:19 +02:00
Leendert de Borst
9d2a397317 Add ProtonPass importer (#772) 2025-04-11 11:32:13 +02:00
dependabot[bot]
8f42ebdfa4 Bump System.Drawing.Common from 8.0.0 to 9.0.3
Bumps [System.Drawing.Common](https://github.com/dotnet/winforms) from 8.0.0 to 9.0.3.
- [Release notes](https://github.com/dotnet/winforms/releases)
- [Changelog](https://github.com/dotnet/winforms/blob/main/docs/release-activity.md)
- [Commits](https://github.com/dotnet/winforms/compare/v8.0.0...v9.0.3)

---
updated-dependencies:
- dependency-name: System.Drawing.Common
  dependency-version: 9.0.3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-08 09:38:52 +02:00
dependabot[bot]
3aab43b17a Bump Swashbuckle.AspNetCore from 8.0.0 to 8.1.0
Bumps [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) from 8.0.0 to 8.1.0.
- [Release notes](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases)
- [Commits](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/compare/v8.0.0...v8.1.0)

---
updated-dependencies:
- dependency-name: Swashbuckle.AspNetCore
  dependency-version: 8.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-08 09:38:42 +02:00
dependabot[bot]
6e922237c0 Bump NUglify from 1.21.13 to 1.21.14
Bumps [NUglify](https://github.com/trullock/NUglify) from 1.21.13 to 1.21.14.
- [Release notes](https://github.com/trullock/NUglify/releases)
- [Changelog](https://github.com/trullock/NUglify/blob/master/changelog.md)
- [Commits](https://github.com/trullock/NUglify/compare/v1.21.13...v1.21.14)

---
updated-dependencies:
- dependency-name: NUglify
  dependency-version: 1.21.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-08 09:38:36 +02:00
Leendert de Borst
ebac252162 Merge pull request #767 from lanedirt/dependabot/nuget/main/NUnit.Analyzers-4.7.0
Bump NUnit.Analyzers from 4.6.0 to 4.7.0
2025-04-08 09:38:20 +02:00
dependabot[bot]
9df76ffb43 Bump NUnit.Analyzers from 4.6.0 to 4.7.0
Bumps [NUnit.Analyzers](https://github.com/nunit/nunit.analyzers) from 4.6.0 to 4.7.0.
- [Release notes](https://github.com/nunit/nunit.analyzers/releases)
- [Changelog](https://github.com/nunit/nunit.analyzers/blob/master/CHANGES.md)
- [Commits](https://github.com/nunit/nunit.analyzers/compare/4.6.0...4.7.0)

---
updated-dependencies:
- dependency-name: NUnit.Analyzers
  dependency-version: 4.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 09:56:30 +00:00
66 changed files with 1551 additions and 352 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,10 @@ on:
pull_request:
branches: [ "main" ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
timeout-minutes: 60

View File

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

View File

@@ -9,6 +9,7 @@ on:
- main
pull_request_target:
types: [opened, synchronize, reopened]
jobs:
build:
name: Build and analyze

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ export class AppInfo {
/**
* The current extension version. This should be updated with each release of the extension.
*/
public static readonly VERSION = '0.16.0';
public static readonly VERSION = '0.16.2';
/**
* The minimum supported AliasVault server (API) version. If the server version is below this, the

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,9 +24,11 @@
<ImportService1Password />
<ImportServiceBitwarden />
<ImportServiceChrome />
<ImportServiceDashlane />
<ImportServiceFirefox />
<ImportServiceKeePass />
<ImportServiceKeePassXC />
<ImportServiceProtonPass />
<ImportServiceStrongbox />
<ImportServiceAliasVault />
</div>

View File

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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ public static class AppInfo
/// <summary>
/// Gets the patch version number.
/// </summary>
public const int VersionPatch = 0;
public const int VersionPatch = 2;
/// <summary>
/// Gets the minimum supported AliasVault client version. Normally the minimum client version is the same

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,,
1 username username2 username3 title password note url category otpUrl
2 Test username Test password123 https://Test
3 googleuser Google googlepassword https://www.google.com
4 testusername testusernamealternative Local testpassword testnote https://www.testwebsite.local

View File

@@ -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
1 type name url email username password note totp createTime modifyTime vault
2 login Test proton 1 https://www.website.com/ user1 pass1 otpauth://totp/Strongbox?secret=PLW4SB3PQ7MKVXY2MXF4NEXS6Y&algorithm=SHA1&digits=6&period=30 1744362003 1744362003 Personal
3 alias Test alias testalias.gating981@passinbox.com 1744362031 1744362052 Personal
4 login Test proton2 testuser2 testpassword2 1744362088 1744362088 Personal
5 login testwithoutpass testuser 1744362100 1744362110 Personal

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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