mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-24 06:39:12 -05:00
Compare commits
140 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77ced32206 | ||
|
|
299d1f6075 | ||
|
|
9811e32a73 | ||
|
|
7655773fa3 | ||
|
|
7a5afcac9c | ||
|
|
1ab736fd03 | ||
|
|
018895e8e9 | ||
|
|
0b07a37d73 | ||
|
|
5c0d7fc571 | ||
|
|
d9d84dd90f | ||
|
|
70b7063af2 | ||
|
|
87287e0237 | ||
|
|
477e786454 | ||
|
|
361ea77ab7 | ||
|
|
36237176fd | ||
|
|
e15ecaf793 | ||
|
|
4422ddcaa3 | ||
|
|
e34e96746f | ||
|
|
4c4d51d78e | ||
|
|
e4b12c4617 | ||
|
|
1cf9b5e93c | ||
|
|
6664266c3f | ||
|
|
79af285124 | ||
|
|
66928f74b7 | ||
|
|
c8599ccd9e | ||
|
|
53f69c97af | ||
|
|
11d8c941d2 | ||
|
|
e31f3df45b | ||
|
|
e2aafa3704 | ||
|
|
c2290f3ba4 | ||
|
|
b134ef3aee | ||
|
|
912c486266 | ||
|
|
51901e6ce3 | ||
|
|
0dbe417636 | ||
|
|
6f9528ea2d | ||
|
|
3266f7394e | ||
|
|
9fd5848029 | ||
|
|
0e2d7cabe8 | ||
|
|
2e5b00ea2c | ||
|
|
ff535188da | ||
|
|
bb41207cfe | ||
|
|
5944cd3248 | ||
|
|
0f02412db2 | ||
|
|
db479182f0 | ||
|
|
d5f8516abc | ||
|
|
1682304ae7 | ||
|
|
d0bbf3ac9f | ||
|
|
12492c922d | ||
|
|
3240c3760a | ||
|
|
58801926cc | ||
|
|
39b5c03ae1 | ||
|
|
b01cdc1f52 | ||
|
|
ce0f466f01 | ||
|
|
80e40b3ceb | ||
|
|
70bb8ef3e4 | ||
|
|
00fb290598 | ||
|
|
9d8a2e784f | ||
|
|
e57cb01164 | ||
|
|
6f421bbdc1 | ||
|
|
eaa42196f8 | ||
|
|
e844e20322 | ||
|
|
b53a4334ca | ||
|
|
afe2ba52b5 | ||
|
|
3e82c6e5d0 | ||
|
|
68dbecd536 | ||
|
|
c0c1b75e73 | ||
|
|
8510648b5f | ||
|
|
0e803205c0 | ||
|
|
2fc7ffa509 | ||
|
|
b16fd8e157 | ||
|
|
effeb211ff | ||
|
|
bfc15fcea6 | ||
|
|
6bb204efb9 | ||
|
|
dbc9724377 | ||
|
|
71783f1af2 | ||
|
|
7ead1d270b | ||
|
|
19b89cbfda | ||
|
|
0617ccb42e | ||
|
|
a3d702f2e5 | ||
|
|
3967b0f832 | ||
|
|
867dd90000 | ||
|
|
6ed1be3b91 | ||
|
|
56e065feea | ||
|
|
3b27e647ef | ||
|
|
62732a71f0 | ||
|
|
f3ad61a77a | ||
|
|
0d878f669f | ||
|
|
6fba784cfe | ||
|
|
c46a95cf82 | ||
|
|
bba16e6e14 | ||
|
|
b4c4603868 | ||
|
|
925455b5d6 | ||
|
|
6aa0c2b9df | ||
|
|
1799a2f580 | ||
|
|
615b5b2883 | ||
|
|
006f89b6b7 | ||
|
|
76c60ad200 | ||
|
|
1830dc0ca1 | ||
|
|
c3599c9f26 | ||
|
|
5d050cd278 | ||
|
|
ff57091eef | ||
|
|
64ef5837c0 | ||
|
|
771f372434 | ||
|
|
7690355434 | ||
|
|
822b95d940 | ||
|
|
41b2a959ed | ||
|
|
3e82f78fe9 | ||
|
|
421884e301 | ||
|
|
d149e5aeec | ||
|
|
8b2702cbe3 | ||
|
|
7b1cfd363c | ||
|
|
5e965d7b3f | ||
|
|
d8ac05f325 | ||
|
|
a1c13a15f9 | ||
|
|
f285b36c61 | ||
|
|
c6fa90e00c | ||
|
|
cb8de80f08 | ||
|
|
15bb7f6593 | ||
|
|
516dd524df | ||
|
|
87e58f8546 | ||
|
|
3baaf78689 | ||
|
|
336bbafe27 | ||
|
|
83d9eadeea | ||
|
|
1cdd8f456e | ||
|
|
395f881bd0 | ||
|
|
293ae102c5 | ||
|
|
8f5852bb86 | ||
|
|
9ccaff74cd | ||
|
|
ee6b40dd3d | ||
|
|
3ca4c0a78d | ||
|
|
b246def212 | ||
|
|
1eecb8be38 | ||
|
|
9a7fbe7d2a | ||
|
|
7776fb6d82 | ||
|
|
0eebaddf04 | ||
|
|
8b145e66b5 | ||
|
|
4e3c992c24 | ||
|
|
65944b1523 | ||
|
|
d05114fddc | ||
|
|
8e0fef4b16 |
@@ -14,9 +14,9 @@
|
||||
# Docker containers to apply the changes.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# Set the ports that your AliasVault will be accessible at.
|
||||
# These are the default ports that will be used by the `reverse-proxy` and `smtp` containers.
|
||||
# You can change these to any other ports that are available on your system.
|
||||
# Configure the network ports used by AliasVault by the `reverse-proxy` and `smtp` containers.
|
||||
# You can change these if the defaults are in use on your system.
|
||||
# After making changes, re-run the install script to apply them.
|
||||
HTTP_PORT=80
|
||||
HTTPS_PORT=443
|
||||
SMTP_PORT=25
|
||||
|
||||
@@ -35,6 +35,7 @@ jobs:
|
||||
"apps/browser-extension/src/utils/dist/shared/identity-generator"
|
||||
"apps/browser-extension/src/utils/dist/shared/password-generator"
|
||||
"apps/browser-extension/src/utils/dist/shared/models"
|
||||
"apps/browser-extension/src/utils/dist/shared/vault-sql"
|
||||
)
|
||||
|
||||
for dir in "${TARGET_DIRS[@]}"; do
|
||||
|
||||
9
.github/workflows/docker-build.yml
vendored
9
.github/workflows/docker-build.yml
vendored
@@ -125,8 +125,8 @@ jobs:
|
||||
- name: Test reset-admin-password output
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
run: |
|
||||
output=$(./install.sh reset-admin-password)
|
||||
if ! echo "$output" | grep -E '.*New admin password: [A-Za-z0-9+/=]{8,}.*'; then
|
||||
output=$(./install.sh reset-admin-password | sed 's/\x1b\[[0-9;]*m//g')
|
||||
if ! echo "$output" | grep -Eq '^\s*Password: [A-Za-z0-9+/=]{8,}'; then
|
||||
echo "Invalid reset-admin-password output"
|
||||
exit 1
|
||||
fi
|
||||
@@ -197,9 +197,10 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Test reset-admin-password output
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
run: |
|
||||
output=$(./install.sh reset-admin-password)
|
||||
if ! echo "$output" | grep -E '.*New admin password: [A-Za-z0-9+/=]{8,}.*'; then
|
||||
output=$(./install.sh reset-admin-password | sed 's/\x1b\[[0-9;]*m//g')
|
||||
if ! echo "$output" | grep -Eq '^\s*Password: [A-Za-z0-9+/=]{8,}'; then
|
||||
echo "Invalid reset-admin-password output"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
1
.github/workflows/mobile-app-build.yml
vendored
1
.github/workflows/mobile-app-build.yml
vendored
@@ -56,6 +56,7 @@ jobs:
|
||||
"utils/dist/shared/identity-generator"
|
||||
"utils/dist/shared/password-generator"
|
||||
"utils/dist/shared/models"
|
||||
"utils/dist/shared/vault-sql"
|
||||
)
|
||||
|
||||
for dir in "${TARGET_DIRS[@]}"; do
|
||||
|
||||
@@ -66,9 +66,9 @@ jobs:
|
||||
run: |
|
||||
$scanner = "${{ github.workspace }}\.sonar\scanner\dotnet-sonarscanner"
|
||||
if ('${{ github.event_name }}' -eq 'pull_request_target') {
|
||||
& $scanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.pullrequest.key=${{ github.event.pull_request.number }} /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs" /d:sonar.exclusions="**/__tests__/test-forms/*.html,**/dist/shared/**"
|
||||
& $scanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.pullrequest.key=${{ github.event.pull_request.number }} /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs" /d:sonar.exclusions="**/__tests__/test-forms/*.html,**/dist/shared/**,**/*.sql"
|
||||
} else {
|
||||
& $scanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs" /d:sonar.exclusions="**/__tests__/test-forms/*.html,**/dist/shared/**"
|
||||
& $scanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs" /d:sonar.exclusions="**/__tests__/test-forms/*.html,**/dist/shared/**,**/*.sql"
|
||||
}
|
||||
dotnet build
|
||||
dotnet test -c Release /p:CollectCoverage=true /p:CoverletOutput=coverage /p:CoverletOutputFormat=opencover --filter 'FullyQualifiedName!~AliasVault.E2ETests'
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -378,6 +378,10 @@ FodyWeavers.xsd
|
||||
# Codebuddy Rider plugin
|
||||
.codebuddy
|
||||
|
||||
# Claude Code
|
||||
.claude
|
||||
CLAUDE.md
|
||||
|
||||
# -------------------
|
||||
# AliasVault specifics
|
||||
# -------------------
|
||||
|
||||
14
.vscode/tasks.json
vendored
14
.vscode/tasks.json
vendored
@@ -43,6 +43,20 @@
|
||||
"cwd": "${workspaceFolder}/apps/server/AliasVault.Admin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch SMTP Service",
|
||||
"type": "shell",
|
||||
"command": "dotnet watch",
|
||||
"args": [],
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/server/Services/AliasVault.SmtpService"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch Client CSS",
|
||||
"type": "shell",
|
||||
|
||||
@@ -1,26 +1,60 @@
|
||||
# Contributing to the source code
|
||||
We welcome contributions to AliasVault. Please read the guidelines on the official AliasVault docs website on how to get your local development environment setup and the general contribution guidelines:
|
||||
# Contributing to AliasVault
|
||||
|
||||
Thanks for your interest in contributing to the AliasVault project! There are a lot of ways to help out.
|
||||
|
||||
## Community Engagement
|
||||
|
||||
Become active in AliasVault's community, helping by:
|
||||
|
||||
- **Answering questions** in our [Discord community](https://discord.gg/DsaXMTEtpF)
|
||||
- **Helping users** with self-hosting setup and troubleshooting
|
||||
- **Reporting bugs** and suggesting improvements
|
||||
- **Participating in discussions** about features and improvements
|
||||
|
||||
## Spreading the Word
|
||||
|
||||
Getting the word out about AliasVault is important so we can reach and help more people to improve their privacy. You can help by:
|
||||
|
||||
- **Sharing on social media** (X, Reddit, Mastodon, etc.)
|
||||
- **Writing blog posts** about your AliasVault experience
|
||||
- **Creating video tutorials** or walkthroughs
|
||||
- **Mentioning AliasVault** in privacy/self-hosting discussions
|
||||
- **Telling friends and colleagues** about the project
|
||||
|
||||
## Contributing to the Documentation
|
||||
|
||||
The docs are built using Jekyll and automatically deploy to GitHub Pages via GitHub Actions. You can build the docs locally by running `docker compose up` in the `./docs` folder.
|
||||
|
||||
The docs site is based on the open-source template called Just The Docs. Find more information about how this template works in the [official docs](https://just-the-docs.github.io/just-the-docs/).
|
||||
|
||||
To make changes to the AliasVault documentation please make a PR that directly edits the `docs` markdown files in this repository.
|
||||
|
||||
## Contributing to the Main Codebase
|
||||
### Get in contact
|
||||
If you’re planning to work on a new feature or improvement for AliasVault, we strongly encourage you to get in touch with us first. This ensures that your proposed changes align with the project's direction and increases the likelihood of your work being accepted into the official repository. You can reach us through:
|
||||
|
||||
- Opening an issue on GitHub to discuss your proposed changes
|
||||
- Reaching out via Discord or email
|
||||
- Contacting the maintainers directly
|
||||
|
||||
### Set up your local development environment
|
||||
You can find instructions on how to get your local development environment setup for the different parts of the AliasVault codebase here:
|
||||
|
||||
https://docs.aliasvault.net/misc/dev/
|
||||
|
||||
> Tip: if the URL above is not available, the raw doc pages can also be found in the `docs` folder in this repository.
|
||||
|
||||
## Contributing to the documentation
|
||||
The docs are built using Jekyll and automatically deploy to GitHub Pages via GitHub Actions. You can build the docs locally by running `docker compose up` in the `./docs` folder.
|
||||
If you run into any issues, feel free to join our [Discord](https://discord.gg/DsaXMTEtpF) to chat with the maintainers and author.
|
||||
|
||||
The docs site is based on the open-source template called Just The Docs. Find more information about how this template works in the [official docs](https://just-the-docs.github.io/just-the-docs/).
|
||||
## License and Contributions
|
||||
|
||||
AliasVault is licensed under the GNU Affero General Public License v3.0 (AGPLv3). By submitting code, documentation, or other contributions to this project, you agree that:
|
||||
|
||||
To make changes to the AliasVault documentation please make a PR that directly edits the `docs` markdown files in this repository.
|
||||
1. Your contribution will be licensed under the same AGPLv3 license as the project
|
||||
2. You have the legal right to grant this license (e.g., you are the author, or have permission)
|
||||
3. You understand that your contribution will be made public under the AGPLv3 terms
|
||||
4. You are not expected to provide support or warranties for your contribution
|
||||
|
||||
## Contributor License Agreement (CLA)
|
||||
Thank you for your interest in contributing to AliasVault (“Project”).
|
||||
✅ There is no Contributor License Agreement (CLA) required. We believe in a balanced open source model where all contributors are treated equally under the terms of the AGPLv3.
|
||||
|
||||
By submitting code, documentation, or other contributions to this Project, you agree to the following:
|
||||
|
||||
1. You are legally entitled to grant this license (e.g., you are the author, or have permission).
|
||||
2. You grant the Project maintainers a perpetual, worldwide, non-exclusive, royalty-free license to use, modify, distribute, and sublicense your contribution as part of the Project and any derivative works.
|
||||
3. You understand that your contribution will be made public and licensed under the same terms as the Project (e.g., AGPLv3), or any later version the maintainers may release.
|
||||
4. You are not expected to provide support or warranties for your contribution.
|
||||
|
||||
> All contributors must accept the CLA as a condition of contributing. By opening a pull request, you agree to these terms. We may enforce this automatically via GitHub if needed.
|
||||
> By opening a pull request, you agree to these terms. Your contributions will be published under the AGPLv3 license.
|
||||
|
||||
30
README.md
30
README.md
@@ -1,5 +1,5 @@
|
||||
# <img src="https://github.com/user-attachments/assets/933c8b45-a190-4df6-913e-b7c64ad9938b" width="35" alt="AliasVault"> AliasVault
|
||||
The privacy-first password & email alias manager. Fully end-to-end encrypted, with built-in alias generation and email server — giving you full control over your online identity and safeguarding your privacy.
|
||||
AliasVault is a privacy-first password and email alias manager. Create unique identities, strong passwords, and random email aliases for every website you use. Fully end-to-end encrypted, with a built-in email server and zero third-party dependencies.
|
||||
|
||||
[<img src="https://img.shields.io/github/v/release/lanedirt/AliasVault?include_prereleases&logo=github&label=Release">](https://github.com/lanedirt/AliasVault/releases)
|
||||
[](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-e2e-tests.yml)
|
||||
@@ -8,12 +8,12 @@ The privacy-first password & email alias manager. Fully end-to-end encrypted, wi
|
||||
|
||||
<a href="https://app.aliasvault.net">Try the cloud version 🔥</a> | <a href="https://aliasvault.net?utm_source=gh-readme">Website </a> | <a href="https://docs.aliasvault.net?utm_source=gh-readme">Documentation </a> | <a href="#self-hosting">Self-host instructions</a>
|
||||
|
||||
⭐ Star us on GitHub — it motivates us a lot!
|
||||
|
||||
## About
|
||||
AliasVault helps protect your privacy online by generating a unique password, identity, and email alias for every service you use. Everything is end-to-end encrypted and under your control — whether in the cloud or self-hosted.
|
||||
Built on 15 years of experience, AliasVault is independent, open-source, self-hostable and community-driven. It’s the response to a web that tracks everything: a way to take back control of your digital privacy and help you stay secure online.
|
||||
|
||||
Built on 15 years of experience, AliasVault is open-source, self-hostable and community-driven. It’s the response to a web that tracks everything: a way to take back control of your digital privacy and help you stay secure online.
|
||||
|
||||
– Leendert de Borst (@lanedirt), Creator of AliasVault
|
||||
– Leendert de Borst ([@lanedirt](https://github.com/lanedirt)), Creator of AliasVault
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -47,7 +47,17 @@ Built on 15 years of experience, AliasVault is open-source, self-hostable and co
|
||||
## Cloud-hosted
|
||||
Use the official cloud version of AliasVault at [app.aliasvault.net](https://app.aliasvault.net). This fully supported platform is always up to date with our latest release.
|
||||
|
||||
AliasVault is available on: [Web](https://app.aliasvault.net) | [iOS](https://apps.apple.com/app/id6745490915) | [Android](https://play.google.com/store/apps/details?id=net.aliasvault.app) | [Chrome](https://chromewebstore.google.com/detail/aliasvault/bmoggiinmnodjphdjnmpcnlleamkfedj) | [Firefox](https://addons.mozilla.org/en-US/firefox/addon/aliasvault/) | [Edge](https://microsoftedge.microsoft.com/addons/detail/aliasvault/kabaanafahnjkfkplbnllebdmppdemfo) | [Safari](https://apps.apple.com/app/id6743163173)
|
||||
AliasVault is available on:
|
||||
- [Web (universal)](https://app.aliasvault.net)
|
||||
- [Chrome](https://chromewebstore.google.com/detail/aliasvault/bmoggiinmnodjphdjnmpcnlleamkfedj)
|
||||
- [Firefox](https://addons.mozilla.org/en-US/firefox/addon/aliasvault/)
|
||||
- [Edge](https://microsoftedge.microsoft.com/addons/detail/aliasvault/kabaanafahnjkfkplbnllebdmppdemfo)
|
||||
- [Safari](https://apps.apple.com/app/id6743163173)
|
||||
|
||||
<p>
|
||||
<a href="https://apps.apple.com/app/id6745490915" style="display: inline-block; margin-right: 20px;"><img src="https://github.com/user-attachments/assets/bad09b85-2635-4e3e-b154-9f348b88f6d6" style="height: 40px;margin-right:10px;" alt="Download on the App Store"></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=net.aliasvault.app" style="display: inline-block;"><img src="https://github.com/user-attachments/assets/b28979c9-f4b8-4090-8735-e384a7fdaa47" style="height: 40px;" alt="Get it on Google Play"></a>
|
||||
</p>
|
||||
|
||||
[<img width="700" alt="Screenshot of AliasVault" src="docs/assets/img/screenshot.png">](https://app.aliasvault.net)
|
||||
|
||||
@@ -62,7 +72,7 @@ This method uses pre-built Docker images and works on minimal hardware specifica
|
||||
- 1 vCPU
|
||||
- 1GB RAM
|
||||
- 16GB disk space
|
||||
- Docker installed
|
||||
- Docker (20.10+) and Docker Compose (2.0+)
|
||||
|
||||
```bash
|
||||
# Download install script from latest stable release
|
||||
@@ -115,7 +125,8 @@ Core features that are being worked on:
|
||||
- [x] Import passwords from traditional password managers
|
||||
- [x] iOS native app
|
||||
- [x] Android native app
|
||||
- [ ] Data model and usability improvements (more flexible aliases and credential types, folder support, editing in browser extension, bulk selecting etc.)
|
||||
- [x] Editing in browser extension
|
||||
- [ ] Data model and usability improvements (more flexible aliases and credential types, folder support, bulk selecting etc.)
|
||||
- [ ] Support for FIDO2/WebAuthn hardware keys and passkeys
|
||||
- [ ] Adding support for family/team sharing (organization features)
|
||||
|
||||
@@ -127,5 +138,4 @@ Feel free to open an issue or join our [Discord](https://discord.gg/DsaXMTEtpF)!
|
||||
### Support the mission
|
||||
Your donation helps me dedicate more time and resources to improving AliasVault, making the internet safer for everyone!
|
||||
|
||||
<a href="https://www.buymeacoffee.com/lanedirt" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
||||
|
||||
<a href="https://www.buymeacoffee.com/lanedirt" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 50px !important;" ></a>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "aliasvault-browser-extension",
|
||||
"description": "AliasVault Browser Extension",
|
||||
"private": true,
|
||||
"version": "0.19.0",
|
||||
"version": "0.20.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:chrome": "wxt -b chrome",
|
||||
|
||||
@@ -447,7 +447,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 20;
|
||||
CURRENT_PROJECT_VERSION = 22;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -460,7 +460,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.19.0;
|
||||
MARKETING_VERSION = 0.20.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -479,7 +479,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 20;
|
||||
CURRENT_PROJECT_VERSION = 22;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -492,7 +492,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.19.0;
|
||||
MARKETING_VERSION = 0.20.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -515,7 +515,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 20;
|
||||
CURRENT_PROJECT_VERSION = 22;
|
||||
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.19.0;
|
||||
MARKETING_VERSION = 0.20.0;
|
||||
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 = 20;
|
||||
CURRENT_PROJECT_VERSION = 22;
|
||||
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.19.0;
|
||||
MARKETING_VERSION = 0.20.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@media (max-width: 400px) {
|
||||
@media (max-width: 380px) {
|
||||
html, body {
|
||||
width: 350px;
|
||||
max-width: 350px;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { onMessage, sendMessage } from "webext-bridge/background";
|
||||
|
||||
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
|
||||
import { handleOpenPopup, handlePopupWithCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler';
|
||||
import { handleCheckAuthStatus, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentityLanguage, handleGetDerivedKey, handleGetPasswordSettings, handleGetVault, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler';
|
||||
import { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentitySettings, handleGetDerivedKey, handleGetPasswordSettings, handleGetPersistedFormValues, handleGetVault, handlePersistFormValues, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler';
|
||||
|
||||
import { GLOBAL_CONTEXT_MENU_ENABLED_KEY } from '@/utils/Constants';
|
||||
|
||||
@@ -23,13 +23,17 @@ export default defineBackground({
|
||||
onMessage('GET_CREDENTIALS', () => handleGetCredentials());
|
||||
onMessage('CREATE_IDENTITY', ({ data }) => handleCreateIdentity(data));
|
||||
onMessage('GET_DEFAULT_EMAIL_DOMAIN', () => handleGetDefaultEmailDomain());
|
||||
onMessage('GET_DEFAULT_IDENTITY_LANGUAGE', () => handleGetDefaultIdentityLanguage());
|
||||
onMessage('GET_DEFAULT_IDENTITY_SETTINGS', () => handleGetDefaultIdentitySettings());
|
||||
onMessage('GET_PASSWORD_SETTINGS', () => handleGetPasswordSettings());
|
||||
onMessage('GET_DERIVED_KEY', () => handleGetDerivedKey());
|
||||
onMessage('OPEN_POPUP', () => handleOpenPopup());
|
||||
onMessage('OPEN_POPUP_WITH_CREDENTIAL', ({ data }) => handlePopupWithCredential(data));
|
||||
onMessage('TOGGLE_CONTEXT_MENU', ({ data }) => handleToggleContextMenu(data));
|
||||
|
||||
onMessage('PERSIST_FORM_VALUES', ({ data }) => handlePersistFormValues(data));
|
||||
onMessage('GET_PERSISTED_FORM_VALUES', () => handleGetPersistedFormValues());
|
||||
onMessage('CLEAR_PERSISTED_FORM_VALUES', () => handleClearPersistedFormValues());
|
||||
|
||||
// Setup context menus
|
||||
const isContextMenuEnabled = await storage.getItem(GLOBAL_CONTEXT_MENU_ENABLED_KEY) ?? true;
|
||||
if (isContextMenuEnabled) {
|
||||
|
||||
@@ -62,9 +62,7 @@ export function handleContextMenuClick(info: Browser.contextMenus.OnClickData, t
|
||||
args: [password]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (info.menuItemId === "aliasvault-activate-form" && tab?.id) {
|
||||
} else if (info.menuItemId === "aliasvault-activate-form" && tab?.id) {
|
||||
// First get the active element's identifier
|
||||
browser.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { BoolResponse } from '@/utils/types/messaging/BoolResponse';
|
||||
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
|
||||
|
||||
import { setupContextMenus } from './ContextMenu';
|
||||
import { BoolResponse } from '@/utils/types/messaging/BoolResponse';
|
||||
|
||||
import { browser } from '#imports';
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { EncryptionUtility } from '@/utils/EncryptionUtility';
|
||||
import { SqliteClient } from '@/utils/SqliteClient';
|
||||
import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/BoolResponse';
|
||||
import { CredentialsResponse as messageCredentialsResponse } from '@/utils/types/messaging/CredentialsResponse';
|
||||
import { IdentitySettingsResponse } from '@/utils/types/messaging/IdentitySettingsResponse';
|
||||
import { PasswordSettingsResponse as messagePasswordSettingsResponse } from '@/utils/types/messaging/PasswordSettingsResponse';
|
||||
import { StoreVaultRequest } from '@/utils/types/messaging/StoreVaultRequest';
|
||||
import { StringResponse as stringResponse } from '@/utils/types/messaging/StringResponse';
|
||||
@@ -14,9 +15,9 @@ import { VaultUploadResponse as messageVaultUploadResponse } from '@/utils/types
|
||||
import { WebApiService } from '@/utils/WebApiService';
|
||||
|
||||
/**
|
||||
* Check if the user is logged in and if the vault is locked.
|
||||
* Check if the user is logged in and if the vault is locked, and also check for pending migrations.
|
||||
*/
|
||||
export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, isVaultLocked: boolean }> {
|
||||
export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, isVaultLocked: boolean, hasPendingMigrations: boolean, error?: string }> {
|
||||
const username = await storage.getItem('local:username');
|
||||
const accessToken = await storage.getItem('local:accessToken');
|
||||
const vaultData = await storage.getItem('session:encryptedVault');
|
||||
@@ -24,10 +25,42 @@ export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, i
|
||||
const isLoggedIn = username !== null && accessToken !== null;
|
||||
const isVaultLocked = isLoggedIn && vaultData === null;
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
isVaultLocked
|
||||
};
|
||||
// If vault is locked, we can't check for pending migrations
|
||||
if (isVaultLocked) {
|
||||
return {
|
||||
isLoggedIn,
|
||||
isVaultLocked,
|
||||
hasPendingMigrations: false
|
||||
};
|
||||
}
|
||||
|
||||
// If not logged in, no need to check migrations
|
||||
if (!isLoggedIn) {
|
||||
return {
|
||||
isLoggedIn,
|
||||
isVaultLocked,
|
||||
hasPendingMigrations: false
|
||||
};
|
||||
}
|
||||
|
||||
// Vault is unlocked, check for pending migrations
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const hasPendingMigrations = await sqliteClient.hasPendingMigrations();
|
||||
return {
|
||||
isLoggedIn,
|
||||
isVaultLocked,
|
||||
hasPendingMigrations
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error checking pending migrations:', error);
|
||||
return {
|
||||
isLoggedIn,
|
||||
isVaultLocked,
|
||||
hasPendingMigrations: false,
|
||||
error: error instanceof Error ? error.message : 'An unknown error occurred'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,18 +277,25 @@ export function handleGetDefaultEmailDomain(): Promise<stringResponse> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default identity language.
|
||||
* Get the default identity settings.
|
||||
*/
|
||||
export async function handleGetDefaultIdentityLanguage(
|
||||
) : Promise<stringResponse> {
|
||||
export async function handleGetDefaultIdentitySettings(
|
||||
) : Promise<IdentitySettingsResponse> {
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const settingValue = sqliteClient.getDefaultIdentityLanguage();
|
||||
const language = sqliteClient.getDefaultIdentityLanguage();
|
||||
const gender = sqliteClient.getDefaultIdentityGender();
|
||||
|
||||
return { success: true, value: settingValue };
|
||||
return {
|
||||
success: true,
|
||||
settings: {
|
||||
language,
|
||||
gender
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting default identity language:', error);
|
||||
return { success: false, error: 'Failed to get default identity language' };
|
||||
console.error('Error getting default identity settings:', error);
|
||||
return { success: false, error: 'Failed to get default identity settings' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,6 +346,56 @@ export async function handleUploadVault(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle persisting form values to storage.
|
||||
* Data is encrypted using the derived key for additional security.
|
||||
*/
|
||||
export async function handlePersistFormValues(data: any): Promise<void> {
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
if (!derivedKey) {
|
||||
throw new Error('No derived key available for encryption');
|
||||
}
|
||||
|
||||
// Always stringify the data properly
|
||||
const serializedData = JSON.stringify(data);
|
||||
const encryptedData = await EncryptionUtility.symmetricEncrypt(
|
||||
serializedData,
|
||||
derivedKey
|
||||
);
|
||||
await storage.setItem('session:persistedFormValues', encryptedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle retrieving persisted form values from storage.
|
||||
* Data is decrypted using the derived key.
|
||||
*/
|
||||
export async function handleGetPersistedFormValues(): Promise<any | null> {
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
const encryptedData = await storage.getItem('session:persistedFormValues') as string | null;
|
||||
|
||||
if (!encryptedData || !derivedKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedData = await EncryptionUtility.symmetricDecrypt(
|
||||
encryptedData,
|
||||
derivedKey
|
||||
);
|
||||
return JSON.parse(decryptedData);
|
||||
} catch (error) {
|
||||
console.error('Failed to decrypt or parse persisted form values:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle clearing persisted form values from storage.
|
||||
*/
|
||||
export async function handleClearPersistedFormValues(): Promise<void> {
|
||||
await storage.removeItem('session:persistedFormValues');
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a new version of the vault to the server using the provided sqlite client.
|
||||
*/
|
||||
@@ -341,7 +431,7 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<Vaul
|
||||
client: '', // Empty on purpose, API will not use this for vault updates.
|
||||
updatedAt: new Date().toISOString(),
|
||||
username: username,
|
||||
version: sqliteClient.getDatabaseVersion() ?? '0.0.0'
|
||||
version: sqliteClient.getDatabaseVersion().version
|
||||
};
|
||||
|
||||
const webApi = new WebApiService(() => {});
|
||||
|
||||
@@ -2,7 +2,7 @@ import '@/entrypoints/contentScript/style.css';
|
||||
import { onMessage } from "webext-bridge/content-script";
|
||||
|
||||
import { injectIcon, popupDebounceTimeHasPassed, validateInputField } from '@/entrypoints/contentScript/Form';
|
||||
import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup } from '@/entrypoints/contentScript/Popup';
|
||||
import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup, createUpgradeRequiredPopup } from '@/entrypoints/contentScript/Popup';
|
||||
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/BoolResponse';
|
||||
@@ -69,7 +69,7 @@ export default defineContentScript({
|
||||
|
||||
// Only show popup if debounce time has passed
|
||||
if (popupDebounceTimeHasPassed()) {
|
||||
openAutofillPopup(inputElement, container);
|
||||
await showPopupWithAuthCheck(inputElement, container);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +132,48 @@ export default defineContentScript({
|
||||
|
||||
if (canShowPopup) {
|
||||
injectIcon(inputElement, container);
|
||||
await showPopupWithAuthCheck(inputElement, container);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show popup with auth check.
|
||||
*/
|
||||
async function showPopupWithAuthCheck(inputElement: HTMLInputElement, container: HTMLElement) : Promise<void> {
|
||||
try {
|
||||
// Check auth status and pending migrations in a single call
|
||||
const { sendMessage } = await import('webext-bridge/content-script');
|
||||
const authStatus = await sendMessage('CHECK_AUTH_STATUS', {}, 'background') as {
|
||||
isLoggedIn: boolean,
|
||||
isVaultLocked: boolean,
|
||||
hasPendingMigrations: boolean,
|
||||
error?: string
|
||||
};
|
||||
|
||||
if (authStatus.isVaultLocked) {
|
||||
// Vault is locked, show vault locked popup
|
||||
const { createVaultLockedPopup } = await import('@/entrypoints/contentScript/Popup');
|
||||
createVaultLockedPopup(inputElement, container);
|
||||
return;
|
||||
}
|
||||
|
||||
if (authStatus.hasPendingMigrations) {
|
||||
// Show upgrade required popup
|
||||
createUpgradeRequiredPopup(inputElement, container, 'Vault upgrade required.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (authStatus.error) {
|
||||
// Show upgrade required popup for version-related errors
|
||||
createUpgradeRequiredPopup(inputElement, container, authStatus.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// No upgrade required, show normal autofill popup
|
||||
openAutofillPopup(inputElement, container);
|
||||
} catch (error) {
|
||||
console.error('Error checking vault status:', error);
|
||||
// Fall back to normal autofill popup if check fails
|
||||
openAutofillPopup(inputElement, container);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { CreatePasswordGenerator, PasswordGenerator } from '@/utils/dist/shared/
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { SqliteClient } from '@/utils/SqliteClient';
|
||||
import { CredentialsResponse } from '@/utils/types/messaging/CredentialsResponse';
|
||||
import { IdentitySettingsResponse } from '@/utils/types/messaging/IdentitySettingsResponse';
|
||||
import { PasswordSettingsResponse } from '@/utils/types/messaging/PasswordSettingsResponse';
|
||||
import { StringResponse } from '@/utils/types/messaging/StringResponse';
|
||||
|
||||
@@ -244,9 +245,9 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
|
||||
};
|
||||
} else {
|
||||
// Generate new random identity using identity generator.
|
||||
const identityLanguage = await sendMessage('GET_DEFAULT_IDENTITY_LANGUAGE', {}, 'background') as StringResponse;
|
||||
const identityGenerator = CreateIdentityGenerator(identityLanguage.value ?? 'en');
|
||||
const identity = identityGenerator.generateRandomIdentity();
|
||||
const identitySettings = await sendMessage('GET_DEFAULT_IDENTITY_SETTINGS', {}, 'background') as IdentitySettingsResponse;
|
||||
const identityGenerator = CreateIdentityGenerator(identitySettings.settings?.language ?? 'en');
|
||||
const identity = identityGenerator.generateRandomIdentity(identitySettings.settings?.gender);
|
||||
|
||||
// Get password settings from background
|
||||
const passwordSettingsResponse = await sendMessage('GET_PASSWORD_SETTINGS', {}, 'background') as PasswordSettingsResponse;
|
||||
@@ -1462,3 +1463,92 @@ function addReliableClickHandler(element: HTMLElement, handler: (e: Event) => vo
|
||||
isMouseDown = false;
|
||||
}, { capture: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create upgrade required popup.
|
||||
*/
|
||||
export function createUpgradeRequiredPopup(input: HTMLInputElement, rootContainer: HTMLElement, errorMessage: string): void {
|
||||
/**
|
||||
* Handle upgrade click.
|
||||
*/
|
||||
const handleUpgradeClick = () : void => {
|
||||
sendMessage('OPEN_POPUP', {}, 'background');
|
||||
removeExistingPopup(rootContainer);
|
||||
}
|
||||
|
||||
const popup = createBasePopup(input, rootContainer);
|
||||
popup.classList.add('av-upgrade-required');
|
||||
|
||||
// Create container for message and button
|
||||
const container = document.createElement('div');
|
||||
container.className = 'av-upgrade-required-container';
|
||||
|
||||
// Make the entire container clickable
|
||||
addReliableClickHandler(container, handleUpgradeClick);
|
||||
container.style.cursor = 'pointer';
|
||||
|
||||
// Add message
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.className = 'av-upgrade-required-message';
|
||||
messageElement.textContent = errorMessage;
|
||||
container.appendChild(messageElement);
|
||||
|
||||
// Add upgrade button with SVG icon
|
||||
const button = document.createElement('button');
|
||||
button.title = 'Open AliasVault to upgrade';
|
||||
button.className = 'av-upgrade-required-button';
|
||||
button.innerHTML = `
|
||||
<svg class="av-icon-upgrade" viewBox="0 0 24 24">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
|
||||
</svg>
|
||||
`;
|
||||
container.appendChild(button);
|
||||
|
||||
// Add the container to the popup
|
||||
popup.appendChild(container);
|
||||
|
||||
// Add close button as a separate element positioned to the right
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.className = 'av-button av-button-close av-upgrade-required-close';
|
||||
closeButton.title = 'Dismiss popup';
|
||||
closeButton.innerHTML = `
|
||||
<svg class="av-icon" viewBox="0 0 24 24">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Position the close button to the right of the container
|
||||
closeButton.style.position = 'absolute';
|
||||
closeButton.style.right = '8px';
|
||||
closeButton.style.top = '50%';
|
||||
closeButton.style.transform = 'translateY(-50%)';
|
||||
|
||||
// Handle close button click
|
||||
addReliableClickHandler(closeButton, (e) => {
|
||||
e.stopPropagation(); // Prevent opening the upgrade popup
|
||||
removeExistingPopup(rootContainer);
|
||||
});
|
||||
|
||||
popup.appendChild(closeButton);
|
||||
|
||||
/**
|
||||
* Add event listener to document to close popup when clicking outside.
|
||||
*/
|
||||
const handleClickOutside = (event: MouseEvent): void => {
|
||||
const target = event.target as Node;
|
||||
const targetElement = event.target as HTMLElement;
|
||||
|
||||
// Check if the click is outside the popup and outside the shadow UI
|
||||
if (popup && !popup.contains(target) && !input.contains(target) && targetElement.tagName !== 'ALIASVAULT-UI') {
|
||||
removeExistingPopup(rootContainer);
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}, 100);
|
||||
|
||||
rootContainer.appendChild(popup);
|
||||
}
|
||||
|
||||
@@ -299,6 +299,71 @@ body {
|
||||
border: 1px solid #6f6f6f;
|
||||
}
|
||||
|
||||
/* Upgrade Required Popup */
|
||||
.av-upgrade-required {
|
||||
padding: 12px 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.av-upgrade-required:hover {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.av-upgrade-required-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 32px;
|
||||
width: 100%;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.av-upgrade-required-message {
|
||||
color: #d1d5db;
|
||||
font-size: 14px;
|
||||
flex-grow: 1;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-upgrade-required-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
padding-right: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #f59e0b;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.av-upgrade-required-close {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
border: 1px solid #6f6f6f;
|
||||
}
|
||||
|
||||
.av-icon-upgrade {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* Create Name Popup */
|
||||
.av-create-popup-overlay {
|
||||
position: fixed;
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
|
||||
import GlobalStateChangeHandler from '@/entrypoints/popup/components/GlobalStateChangeHandler';
|
||||
import BottomNav from '@/entrypoints/popup/components/Layout/BottomNav';
|
||||
import Header from '@/entrypoints/popup/components/Layout/Header';
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { NavigationProvider } from '@/entrypoints/popup/context/NavigationContext';
|
||||
import AuthSettings from '@/entrypoints/popup/pages/AuthSettings';
|
||||
import CredentialAddEdit from '@/entrypoints/popup/pages/CredentialAddEdit';
|
||||
import CredentialDetails from '@/entrypoints/popup/pages/CredentialDetails';
|
||||
import CredentialsList from '@/entrypoints/popup/pages/CredentialsList';
|
||||
import EmailDetails from '@/entrypoints/popup/pages/EmailDetails';
|
||||
import EmailsList from '@/entrypoints/popup/pages/EmailsList';
|
||||
import Home from '@/entrypoints/popup/pages/Home';
|
||||
import Index from '@/entrypoints/popup/pages/Index';
|
||||
import Login from '@/entrypoints/popup/pages/Login';
|
||||
import Logout from '@/entrypoints/popup/pages/Logout';
|
||||
import Reinitialize from '@/entrypoints/popup/pages/Reinitialize';
|
||||
import Settings from '@/entrypoints/popup/pages/Settings';
|
||||
import Unlock from '@/entrypoints/popup/pages/Unlock';
|
||||
import UnlockSuccess from '@/entrypoints/popup/pages/UnlockSuccess';
|
||||
import Upgrade from '@/entrypoints/popup/pages/Upgrade';
|
||||
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
|
||||
import '@/entrypoints/popup/style.css';
|
||||
|
||||
/**
|
||||
@@ -43,7 +49,12 @@ const App: React.FC = () => {
|
||||
|
||||
// Add these route configurations
|
||||
const routes: RouteConfig[] = [
|
||||
{ path: '/', element: <Home />, showBackButton: false },
|
||||
{ path: '/', element: <Index />, showBackButton: false },
|
||||
{ path: '/reinitialize', element: <Reinitialize />, showBackButton: false },
|
||||
{ path: '/login', element: <Login />, showBackButton: false },
|
||||
{ path: '/unlock', element: <Unlock />, showBackButton: false },
|
||||
{ path: '/unlock-success', element: <UnlockSuccess />, showBackButton: false },
|
||||
{ path: '/upgrade', element: <Upgrade />, showBackButton: false },
|
||||
{ path: '/auth-settings', element: <AuthSettings />, showBackButton: true, title: 'Settings' },
|
||||
{ path: '/credentials', element: <CredentialsList />, showBackButton: false },
|
||||
{ path: '/credentials/add', element: <CredentialAddEdit />, showBackButton: true, title: 'Add credential' },
|
||||
@@ -74,44 +85,44 @@ const App: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<div className="min-h-screen min-w-[350px] bg-white dark:bg-gray-900 flex flex-col max-h-[600px]">
|
||||
{isLoading && (
|
||||
<div className="fixed inset-0 bg-white dark:bg-gray-900 z-50 flex items-center justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
)}
|
||||
<NavigationProvider>
|
||||
<div className="min-h-screen min-w-[350px] bg-white dark:bg-gray-900 flex flex-col max-h-[600px]">
|
||||
{isLoading && (
|
||||
<div className="fixed inset-0 bg-white dark:bg-gray-900 z-50 flex items-center justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<GlobalStateChangeHandler />
|
||||
<Header
|
||||
routes={routes}
|
||||
rightButtons={headerButtons}
|
||||
/>
|
||||
<Header
|
||||
routes={routes}
|
||||
rightButtons={headerButtons}
|
||||
/>
|
||||
|
||||
<main
|
||||
className="flex-1 overflow-y-auto bg-gray-100 dark:bg-gray-900"
|
||||
style={{
|
||||
paddingTop: '64px',
|
||||
height: 'calc(100% - 120px)',
|
||||
}}
|
||||
>
|
||||
<div className="p-4 mb-16">
|
||||
{message && (
|
||||
<p className="text-red-500 mb-4">{message}</p>
|
||||
)}
|
||||
<Routes>
|
||||
{routes.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={route.element}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
<main
|
||||
className="flex-1 overflow-y-auto bg-gray-100 dark:bg-gray-900"
|
||||
style={{
|
||||
paddingTop: '64px',
|
||||
height: 'calc(100% - 120px)',
|
||||
}}
|
||||
>
|
||||
<div className="p-4 mb-16">
|
||||
{message && (
|
||||
<p className="text-red-500 mb-4">{message}</p>
|
||||
)}
|
||||
<Routes>
|
||||
{routes.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={route.element}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
</div>
|
||||
</main>
|
||||
<BottomNav />
|
||||
</div>
|
||||
</NavigationProvider>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
const [lastEmailId, setLastEmailId] = useState<number>(0);
|
||||
const [isSpamOk, setIsSpamOk] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSupportedDomain, setIsSupportedDomain] = useState(false);
|
||||
const webApi = useWebApi();
|
||||
const dbContext = useDb();
|
||||
|
||||
@@ -35,6 +36,15 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
return publicEmailDomains.some(domain => emailAddress.toLowerCase().endsWith(domain));
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the email is a private domain.
|
||||
*/
|
||||
const isPrivateDomain = async (emailAddress: string): Promise<boolean> => {
|
||||
// Get metadata from storage
|
||||
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[] ?? [];
|
||||
return privateEmailDomains.some(domain => emailAddress.toLowerCase().endsWith(domain));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Loads the latest emails from the server and decrypts them locally if needed.
|
||||
@@ -43,7 +53,15 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
try {
|
||||
setError(null);
|
||||
const isPublic = await isPublicDomain(email);
|
||||
const isPrivate = await isPrivateDomain(email);
|
||||
const isSupported = isPublic || isPrivate;
|
||||
|
||||
setIsSpamOk(isPublic);
|
||||
setIsSupportedDomain(isSupported);
|
||||
|
||||
if (!isSupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPublic) {
|
||||
// For public domains (SpamOK), use the SpamOK API directly
|
||||
@@ -73,7 +91,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
}
|
||||
|
||||
setEmails(latestMails);
|
||||
} else {
|
||||
} else if (isPrivate) {
|
||||
// For private domains, use existing encrypted email logic
|
||||
try {
|
||||
/**
|
||||
@@ -134,6 +152,11 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
return () : void => clearInterval(interval);
|
||||
}, [email, loading, webApi, dbContext]);
|
||||
|
||||
// Don't render anything if the domain is not supported
|
||||
if (!isSupportedDomain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
|
||||
/**
|
||||
* Global state change handler component which listens for global state changes and e.g. redirects user to login
|
||||
* page if login state changes.
|
||||
*/
|
||||
const GlobalStateChangeHandler: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const lastLoginState = useRef(authContext.isLoggedIn);
|
||||
const initialRender = useRef(true);
|
||||
|
||||
/**
|
||||
* Listen for auth logged in changes and redirect to home page if logged in state changes to handle logins and logouts.
|
||||
*/
|
||||
useEffect(() => {
|
||||
// Only navigate when auth state is different from the last state we acted on.
|
||||
if (lastLoginState.current !== authContext.isLoggedIn) {
|
||||
lastLoginState.current = authContext.isLoggedIn;
|
||||
|
||||
/**
|
||||
* Skip the first auth state change to avoid redirecting when popup opens for the first time
|
||||
* which already causes the auth state to change from false to true.
|
||||
*/
|
||||
if (initialRender.current) {
|
||||
initialRender.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Redirect to home page if logged in state changes.
|
||||
navigate('/');
|
||||
}
|
||||
}, [authContext.isLoggedIn]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default GlobalStateChangeHandler;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { HeaderIcon, HeaderIconType } from './icons/HeaderIcons';
|
||||
import { HeaderIcon, HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
|
||||
type HeaderButtonProps = {
|
||||
onClick: () => void;
|
||||
|
||||
@@ -8,7 +8,8 @@ export enum HeaderIconType {
|
||||
RELOAD = 'reload',
|
||||
EXTERNAL_LINK = 'external_link',
|
||||
SAVE = 'save',
|
||||
PLUS = 'plus'
|
||||
PLUS = 'plus',
|
||||
TAB = 'tab'
|
||||
}
|
||||
|
||||
type HeaderIconProps = {
|
||||
@@ -156,6 +157,28 @@ export const HeaderIcon: React.FC<HeaderIconProps> = ({ type, className = 'w-5 h
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
),
|
||||
[HeaderIconType.TAB]: (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
type TabName = 'credentials' | 'emails' | 'settings';
|
||||
|
||||
/**
|
||||
* Bottom nav component.
|
||||
*/
|
||||
const BottomNav: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [currentTab, setCurrentTab] = useState<TabName>('credentials');
|
||||
|
||||
// Add effect to update currentTab based on route
|
||||
useEffect(() => {
|
||||
const path = location.pathname.substring(1) as TabName;
|
||||
if (['credentials', 'emails', 'settings'].includes(path)) {
|
||||
setCurrentTab(path);
|
||||
const path = location.pathname.substring(1); // Remove leading slash
|
||||
const tabNames: TabName[] = ['credentials', 'emails', 'settings'];
|
||||
|
||||
// Find the first tab name that matches the start of the path
|
||||
const matchingTab = tabNames.find(tab => path === tab || path.startsWith(`${tab}/`));
|
||||
if (matchingTab) {
|
||||
setCurrentTab(matchingTab);
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
@@ -32,7 +31,11 @@ const BottomNav: React.FC = () => {
|
||||
navigate(`/${tab}`);
|
||||
};
|
||||
|
||||
if (!authContext.isLoggedIn || !dbContext.dbAvailable) {
|
||||
// Auth pages that don't show bottom navigation but still show header
|
||||
const authPages = ['/login', '/auth-settings', '/unlock', '/unlock-success', '/upgrade'];
|
||||
const isAuthPage = authPages.includes(location.pathname);
|
||||
|
||||
if (isAuthPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,11 @@ const Header: React.FC<HeaderProps> = ({
|
||||
* Handle logo click.
|
||||
*/
|
||||
const logoClick = () : void => {
|
||||
// Don't navigate if on upgrade page or login page
|
||||
if (location.pathname === '/upgrade' || location.pathname === '/login' || location.pathname === '/unlock') {
|
||||
return;
|
||||
}
|
||||
|
||||
// If logged in, navigate to credentials.
|
||||
if (authContext.isLoggedIn) {
|
||||
navigate('/credentials');
|
||||
@@ -94,16 +99,19 @@ const Header: React.FC<HeaderProps> = ({
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{!authContext.isLoggedIn ? (
|
||||
<button
|
||||
id="settings"
|
||||
onClick={(handleSettings)}
|
||||
className="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<span className="sr-only">Settings</span>
|
||||
<svg className="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<>
|
||||
{rightButtons}
|
||||
<button
|
||||
id="settings"
|
||||
onClick={(handleSettings)}
|
||||
className="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<span className="sr-only">Settings</span>
|
||||
<svg className="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
rightButtons
|
||||
)}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
|
||||
/**
|
||||
* User menu component.
|
||||
*/
|
||||
const UserMenu: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
/**
|
||||
* Handle logout.
|
||||
*/
|
||||
const handleLogout = async () : Promise<void> => {
|
||||
await authContext.logout();
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||||
<span className="text-primary-600 dark:text-primary-400 text-lg font-medium">
|
||||
{authContext.username?.[0]?.toUpperCase() || '?'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{authContext.username}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Logged in
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-4 py-2 text-sm font-medium text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserMenu;
|
||||
@@ -1,30 +1,21 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
|
||||
import { storage } from '#imports';
|
||||
import { useApiUrl } from '@/entrypoints/popup/utils/ApiUrlUtility';
|
||||
|
||||
/**
|
||||
* Component for displaying the login server information.
|
||||
*/
|
||||
const LoginServerInfo: React.FC = () => {
|
||||
const [baseUrl, setBaseUrl] = useState<string>('');
|
||||
const { loadApiUrl, getDisplayUrl } = useApiUrl();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Loads the base URL for the login server.
|
||||
*/
|
||||
const loadApiUrl = async () : Promise<void> => {
|
||||
const apiUrl = await storage.getItem('local:apiUrl') as string;
|
||||
setBaseUrl(apiUrl ?? AppInfo.DEFAULT_API_URL);
|
||||
};
|
||||
loadApiUrl();
|
||||
}, []);
|
||||
|
||||
const isDefaultServer = !baseUrl || baseUrl === AppInfo.DEFAULT_API_URL;
|
||||
const displayUrl = isDefaultServer ? 'aliasvault.net' : new URL(baseUrl).hostname;
|
||||
}, [loadApiUrl]);
|
||||
|
||||
/**
|
||||
* Handles the click event for the login server information.
|
||||
@@ -41,7 +32,7 @@ const LoginServerInfo: React.FC = () => {
|
||||
type="button"
|
||||
className="text-orange-500 hover:text-orange-600 dark:text-orange-400 dark:hover:text-orange-500 underline"
|
||||
>
|
||||
{displayUrl}
|
||||
{getDisplayUrl()}
|
||||
</button>)
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -20,8 +20,8 @@ const Modal: React.FC<IModalProps> = ({
|
||||
onConfirm,
|
||||
title,
|
||||
message,
|
||||
confirmText = 'Confirm',
|
||||
cancelText = 'Cancel',
|
||||
confirmText = '',
|
||||
cancelText = '',
|
||||
variant = 'default'
|
||||
}) => {
|
||||
if (!isOpen) {
|
||||
@@ -75,20 +75,24 @@ const Modal: React.FC<IModalProps> = ({
|
||||
|
||||
{/* Actions */}
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
type="button"
|
||||
className={`inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:ml-3 sm:w-auto ${confirmButtonClass}`}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:mt-0 sm:w-auto"
|
||||
onClick={onClose}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
{confirmText && (
|
||||
<button
|
||||
type="button"
|
||||
className={`inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:ml-3 sm:w-auto ${confirmButtonClass}`}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
)}
|
||||
{cancelText && (
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:mt-0 sm:w-auto"
|
||||
onClick={onClose}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,17 +40,20 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
* @returns object containing whether the user is logged in.
|
||||
*/
|
||||
const initializeAuth = useCallback(async () : Promise<{ isLoggedIn: boolean }> => {
|
||||
let isLoggedIn = false;
|
||||
|
||||
const accessToken = await storage.getItem('local:accessToken') as string;
|
||||
const refreshToken = await storage.getItem('local:refreshToken') as string;
|
||||
const username = await storage.getItem('local:username') as string;
|
||||
if (accessToken && refreshToken && username) {
|
||||
setUsername(username);
|
||||
setIsLoggedIn(true);
|
||||
isLoggedIn = true;
|
||||
}
|
||||
setIsInitialized(true);
|
||||
|
||||
return { isLoggedIn };
|
||||
}, [setUsername, setIsLoggedIn, isLoggedIn]);
|
||||
}, [setUsername, setIsLoggedIn]);
|
||||
|
||||
/**
|
||||
* Check for tokens in browser local storage on initial load when this context is mounted.
|
||||
|
||||
@@ -12,10 +12,11 @@ type DbContextType = {
|
||||
sqliteClient: SqliteClient | null;
|
||||
dbInitialized: boolean;
|
||||
dbAvailable: boolean;
|
||||
initializeDatabase: (vaultResponse: VaultResponse, derivedKey: string) => Promise<void>;
|
||||
initializeDatabase: (vaultResponse: VaultResponse, derivedKey: string) => Promise<SqliteClient>;
|
||||
clearDatabase: () => void;
|
||||
getVaultMetadata: () => Promise<VaultMetadata | null>;
|
||||
setCurrentVaultRevisionNumber: (revisionNumber: number) => Promise<void>;
|
||||
hasPendingMigrations: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
const DbContext = createContext<DbContextType | undefined>(undefined);
|
||||
@@ -76,6 +77,8 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
};
|
||||
|
||||
await sendMessage('STORE_VAULT', request, 'background');
|
||||
|
||||
return client;
|
||||
}, []);
|
||||
|
||||
const checkStoredVault = useCallback(async () => {
|
||||
@@ -88,6 +91,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
setSqliteClient(client);
|
||||
setDbInitialized(true);
|
||||
setDbAvailable(true);
|
||||
|
||||
setVaultMetadata({
|
||||
publicEmailDomains: response.publicEmailDomains ?? [],
|
||||
privateEmailDomains: response.privateEmailDomains ?? [],
|
||||
@@ -122,6 +126,16 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
});
|
||||
}, [vaultMetadata]);
|
||||
|
||||
/**
|
||||
* Check if there are pending migrations.
|
||||
*/
|
||||
const hasPendingMigrations = useCallback(async () => {
|
||||
if (!sqliteClient) {
|
||||
return false;
|
||||
}
|
||||
return await sqliteClient.hasPendingMigrations();
|
||||
}, [sqliteClient]);
|
||||
|
||||
/**
|
||||
* Check if database is initialized and try to retrieve vault from background
|
||||
*/
|
||||
@@ -137,6 +151,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
const clearDatabase = useCallback(() : void => {
|
||||
setSqliteClient(null);
|
||||
setDbInitialized(false);
|
||||
setDbAvailable(false);
|
||||
sendMessage('CLEAR_VAULT', {}, 'background');
|
||||
}, []);
|
||||
|
||||
@@ -148,7 +163,8 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
clearDatabase,
|
||||
getVaultMetadata,
|
||||
setCurrentVaultRevisionNumber,
|
||||
}), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, clearDatabase, getVaultMetadata, setCurrentVaultRevisionNumber]);
|
||||
hasPendingMigrations,
|
||||
}), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, clearDatabase, getVaultMetadata, setCurrentVaultRevisionNumber, hasPendingMigrations]);
|
||||
|
||||
return (
|
||||
<DbContext.Provider value={contextValue}>
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
const LAST_VISITED_PAGE_KEY = 'session:lastVisitedPage';
|
||||
const LAST_VISITED_TIME_KEY = 'session:lastVisitedTime';
|
||||
const NAVIGATION_HISTORY_KEY = 'session:navigationHistory';
|
||||
|
||||
type NavigationHistoryEntry = {
|
||||
pathname: string;
|
||||
search: string;
|
||||
hash: string;
|
||||
};
|
||||
|
||||
type NavigationContextType = {
|
||||
storeCurrentPage: () => Promise<void>;
|
||||
isFullyInitialized: boolean;
|
||||
requiresAuth: boolean;
|
||||
};
|
||||
|
||||
const NavigationContext = createContext<NavigationContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Navigation provider component that handles storing the last visited page.
|
||||
*/
|
||||
export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const location = useLocation();
|
||||
|
||||
// Auth and DB state
|
||||
const { isInitialized: authInitialized, isLoggedIn } = useAuth();
|
||||
const { dbInitialized, dbAvailable, upgradeRequired } = useDb();
|
||||
|
||||
// Derived state
|
||||
const isFullyInitialized = authInitialized && dbInitialized;
|
||||
const requiresAuth = isFullyInitialized && (!isLoggedIn || (!dbAvailable && !upgradeRequired));
|
||||
|
||||
/**
|
||||
* Store the current page path, timestamp, and navigation history in storage.
|
||||
*/
|
||||
const storeCurrentPage = useCallback(async (): Promise<void> => {
|
||||
// Pages that are not allowed to be stored as these are auth conditional pages.
|
||||
const notAllowedPaths = ['/', '/reinitialize', '/login', '/unlock', '/unlock-success', '/auth-settings', '/upgrade', '/logout'];
|
||||
|
||||
// Only store the page if we're fully initialized and don't need auth
|
||||
if (isFullyInitialized && !requiresAuth && !notAllowedPaths.includes(location.pathname)) {
|
||||
// Split the path into segments and build up the history
|
||||
const segments = location.pathname.split('/').filter(Boolean);
|
||||
const historyEntries: NavigationHistoryEntry[] = [];
|
||||
|
||||
let currentPath = '';
|
||||
for (const segment of segments) {
|
||||
currentPath += '/' + segment;
|
||||
historyEntries.push({
|
||||
pathname: currentPath,
|
||||
search: location.search,
|
||||
hash: location.hash,
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
storage.setItem(LAST_VISITED_PAGE_KEY, location.pathname),
|
||||
storage.setItem(LAST_VISITED_TIME_KEY, Date.now()),
|
||||
storage.setItem(NAVIGATION_HISTORY_KEY, historyEntries),
|
||||
]);
|
||||
}
|
||||
}, [location, isFullyInitialized, requiresAuth]);
|
||||
|
||||
// Store the current page whenever it changes
|
||||
useEffect(() => {
|
||||
if (isFullyInitialized) {
|
||||
storeCurrentPage();
|
||||
}
|
||||
}, [location.pathname, location.search, location.hash, isFullyInitialized, storeCurrentPage]);
|
||||
|
||||
const contextValue = useMemo(() => ({
|
||||
storeCurrentPage,
|
||||
isFullyInitialized,
|
||||
requiresAuth
|
||||
}), [storeCurrentPage, isFullyInitialized, requiresAuth]);
|
||||
|
||||
return (
|
||||
<NavigationContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</NavigationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to access the navigation context.
|
||||
* @returns The navigation context
|
||||
*/
|
||||
export const useNavigation = (): NavigationContextType => {
|
||||
const context = useContext(NavigationContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useNavigation must be used within a NavigationProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import { VaultUploadResponse as messageVaultUploadResponse } from '@/utils/types
|
||||
type VaultMutationOptions = {
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
skipSyncCheck?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,9 +70,12 @@ export function useVaultMutate() : {
|
||||
await dbContext.setCurrentVaultRevisionNumber(response.newRevisionNumber);
|
||||
options.onSuccess?.();
|
||||
} else if (response.status === 1) {
|
||||
// Note: vault merge is no longer allowed by the API as of 0.20.0, updates with the same revision number are rejected. So this check can be removed later.
|
||||
throw new Error('Vault merge required. Please login via the web app to merge the multiple pending updates to your vault.');
|
||||
} else if (response.status === 2) {
|
||||
throw new Error('Your vault is outdated. Please login on the AliasVault website and follow the steps.');
|
||||
} else {
|
||||
throw new Error('Failed to upload vault to server');
|
||||
throw new Error('Failed to upload vault to server. Please try again by re-opening the app.');
|
||||
}
|
||||
} catch (error) {
|
||||
// Check if it's a network error
|
||||
@@ -99,6 +103,13 @@ export function useVaultMutate() : {
|
||||
setIsLoading(true);
|
||||
setSyncStatus('Checking for vault updates');
|
||||
|
||||
// Skip sync check if requested (e.g., during upgrade operations)
|
||||
if (options.skipSyncCheck) {
|
||||
setSyncStatus('Executing operation...');
|
||||
await executeMutateOperation(operation, options);
|
||||
return;
|
||||
}
|
||||
|
||||
await syncVault({
|
||||
/**
|
||||
* Handle the status update.
|
||||
|
||||
@@ -37,6 +37,7 @@ type VaultSyncOptions = {
|
||||
onError?: (error: string) => void;
|
||||
onStatus?: (message: string) => void;
|
||||
_onOffline?: () => void;
|
||||
onUpgradeRequired?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +51,7 @@ export const useVaultSync = () : {
|
||||
const webApi = useWebApi();
|
||||
|
||||
const syncVault = useCallback(async (options: VaultSyncOptions = {}) => {
|
||||
const { initialSync = false, onSuccess, onError, onStatus, _onOffline } = options;
|
||||
const { initialSync = false, onSuccess, onError, onStatus, _onOffline, onUpgradeRequired } = options;
|
||||
|
||||
// For the initial sync, we add an artifical delay to various steps which makes it feel more fluid.
|
||||
const enableDelay = initialSync;
|
||||
@@ -113,21 +114,47 @@ export const useVaultSync = () : {
|
||||
try {
|
||||
// Get derived key from background worker
|
||||
const passwordHashBase64 = await sendMessage('GET_DERIVED_KEY', {}, 'background') as string;
|
||||
await dbContext.initializeDatabase(vaultResponseJson as VaultResponse, passwordHashBase64);
|
||||
const sqliteClient = await dbContext.initializeDatabase(vaultResponseJson as VaultResponse, passwordHashBase64);
|
||||
|
||||
// Check if the current vault version is known and up to date, if not known trigger an exception, if not up to date redirect to the upgrade page.
|
||||
if (await sqliteClient.hasPendingMigrations()) {
|
||||
onUpgradeRequired?.();
|
||||
return false;
|
||||
}
|
||||
|
||||
onSuccess?.(true);
|
||||
return true;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Check if it's a version-related error (app needs to be updated)
|
||||
if (error instanceof Error && error.message.includes('This browser extension is outdated')) {
|
||||
await webApi.logout(error.message);
|
||||
onError?.(error.message);
|
||||
return false;
|
||||
}
|
||||
// Vault could not be decrypted, throw an error
|
||||
throw new Error('Vault could not be decrypted, if problem persists please logout and login again.');
|
||||
throw new Error('Vault could not be decrypted, if the problem persists please logout and login again.');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the vault is up to date, if not, redirect to the upgrade page.
|
||||
if (await dbContext.hasPendingMigrations()) {
|
||||
onUpgradeRequired?.();
|
||||
return false;
|
||||
}
|
||||
|
||||
await withMinimumDelay(() => Promise.resolve(onSuccess?.(false)), 300, enableDelay);
|
||||
return false;
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error during vault sync';
|
||||
console.error('Vault sync error:', err);
|
||||
|
||||
// Check if it's a version-related error (app needs to be updated)
|
||||
if (errorMessage.includes('This browser extension is outdated')) {
|
||||
await webApi.logout(errorMessage);
|
||||
onError?.(errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if it's a network error
|
||||
* TODO: browser extension does not support offline mode yet.
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AliasVault</title>
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/icon/16.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/icon/32.png" />
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="/icon/48.png" />
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/icon/192.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/icon/192.png" />
|
||||
<link href="~/assets/tailwind.css" rel="stylesheet" />
|
||||
<meta name="manifest.type" content="browser_action" />
|
||||
</head>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
import { GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, DISABLED_SITES_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants';
|
||||
|
||||
@@ -55,6 +57,7 @@ const AuthSettings: React.FC = () => {
|
||||
const [customClientUrl, setCustomClientUrl] = useState<string>('');
|
||||
const [isGloballyEnabled, setIsGloballyEnabled] = useState<boolean>(true);
|
||||
const [errors, setErrors] = useState<{ apiUrl?: string; clientUrl?: string }>({});
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
@@ -83,10 +86,11 @@ const AuthSettings: React.FC = () => {
|
||||
} else {
|
||||
setSelectedOption(DEFAULT_OPTIONS[0].value);
|
||||
}
|
||||
setIsInitialLoading(false);
|
||||
};
|
||||
|
||||
loadStoredSettings();
|
||||
}, []);
|
||||
}, [setIsInitialLoading]);
|
||||
|
||||
/**
|
||||
* Handle option change
|
||||
|
||||
@@ -4,11 +4,12 @@ import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { FormInput } from '@/entrypoints/popup/components/FormInput';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/icons/HeaderIcons';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import Modal from '@/entrypoints/popup/components/Modal';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
@@ -24,6 +25,13 @@ import { useLoading } from '../context/LoadingContext';
|
||||
|
||||
type CredentialMode = 'random' | 'manual';
|
||||
|
||||
// Persisted form data type used for JSON serialization.
|
||||
type PersistedFormData = {
|
||||
credentialId: string | null;
|
||||
mode: CredentialMode;
|
||||
formValues: Omit<Credential, 'Logo'> & { Logo?: string | null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema for the credential form.
|
||||
*/
|
||||
@@ -67,7 +75,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
const [mode, setMode] = useState<CredentialMode>('random');
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const [localLoading, setLocalLoading] = useState(false);
|
||||
const [localLoading, setLocalLoading] = useState(true);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const webApi = useWebApi();
|
||||
@@ -94,19 +102,128 @@ const CredentialAddEdit: React.FC = () => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Persists the current form values to storage
|
||||
* @returns Promise that resolves when the form values are persisted
|
||||
*/
|
||||
const persistFormValues = useCallback(async (): Promise<void> => {
|
||||
if (localLoading) {
|
||||
// Do not persist values if the page is still loading.
|
||||
return;
|
||||
}
|
||||
|
||||
const formValues = watch();
|
||||
const persistedData: PersistedFormData = {
|
||||
credentialId: id || null,
|
||||
mode,
|
||||
formValues: {
|
||||
...formValues,
|
||||
Logo: null // Don't persist the Logo field as it can't be user modified in the UI.
|
||||
}
|
||||
};
|
||||
await sendMessage('PERSIST_FORM_VALUES', JSON.stringify(persistedData), 'background');
|
||||
}, [watch, id, mode, localLoading]);
|
||||
|
||||
/**
|
||||
* Watch for mode changes and persist form values
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!localLoading) {
|
||||
void persistFormValues();
|
||||
}
|
||||
}, [mode, persistFormValues, localLoading]);
|
||||
|
||||
// Watch for form changes and persist them
|
||||
useEffect(() => {
|
||||
const subscription = watch(() => {
|
||||
void persistFormValues();
|
||||
});
|
||||
return (): void => subscription.unsubscribe();
|
||||
}, [watch, persistFormValues]);
|
||||
|
||||
// If we received an ID, we're in edit mode
|
||||
const isEditMode = id !== undefined && id.length > 0;
|
||||
|
||||
/**
|
||||
* Loads persisted form values from storage. This is used to keep track of form changes
|
||||
* and restore them when the page is reloaded. The browser extension popup will close
|
||||
* automatically by clicking outside of the popup, but with this logic we can restore
|
||||
* the form values when the page is reloaded so the user can continue their mutation operation.
|
||||
*
|
||||
* @returns Promise that resolves when the form values are loaded
|
||||
*/
|
||||
const loadPersistedValues = useCallback(async (): Promise<void> => {
|
||||
const persistedData = await sendMessage('GET_PERSISTED_FORM_VALUES', null, 'background') as string | null;
|
||||
|
||||
// Try to parse the persisted data as a JSON object.
|
||||
try {
|
||||
let persistedDataObject: PersistedFormData | null = null;
|
||||
try {
|
||||
if (persistedData) {
|
||||
persistedDataObject = JSON.parse(persistedData) as PersistedFormData;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing persisted data:', error);
|
||||
}
|
||||
|
||||
// Check if the object has a value and is not null
|
||||
const objectEmpty = persistedDataObject === null || persistedDataObject === undefined;
|
||||
if (objectEmpty) {
|
||||
// If the persisted data object is empty, we don't have any values to restore and can exit early.
|
||||
setLocalLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentPage = persistedDataObject?.credentialId == id;
|
||||
if (persistedDataObject && isCurrentPage) {
|
||||
// Only restore if the persisted credential ID matches current page
|
||||
setMode(persistedDataObject.mode);
|
||||
Object.entries(persistedDataObject.formValues).forEach(([key, value]) => {
|
||||
setValue(key as keyof Credential, value as Credential[keyof Credential]);
|
||||
});
|
||||
} else {
|
||||
console.error('Persisted values do not match current page');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading persisted data:', error);
|
||||
}
|
||||
|
||||
// Set local loading state to false which also activates the persisting of form value changes from this point on.
|
||||
setLocalLoading(false);
|
||||
}, [setValue, id, setMode, setLocalLoading]);
|
||||
|
||||
/**
|
||||
* Clears persisted form values from storage
|
||||
* @returns Promise that resolves when the form values are cleared
|
||||
*/
|
||||
const clearPersistedValues = useCallback(async (): Promise<void> => {
|
||||
await sendMessage('CLEAR_PERSISTED_FORM_VALUES', null, 'background');
|
||||
}, []);
|
||||
|
||||
// Clear persisted values when the page is unmounted.
|
||||
useEffect(() => {
|
||||
return (): void => {
|
||||
void clearPersistedValues();
|
||||
};
|
||||
}, [clearPersistedValues]);
|
||||
|
||||
/**
|
||||
* Load an existing credential from the database in edit mode.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!dbContext?.sqliteClient || !id) {
|
||||
if (!dbContext?.sqliteClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
// On create mode, focus the service name field after a short delay to ensure the component is mounted.
|
||||
setTimeout(() => {
|
||||
serviceNameRef.current?.focus();
|
||||
}, 100);
|
||||
setIsInitialLoading(false);
|
||||
|
||||
// Load persisted form values if they exist.
|
||||
loadPersistedValues();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,16 +239,19 @@ const CredentialAddEdit: React.FC = () => {
|
||||
});
|
||||
|
||||
setMode('manual');
|
||||
setIsInitialLoading(false);
|
||||
|
||||
// On create mode, focus the service name field after a short delay to ensure the component is mounted
|
||||
// Check for persisted values that might override the loaded values if they exist.
|
||||
loadPersistedValues();
|
||||
} else {
|
||||
console.error('Credential not found');
|
||||
navigate('/credentials');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading credential:', err);
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue]);
|
||||
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue, loadPersistedValues]);
|
||||
|
||||
/**
|
||||
* Handle the delete button click.
|
||||
@@ -148,10 +268,11 @@ const CredentialAddEdit: React.FC = () => {
|
||||
* Navigate to the credentials list page on success.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
void clearPersistedValues();
|
||||
navigate('/credentials');
|
||||
}
|
||||
});
|
||||
}, [id, executeVaultMutation, dbContext.sqliteClient, navigate]);
|
||||
}, [id, executeVaultMutation, dbContext.sqliteClient, navigate, clearPersistedValues]);
|
||||
|
||||
/**
|
||||
* Initialize the identity and password generators with settings from user's vault.
|
||||
@@ -176,7 +297,11 @@ const CredentialAddEdit: React.FC = () => {
|
||||
const generateRandomAlias = useCallback(async () => {
|
||||
const { identityGenerator, passwordGenerator } = await initializeGenerators();
|
||||
|
||||
const identity = identityGenerator.generateRandomIdentity();
|
||||
// Get gender preference from database
|
||||
const genderPreference = dbContext.sqliteClient!.getDefaultIdentityGender();
|
||||
|
||||
// Generate identity with gender preference
|
||||
const identity = identityGenerator.generateRandomIdentity(genderPreference);
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
|
||||
const metadata = await dbContext.getVaultMetadata();
|
||||
@@ -312,6 +437,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
* Navigate to the credential details page on success.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
void clearPersistedValues();
|
||||
// If in add mode, navigate to the credential details page.
|
||||
if (!isEditMode) {
|
||||
// Navigate to the credential details page.
|
||||
@@ -322,7 +448,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [isEditMode, dbContext.sqliteClient, executeVaultMutation, navigate, mode, watch, generateRandomAlias, webApi]);
|
||||
}, [isEditMode, dbContext.sqliteClient, executeVaultMutation, navigate, mode, watch, generateRandomAlias, webApi, clearPersistedValues]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
@@ -380,6 +506,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
title="Delete Credential"
|
||||
message="Are you sure you want to delete this credential? This action cannot be undone."
|
||||
confirmText="Delete"
|
||||
cancelText="Cancel"
|
||||
variant="danger"
|
||||
/>
|
||||
|
||||
|
||||
@@ -10,10 +10,11 @@ import {
|
||||
NotesBlock
|
||||
} from '@/entrypoints/popup/components/CredentialDetails';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/icons/HeaderIcons';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
@@ -28,30 +29,11 @@ const CredentialDetails: React.FC = (): React.ReactElement => {
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
|
||||
/**
|
||||
* Check if the current page is an expanded popup.
|
||||
*/
|
||||
const isPopup = (): boolean => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('expanded') === 'true';
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the credential details in a new expanded popup.
|
||||
*/
|
||||
const openInNewPopup = useCallback((): void => {
|
||||
const width = 800;
|
||||
const height = 1000;
|
||||
const left = window.screen.width / 2 - width / 2;
|
||||
const top = window.screen.height / 2 - height / 2;
|
||||
|
||||
window.open(
|
||||
`popup.html?expanded=true#/credentials/${id}`,
|
||||
'CredentialDetails',
|
||||
`width=${width},height=${height},left=${left},top=${top},popup=true`
|
||||
);
|
||||
|
||||
window.close();
|
||||
PopoutUtility.openInNewPopup(`/credentials/${id}`);
|
||||
}, [id]);
|
||||
|
||||
/**
|
||||
@@ -62,7 +44,7 @@ const CredentialDetails: React.FC = (): React.ReactElement => {
|
||||
}, [id, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPopup()) {
|
||||
if (PopoutUtility.isPopup()) {
|
||||
window.history.replaceState({}, '', `popup.html#/credentials`);
|
||||
window.history.pushState({}, '', `popup.html#/credentials/${id}`);
|
||||
}
|
||||
@@ -89,11 +71,13 @@ const CredentialDetails: React.FC = (): React.ReactElement => {
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
<HeaderButton
|
||||
onClick={openInNewPopup}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
{!PopoutUtility.isPopup() && (
|
||||
<HeaderButton
|
||||
onClick={openInNewPopup}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
)}
|
||||
<HeaderButton
|
||||
onClick={handleEdit}
|
||||
title="Edit credential"
|
||||
@@ -124,10 +108,10 @@ const CredentialDetails: React.FC = (): React.ReactElement => {
|
||||
email={credential.Alias.Email}
|
||||
/>
|
||||
)}
|
||||
<NotesBlock notes={credential.Notes} />
|
||||
<TotpBlock credentialId={credential.Id} />
|
||||
<LoginCredentialsBlock credential={credential} />
|
||||
<AliasBlock credential={credential} />
|
||||
<NotesBlock notes={credential.Notes} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import CredentialCard from '@/entrypoints/popup/components/CredentialCard';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/icons/HeaderIcons';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import ReloadButton from '@/entrypoints/popup/components/ReloadButton';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
@@ -11,6 +11,7 @@ import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsConte
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import { useVaultSync } from '@/entrypoints/popup/hooks/useVaultSync';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
@@ -70,13 +71,15 @@ const CredentialsList: React.FC = () => {
|
||||
onError: async (error) => {
|
||||
console.error('Error syncing vault:', error);
|
||||
await webApi.logout('Error while syncing vault, please re-authenticate.');
|
||||
navigate('/logout');
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error refreshing credentials:', err);
|
||||
await webApi.logout('Error while syncing vault, please re-authenticate.');
|
||||
navigate('/logout');
|
||||
}
|
||||
}, [dbContext, webApi, syncVault]);
|
||||
}, [dbContext, webApi, syncVault, navigate]);
|
||||
|
||||
/**
|
||||
* Get latest vault from server and refresh the credentials list.
|
||||
@@ -85,13 +88,19 @@ const CredentialsList: React.FC = () => {
|
||||
setIsLoading(true);
|
||||
await onRefresh();
|
||||
setIsLoading(false);
|
||||
setIsInitialLoading(false);
|
||||
}, [onRefresh, setIsLoading, setIsInitialLoading]);
|
||||
}, [onRefresh, setIsLoading]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
{!PopoutUtility.isPopup() && (
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
)}
|
||||
<HeaderButton
|
||||
onClick={handleAddCredential}
|
||||
title="Add new credential"
|
||||
@@ -117,16 +126,12 @@ const CredentialsList: React.FC = () => {
|
||||
const results = dbContext.sqliteClient?.getAllCredentials() ?? [];
|
||||
setCredentials(results);
|
||||
setIsLoading(false);
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
refreshCredentials();
|
||||
}, [dbContext?.sqliteClient, setIsLoading]);
|
||||
|
||||
// Call syncVaultAndRefresh when the page first mounts
|
||||
useEffect(() => {
|
||||
syncVaultAndRefresh();
|
||||
}, [syncVaultAndRefresh]);
|
||||
}, [dbContext?.sqliteClient, setIsLoading, setIsInitialLoading]);
|
||||
|
||||
// Add this function to filter credentials
|
||||
const filteredCredentials = credentials.filter(cred => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsConte
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import ConversionUtility from '@/entrypoints/popup/utils/ConversionUtility';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
import type { EmailAttachment, Email } from '@/utils/dist/shared/models/webapi';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
@@ -15,7 +16,7 @@ import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
|
||||
import HeaderButton from '../components/HeaderButton';
|
||||
import { HeaderIconType } from '../components/icons/HeaderIcons';
|
||||
import { HeaderIconType } from '../components/Icons/HeaderIcons';
|
||||
|
||||
/**
|
||||
* Email details page.
|
||||
@@ -35,7 +36,7 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
|
||||
useEffect(() => {
|
||||
// For popup windows, ensure we have proper history state for navigation
|
||||
if (isPopup()) {
|
||||
if (PopoutUtility.isPopup()) {
|
||||
// Clear existing history and create fresh entries
|
||||
window.history.replaceState({}, '', `popup.html#/emails`);
|
||||
window.history.pushState({}, '', `popup.html#/emails/${id}`);
|
||||
@@ -76,7 +77,7 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
const handleDelete = useCallback(async () : Promise<void> => {
|
||||
try {
|
||||
await webApi.delete(`Email/${id}`);
|
||||
if (isPopup()) {
|
||||
if (PopoutUtility.isPopup()) {
|
||||
window.close();
|
||||
} else {
|
||||
navigate('/emails');
|
||||
@@ -87,30 +88,10 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
}, [id, webApi, navigate]);
|
||||
|
||||
/**
|
||||
* Check if the current page is an expanded popup.
|
||||
*/
|
||||
const isPopup = () : boolean => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('expanded') === 'true';
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the credential details in a new expanded popup.
|
||||
* Open the email details in a new expanded popup.
|
||||
*/
|
||||
const openInNewPopup = useCallback((): void => {
|
||||
const width = 800;
|
||||
const height = 1000;
|
||||
const left = window.screen.width / 2 - width / 2;
|
||||
const top = window.screen.height / 2 - height / 2;
|
||||
|
||||
window.open(
|
||||
`popup.html?expanded=true#/emails/${id}`,
|
||||
'EmailDetails',
|
||||
`width=${width},height=${height},left=${left},top=${top},popup=true`
|
||||
);
|
||||
|
||||
// Close the current tab
|
||||
window.close();
|
||||
PopoutUtility.openInNewPopup(`/emails/${id}`);
|
||||
}, [id]);
|
||||
|
||||
/**
|
||||
@@ -165,11 +146,13 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
if (!headerButtonsConfigured) {
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
<HeaderButton
|
||||
onClick={openInNewPopup}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
{!PopoutUtility.isPopup() && (
|
||||
<HeaderButton
|
||||
onClick={openInNewPopup}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
)}
|
||||
<HeaderButton
|
||||
onClick={() => setShowDeleteModal(true)}
|
||||
title="Delete email"
|
||||
@@ -218,6 +201,7 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
title="Delete Email"
|
||||
message="Are you sure you want to delete this email? This action cannot be undone."
|
||||
confirmText="Delete"
|
||||
cancelText="Cancel"
|
||||
variant="danger"
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import ReloadButton from '@/entrypoints/popup/components/ReloadButton';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
import type { MailboxBulkRequest, MailboxBulkResponse, MailboxEmail } from '@/utils/dist/shared/models/webapi';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
@@ -18,6 +22,7 @@ import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
const EmailsList: React.FC = () => {
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [emails, setEmails] = useState<MailboxEmail[]>([]);
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
@@ -73,6 +78,23 @@ const EmailsList: React.FC = () => {
|
||||
loadEmails();
|
||||
}, [loadEmails]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = !PopoutUtility.isPopup() ? (
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
|
||||
return () => {
|
||||
setHeaderButtons(null);
|
||||
};
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
/**
|
||||
* Formats the date display for emails
|
||||
*/
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import Login from '@/entrypoints/popup/pages/Login';
|
||||
import Unlock from '@/entrypoints/popup/pages/Unlock';
|
||||
import UnlockSuccess from '@/entrypoints/popup/pages/UnlockSuccess';
|
||||
|
||||
/**
|
||||
* Home page that shows the correct page based on the user's authentication state.
|
||||
*/
|
||||
const Home: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const navigate = useNavigate();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const [isInlineUnlockMode, setIsInlineUnlockMode] = useState(false);
|
||||
|
||||
// Initialization state.
|
||||
const isFullyInitialized = authContext.isInitialized && dbContext.dbInitialized;
|
||||
const isAuthenticated = authContext.isLoggedIn;
|
||||
const isDatabaseAvailable = dbContext.dbAvailable;
|
||||
const requireLoginOrUnlock = isFullyInitialized && (!isAuthenticated || !isDatabaseAvailable || isInlineUnlockMode);
|
||||
|
||||
useEffect(() => {
|
||||
// Detect if the user is coming from the unlock page with mode=inline_unlock.
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const isInlineUnlockMode = urlParams.get('mode') === 'inline_unlock';
|
||||
setIsInlineUnlockMode(isInlineUnlockMode);
|
||||
|
||||
// Redirect to credentials if fully initialized and doesn't need unlock.
|
||||
if (isFullyInitialized && !requireLoginOrUnlock) {
|
||||
navigate('/credentials', { replace: true });
|
||||
}
|
||||
}, [isFullyInitialized, requireLoginOrUnlock, isInlineUnlockMode, navigate]);
|
||||
|
||||
// Show loading state if not fully initialized or when about to redirect to credentials.
|
||||
if (!isFullyInitialized || (isFullyInitialized && !requireLoginOrUnlock)) {
|
||||
// Global loading spinner will be shown by the parent component.
|
||||
return null;
|
||||
}
|
||||
|
||||
setIsInitialLoading(false);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
if (!isDatabaseAvailable) {
|
||||
return <Unlock />;
|
||||
}
|
||||
|
||||
if (isInlineUnlockMode) {
|
||||
return <UnlockSuccess onClose={() => setIsInlineUnlockMode(false)} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Home;
|
||||
20
apps/browser-extension/src/entrypoints/popup/pages/Index.tsx
Normal file
20
apps/browser-extension/src/entrypoints/popup/pages/Index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import { useNavigation } from '@/entrypoints/popup/context/NavigationContext';
|
||||
|
||||
/**
|
||||
* Home page that shows the correct page based on the user's authentication state.
|
||||
* Most of the navigation logic is now handled by NavigationContext.
|
||||
*/
|
||||
const Home: React.FC = () => {
|
||||
const { isFullyInitialized } = useNavigation();
|
||||
|
||||
if (!isFullyInitialized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Navigate to="/reinitialize" replace />;
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -1,13 +1,18 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import LoginServerInfo from '@/entrypoints/popup/components/LoginServerInfo';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
import SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
@@ -23,13 +28,15 @@ import { storage } from '#imports';
|
||||
* Login page
|
||||
*/
|
||||
const Login: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const [credentials, setCredentials] = useState({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
const { showLoading, hideLoading } = useLoading();
|
||||
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
|
||||
const [rememberMe, setRememberMe] = useState(true);
|
||||
const [loginResponse, setLoginResponse] = useState<LoginResponse | null>(null);
|
||||
const [passwordHashString, setPasswordHashString] = useState<string | null>(null);
|
||||
@@ -53,9 +60,29 @@ const Login: React.FC = () => {
|
||||
}
|
||||
|
||||
setClientUrl(clientUrl);
|
||||
setIsInitialLoading(false);
|
||||
};
|
||||
loadClientUrl();
|
||||
}, []);
|
||||
}, [setIsInitialLoading]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = !PopoutUtility.isPopup() ? (
|
||||
<>
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
</>
|
||||
) : null;
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
|
||||
return () => {
|
||||
setHeaderButtons(null);
|
||||
};
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
/**
|
||||
* Handle submit
|
||||
@@ -130,11 +157,28 @@ const Login: React.FC = () => {
|
||||
await authContext.setAuthTokens(ConversionUtility.normalizeUsername(credentials.username), validationResponse.token.token, validationResponse.token.refreshToken);
|
||||
|
||||
// Initialize the SQLite context with the new vault data.
|
||||
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
const sqliteClient = await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
|
||||
// Set logged in status to true which refreshes the app.
|
||||
await authContext.login();
|
||||
|
||||
// If there are pending migrations, redirect to the upgrade page.
|
||||
try {
|
||||
if (await sqliteClient.hasPendingMigrations()) {
|
||||
navigate('/upgrade', { replace: true });
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
await authContext.logout();
|
||||
setError(err instanceof Error ? err.message : 'An error occurred while checking for pending migrations.');
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to reinitialize page which will take care of the proper redirect.
|
||||
navigate('/reinitialize', { replace: true });
|
||||
|
||||
// Show app.
|
||||
hideLoading();
|
||||
} catch (err) {
|
||||
@@ -197,11 +241,28 @@ const Login: React.FC = () => {
|
||||
await authContext.setAuthTokens(ConversionUtility.normalizeUsername(credentials.username), validationResponse.token.token, validationResponse.token.refreshToken);
|
||||
|
||||
// Initialize the SQLite context with the new vault data.
|
||||
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
const sqliteClient = await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
|
||||
// Set logged in status to true which refreshes the app.
|
||||
await authContext.login();
|
||||
|
||||
// If there are pending migrations, redirect to the upgrade page.
|
||||
try {
|
||||
if (await sqliteClient.hasPendingMigrations()) {
|
||||
navigate('/upgrade', { replace: true });
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
await authContext.logout();
|
||||
setError(err instanceof Error ? err.message : 'An error occurred while checking for pending migrations.');
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to reinitialize page which will take care of the proper redirect.
|
||||
navigate('/reinitialize', { replace: true });
|
||||
|
||||
// Reset 2FA state and login response as it's no longer needed
|
||||
setTwoFactorRequired(false);
|
||||
setTwoFactorCode('');
|
||||
@@ -234,7 +295,7 @@ const Login: React.FC = () => {
|
||||
|
||||
if (twoFactorRequired) {
|
||||
return (
|
||||
<div className="max-w-md">
|
||||
<div>
|
||||
<form onSubmit={handleTwoFactorSubmit} className="bg-white dark:bg-gray-700 w-full shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
|
||||
@@ -291,7 +352,7 @@ const Login: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-md">
|
||||
<div>
|
||||
<form onSubmit={handleSubmit} className="bg-white dark:bg-gray-700 w-full shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
|
||||
|
||||
@@ -20,7 +20,7 @@ const Logout: React.FC = () => {
|
||||
*/
|
||||
const performLogout = async () : Promise<void> => {
|
||||
await webApi.logout();
|
||||
navigate('/');
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
performLogout();
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useVaultSync } from '@/entrypoints/popup/hooks/useVaultSync';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
const LAST_VISITED_PAGE_KEY = 'session:lastVisitedPage';
|
||||
const LAST_VISITED_TIME_KEY = 'session:lastVisitedTime';
|
||||
const NAVIGATION_HISTORY_KEY = 'session:navigationHistory';
|
||||
const PAGE_MEMORY_DURATION = 120 * 1000; // 2 minutes in milliseconds
|
||||
|
||||
type NavigationHistoryEntry = {
|
||||
pathname: string;
|
||||
search: string;
|
||||
hash: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize component that handles initial application setup, authentication checks,
|
||||
* vault synchronization, and state restoration.
|
||||
*/
|
||||
const Reinitialize: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const { syncVault } = useVaultSync();
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
// Auth and DB state
|
||||
const { isInitialized: authInitialized, isLoggedIn } = useAuth();
|
||||
const { dbInitialized, dbAvailable } = useDb();
|
||||
|
||||
// Derived state
|
||||
const isFullyInitialized = authInitialized && dbInitialized;
|
||||
const requiresAuth = isFullyInitialized && (!isLoggedIn || !dbAvailable);
|
||||
|
||||
/**
|
||||
* Restore the last visited page and navigation history if it was visited within the memory duration.
|
||||
*/
|
||||
const restoreLastPage = useCallback(async (): Promise<void> => {
|
||||
const [lastPage, lastVisitTime, savedHistory] = await Promise.all([
|
||||
storage.getItem(LAST_VISITED_PAGE_KEY) as Promise<string>,
|
||||
storage.getItem(LAST_VISITED_TIME_KEY) as Promise<number>,
|
||||
storage.getItem(NAVIGATION_HISTORY_KEY) as Promise<NavigationHistoryEntry[]>,
|
||||
]);
|
||||
|
||||
if (lastPage && lastVisitTime) {
|
||||
const timeSinceLastVisit = Date.now() - lastVisitTime;
|
||||
if (timeSinceLastVisit <= PAGE_MEMORY_DURATION) {
|
||||
// Restore the navigation history
|
||||
if (savedHistory?.length) {
|
||||
// First navigate to credentials page as the base
|
||||
navigate('/credentials', { replace: true });
|
||||
|
||||
// Then restore the history stack
|
||||
for (const entry of savedHistory) {
|
||||
navigate(entry.pathname + entry.search + entry.hash);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to simple navigation if no history
|
||||
navigate('/credentials', { replace: true });
|
||||
navigate(lastPage, { replace: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Duration has expired, clear all stored navigation data
|
||||
await Promise.all([
|
||||
storage.removeItem(LAST_VISITED_PAGE_KEY),
|
||||
storage.removeItem(LAST_VISITED_TIME_KEY),
|
||||
storage.removeItem(NAVIGATION_HISTORY_KEY),
|
||||
sendMessage('CLEAR_PERSISTED_FORM_VALUES', null, 'background'),
|
||||
]);
|
||||
|
||||
// Navigate to the credentials page as default entry page
|
||||
navigate('/credentials', { replace: true });
|
||||
}, [navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
// Check for inline unlock mode
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const inlineUnlock = urlParams.get('mode') === 'inline_unlock';
|
||||
|
||||
if (isFullyInitialized) {
|
||||
// Prevent multiple vault syncs (only run sync once)
|
||||
const shouldRunSync = !hasInitialized.current;
|
||||
|
||||
if (requiresAuth) {
|
||||
setIsInitialLoading(false);
|
||||
|
||||
// Determine which auth page to show
|
||||
if (!isLoggedIn) {
|
||||
navigate('/login', { replace: true });
|
||||
} else if (!dbAvailable) {
|
||||
navigate('/unlock', { replace: true });
|
||||
}
|
||||
} else if (shouldRunSync) {
|
||||
// Only perform vault sync once during initialization
|
||||
hasInitialized.current = true;
|
||||
|
||||
// Perform vault sync and restore state
|
||||
syncVault({
|
||||
initialSync: false,
|
||||
/**
|
||||
* Handle successful vault sync.
|
||||
*/
|
||||
onSuccess: async () => {
|
||||
// After successful sync, try to restore last page or go to credentials
|
||||
if (inlineUnlock) {
|
||||
setIsInitialLoading(false);
|
||||
navigate('/unlock-success', { replace: true });
|
||||
} else {
|
||||
await restoreLastPage();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Handle vault sync error.
|
||||
* @param error Error message
|
||||
*/
|
||||
onError: (error) => {
|
||||
console.error('Vault sync error during initialization:', error);
|
||||
// Even if sync fails, continue with initialization
|
||||
restoreLastPage().then(() => {
|
||||
setIsInitialLoading(false);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Handle upgrade required.
|
||||
*/
|
||||
onUpgradeRequired: () => {
|
||||
navigate('/upgrade', { replace: true });
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// User is logged in and db is available, navigate to appropriate page
|
||||
setIsInitialLoading(false);
|
||||
restoreLastPage();
|
||||
}
|
||||
}
|
||||
}, [isFullyInitialized, requiresAuth, isLoggedIn, dbAvailable, navigate, setIsInitialLoading, syncVault, restoreLastPage]);
|
||||
|
||||
// This component doesn't render anything visible - it just handles initialization
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Reinitialize;
|
||||
@@ -1,17 +1,19 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/icons/HeaderIcons';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useTheme } from '@/entrypoints/popup/context/ThemeContext';
|
||||
import { useApiUrl } from '@/entrypoints/popup/utils/ApiUrlUtility';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
import { DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, GLOBAL_CONTEXT_MENU_ENABLED_KEY, TEMPORARY_DISABLED_SITES_KEY } from '@/utils/Constants';
|
||||
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
|
||||
import { storage, browser } from "#imports";
|
||||
|
||||
/**
|
||||
@@ -34,6 +36,8 @@ const Settings: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const { loadApiUrl, getDisplayUrl } = useApiUrl();
|
||||
const navigate = useNavigate();
|
||||
const [settings, setSettings] = useState<PopupSettings>({
|
||||
disabledUrls: [],
|
||||
temporaryDisabledUrls: {},
|
||||
@@ -69,6 +73,15 @@ const Settings: React.FC = () => {
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
{!PopoutUtility.isPopup() && (
|
||||
<>
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<HeaderButton
|
||||
onClick={openClientTab}
|
||||
title="Open web app"
|
||||
@@ -104,6 +117,9 @@ const Settings: React.FC = () => {
|
||||
await storage.setItem(TEMPORARY_DISABLED_SITES_KEY, cleanedTemporaryDisabledUrls);
|
||||
}
|
||||
|
||||
// Load API URL
|
||||
await loadApiUrl();
|
||||
|
||||
setSettings({
|
||||
disabledUrls,
|
||||
temporaryDisabledUrls: cleanedTemporaryDisabledUrls,
|
||||
@@ -113,7 +129,7 @@ const Settings: React.FC = () => {
|
||||
isContextMenuEnabled
|
||||
});
|
||||
setIsInitialLoading(false);
|
||||
}, [setIsInitialLoading]);
|
||||
}, [setIsInitialLoading, loadApiUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
@@ -233,7 +249,7 @@ const Settings: React.FC = () => {
|
||||
* Handle logout.
|
||||
*/
|
||||
const handleLogout = async () : Promise<void> => {
|
||||
await authContext.logout();
|
||||
navigate('/logout', { replace: true });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -342,7 +358,7 @@ const Settings: React.FC = () => {
|
||||
{settings.isGloballyEnabled && (
|
||||
<button
|
||||
onClick={toggleCurrentSite}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
className={`px-4 py-2 ml-1 rounded-md transition-colors ${
|
||||
settings.isEnabled
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
@@ -436,7 +452,7 @@ const Settings: React.FC = () => {
|
||||
)}
|
||||
|
||||
<div className="text-center text-gray-400 dark:text-gray-600">
|
||||
Version: {AppInfo.VERSION}
|
||||
Version {AppInfo.VERSION} ({getDisplayUrl()})
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,10 +4,14 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
import SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
|
||||
|
||||
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants';
|
||||
@@ -23,13 +27,14 @@ const Unlock: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const navigate = useNavigate();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
|
||||
const webApi = useWebApi();
|
||||
const srpUtil = new SrpUtility(webApi);
|
||||
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { showLoading, hideLoading } = useLoading();
|
||||
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
@@ -40,11 +45,30 @@ const Unlock: React.FC = () => {
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
await webApi.logout(statusError);
|
||||
navigate('/logout');
|
||||
}
|
||||
setIsInitialLoading(false);
|
||||
};
|
||||
|
||||
checkStatus();
|
||||
}, [webApi, authContext]);
|
||||
}, [webApi, authContext, setIsInitialLoading, navigate]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = !PopoutUtility.isPopup() ? (
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
|
||||
return () => {
|
||||
setHeaderButtons(null);
|
||||
};
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
/**
|
||||
* Handle submit
|
||||
@@ -84,6 +108,9 @@ const Unlock: React.FC = () => {
|
||||
|
||||
// Clear dismiss until (which can be enabled after user has dimissed vault is locked popup) to ensure popup is shown.
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
|
||||
// Redirect to reinitialize page
|
||||
navigate('/reinitialize', { replace: true });
|
||||
} catch (err) {
|
||||
setError('Failed to unlock vault. Please check your password and try again.');
|
||||
console.error('Unlock error:', err);
|
||||
@@ -100,13 +127,31 @@ const Unlock: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md">
|
||||
<div>
|
||||
<form onSubmit={handleSubmit} className="bg-white dark:bg-gray-700 w-full shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white break-all overflow-hidden mb-4">{authContext.username}</h2>
|
||||
{/* User Avatar and Username Section */}
|
||||
<div className="flex items-center space-x-3 mb-6">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||||
<span className="text-primary-600 dark:text-primary-400 text-lg font-medium">
|
||||
{authContext.username?.[0]?.toUpperCase() || '?'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{authContext.username}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Logged in
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-base text-gray-500 dark:text-gray-200 mb-6">
|
||||
Enter your master password to unlock your vault.
|
||||
</p>
|
||||
{/* Instruction Title */}
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Unlock your vault
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
|
||||
@@ -114,7 +159,7 @@ const Unlock: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="mb-2">
|
||||
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="password">
|
||||
Password
|
||||
</label>
|
||||
|
||||
@@ -1,45 +1,55 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* Unlock success component shown when the vault is successfully unlocked in a separate popup
|
||||
* asking the user if they want to close the popup.
|
||||
*/
|
||||
const UnlockSuccess: React.FC<{
|
||||
onClose: () => void;
|
||||
}> = ({ onClose }) => (
|
||||
<div className="flex flex-col items-center justify-center p-6 text-center">
|
||||
<div className="mb-4 text-green-600 dark:text-green-400">
|
||||
<svg className="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
const UnlockSuccess: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
/**
|
||||
* Handle browsing vault contents - navigate to credentials page and reset mode parameter
|
||||
*/
|
||||
const handleBrowseVaultContents = (): void => {
|
||||
// Remove mode=inline from URL before navigating
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('mode');
|
||||
window.history.replaceState({}, '', url);
|
||||
|
||||
// Navigate to credentials page
|
||||
navigate('/credentials');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-6 text-center">
|
||||
<div className="mb-4 text-green-600 dark:text-green-400">
|
||||
<svg className="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">
|
||||
Your vault is successfully unlocked
|
||||
</h2>
|
||||
<p className="mb-6 text-gray-600 dark:text-gray-400">
|
||||
You can now use autofill in login forms in your browser.
|
||||
</p>
|
||||
<div className="space-y-3 w-full">
|
||||
<button
|
||||
onClick={() => window.close()}
|
||||
className="w-full px-4 py-2 text-white bg-primary-600 rounded hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||||
>
|
||||
Close this popup
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBrowseVaultContents}
|
||||
className="w-full px-4 py-2 text-gray-700 bg-gray-100 rounded hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
>
|
||||
Browse vault contents
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">
|
||||
Your vault is successfully unlocked
|
||||
</h2>
|
||||
<p className="mb-6 text-gray-600 dark:text-gray-400">
|
||||
You can now use autofill in login forms in your browser.
|
||||
</p>
|
||||
<div className="space-y-3 w-full">
|
||||
<button
|
||||
onClick={() => window.close()}
|
||||
className="w-full px-4 py-2 text-white bg-primary-600 rounded hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||||
>
|
||||
Close this popup
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
// Remove mode=inline from URL before closing
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('mode');
|
||||
window.history.replaceState({}, '', url);
|
||||
onClose();
|
||||
}}
|
||||
className="w-full px-4 py-2 text-gray-700 bg-gray-100 rounded hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
>
|
||||
Browse vault contents
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default UnlockSuccess;
|
||||
|
||||
339
apps/browser-extension/src/entrypoints/popup/pages/Upgrade.tsx
Normal file
339
apps/browser-extension/src/entrypoints/popup/pages/Upgrade.tsx
Normal file
@@ -0,0 +1,339 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import Modal from '@/entrypoints/popup/components/Modal';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
|
||||
import { useVaultSync } from '@/entrypoints/popup/hooks/useVaultSync';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
import type { VaultVersion } from '@/utils/dist/shared/vault-sql';
|
||||
import { VaultSqlGenerator } from '@/utils/dist/shared/vault-sql';
|
||||
|
||||
/**
|
||||
* Upgrade page for handling vault version upgrades.
|
||||
*/
|
||||
const Upgrade: React.FC = () => {
|
||||
const { username } = useAuth();
|
||||
const dbContext = useDb();
|
||||
const { sqliteClient } = dbContext;
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentVersion, setCurrentVersion] = useState<VaultVersion | null>(null);
|
||||
const [latestVersion, setLatestVersion] = useState<VaultVersion | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showSelfHostedWarning, setShowSelfHostedWarning] = useState(false);
|
||||
const [showVersionInfo, setShowVersionInfo] = useState(false);
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const webApi = useWebApi();
|
||||
const { executeVaultMutation, isLoading: isVaultMutationLoading, syncStatus } = useVaultMutate();
|
||||
const { syncVault } = useVaultSync();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = !PopoutUtility.isPopup() ? (
|
||||
<>
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
</>
|
||||
) : null;
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
|
||||
return () => {
|
||||
setHeaderButtons(null);
|
||||
};
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
/**
|
||||
* Load version information from the database.
|
||||
*/
|
||||
const loadVersionInfo = useCallback(async () => {
|
||||
try {
|
||||
if (sqliteClient) {
|
||||
const current = sqliteClient.getDatabaseVersion();
|
||||
const latest = await sqliteClient.getLatestDatabaseVersion();
|
||||
setCurrentVersion(current);
|
||||
setLatestVersion(latest);
|
||||
}
|
||||
setIsInitialLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to load version information:', error);
|
||||
setError('Failed to load version information. Please try again.');
|
||||
}
|
||||
}, [sqliteClient, setIsInitialLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
loadVersionInfo();
|
||||
}, [loadVersionInfo]);
|
||||
|
||||
/**
|
||||
* Handle the vault upgrade.
|
||||
*/
|
||||
const handleUpgrade = async (): Promise<void> => {
|
||||
if (!sqliteClient || !currentVersion || !latestVersion) {
|
||||
setError('Unable to get version information. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a self-hosted instance and show warning if needed
|
||||
if (await webApi.isSelfHosted()) {
|
||||
setShowSelfHostedWarning(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await performUpgrade();
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform the actual vault upgrade.
|
||||
*/
|
||||
const performUpgrade = async (): Promise<void> => {
|
||||
if (!sqliteClient || !currentVersion || !latestVersion) {
|
||||
setError('Unable to get version information. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Get upgrade SQL commands from vault-sql shared library
|
||||
const vaultSqlGenerator = new VaultSqlGenerator();
|
||||
const upgradeResult = vaultSqlGenerator.getUpgradeVaultSql(currentVersion.revision, latestVersion.revision);
|
||||
|
||||
if (!upgradeResult.success) {
|
||||
throw new Error(upgradeResult.error ?? 'Failed to generate upgrade SQL');
|
||||
}
|
||||
|
||||
if (upgradeResult.sqlCommands.length === 0) {
|
||||
// No upgrade needed, vault is already up to date
|
||||
await handleUpgradeSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the useVaultMutate hook to handle the upgrade and vault upload
|
||||
console.debug('executeVaultMutation');
|
||||
await executeVaultMutation(async () => {
|
||||
// Begin transaction
|
||||
console.debug('beginTransaction');
|
||||
sqliteClient.beginTransaction();
|
||||
|
||||
// Execute each SQL command
|
||||
console.debug('executeRaw', upgradeResult.sqlCommands.length);
|
||||
for (let i = 0; i < upgradeResult.sqlCommands.length; i++) {
|
||||
const sqlCommand = upgradeResult.sqlCommands[i];
|
||||
|
||||
try {
|
||||
console.debug('executeRaw', sqlCommand);
|
||||
sqliteClient.executeRaw(sqlCommand);
|
||||
} catch (error) {
|
||||
console.debug('error', error);
|
||||
console.error(`Error executing SQL command ${i + 1}:`, sqlCommand, error);
|
||||
sqliteClient.rollbackTransaction();
|
||||
throw new Error(`Failed to apply migration ${i + 1}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
console.debug('commitTransaction');
|
||||
sqliteClient.commitTransaction();
|
||||
}, {
|
||||
skipSyncCheck: true, // Skip sync check during upgrade to prevent loop
|
||||
/**
|
||||
* Handle successful upgrade completion.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
console.debug('onSuccess');
|
||||
void handleUpgradeSuccess();
|
||||
},
|
||||
/**
|
||||
* Handle upgrade error.
|
||||
*/
|
||||
onError: (error: Error) => {
|
||||
console.debug('onError');
|
||||
console.error('Upgrade failed:', error);
|
||||
setError(error.message);
|
||||
}
|
||||
});
|
||||
console.debug('executeVaultMutation done?');
|
||||
} catch (error) {
|
||||
console.error('Upgrade failed:', error);
|
||||
setError(error instanceof Error ? error.message : 'An unknown error occurred during the upgrade. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle successful upgrade completion.
|
||||
*/
|
||||
const handleUpgradeSuccess = async (): Promise<void> => {
|
||||
try {
|
||||
// Sync vault to ensure we have the latest data
|
||||
await syncVault({
|
||||
/**
|
||||
* Handle successful sync completion.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
// Navigate to credentials page
|
||||
navigate('/credentials');
|
||||
},
|
||||
/**
|
||||
* Handle sync error.
|
||||
* @param error Error message
|
||||
*/
|
||||
onError: (error: string) => {
|
||||
console.error('Sync error after upgrade:', error);
|
||||
// Still navigate to credentials even if sync fails
|
||||
navigate('/credentials');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error during post-upgrade sync:', error);
|
||||
// Navigate to credentials even if sync fails
|
||||
navigate('/credentials');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the logout.
|
||||
*/
|
||||
const handleLogout = async (): Promise<void> => {
|
||||
navigate('/logout');
|
||||
};
|
||||
|
||||
/**
|
||||
* Show version description dialog.
|
||||
*/
|
||||
const showVersionDialog = (): void => {
|
||||
setShowVersionInfo(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Full loading screen overlay */}
|
||||
{(isLoading || isVaultMutationLoading) && (
|
||||
<div className="fixed inset-0 flex flex-col justify-center items-center bg-white dark:bg-gray-900 bg-opacity-90 dark:bg-opacity-90 z-50">
|
||||
<LoadingSpinner />
|
||||
<div className="text-sm text-gray-500 mt-2">
|
||||
{syncStatus || 'Upgrading vault...'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Self-hosted warning modal */}
|
||||
<Modal
|
||||
isOpen={showSelfHostedWarning}
|
||||
onClose={() => setShowSelfHostedWarning(false)}
|
||||
onConfirm={() => {
|
||||
setShowSelfHostedWarning(false);
|
||||
void performUpgrade();
|
||||
}}
|
||||
title="Self-Hosted Server"
|
||||
message="If you're using a self-hosted server, make sure to also update your self-hosted instance as otherwise logging in to the web client will stop working. Do you want to continue with the upgrade?"
|
||||
confirmText="Continue"
|
||||
cancelText="Cancel"
|
||||
/>
|
||||
|
||||
{/* Version info modal */}
|
||||
<Modal
|
||||
isOpen={showVersionInfo}
|
||||
onClose={() => setShowVersionInfo(false)}
|
||||
onConfirm={() => setShowVersionInfo(false)}
|
||||
title="What's New"
|
||||
message={`An upgrade is required to support the following changes:\n\n${latestVersion?.description ?? 'No description available for this version.'}`}
|
||||
/>
|
||||
|
||||
<form className="w-full px-2 pt-2 pb-2 mb-4">
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* User display section like settings page */}
|
||||
<div className="flex items-center space-x-3 mb-4">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||||
<span className="text-primary-600 dark:text-primary-400 text-lg font-medium">
|
||||
{username?.[0]?.toUpperCase() || '?'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{username}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-bold dark:text-gray-200 mb-4">Upgrade Vault</h2>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-700 dark:text-gray-200 text-sm mb-4">
|
||||
AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.
|
||||
</p>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded p-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">Version Information</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={showVersionDialog}
|
||||
className="bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200 rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold hover:bg-gray-300 dark:hover:bg-gray-500"
|
||||
title="Show version details"
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Your vault:</span>
|
||||
<span className="text-sm font-bold text-orange-600 dark:text-orange-400">
|
||||
{currentVersion?.releaseVersion ?? '...'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">New version:</span>
|
||||
<span className="text-sm font-bold text-green-600 dark:text-green-400">
|
||||
{latestVersion?.releaseVersion ?? '...'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full space-y-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleUpgrade}
|
||||
>
|
||||
{isLoading || isVaultMutationLoading ? (syncStatus || 'Upgrading...') : 'Upgrade Vault'}
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
className="text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 text-sm font-medium py-2"
|
||||
disabled={isLoading || isVaultMutationLoading}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Upgrade;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
/**
|
||||
* Hook to manage API URL state and display logic.
|
||||
* @returns Object containing apiUrl state and utility functions
|
||||
*/
|
||||
export const useApiUrl = (): {
|
||||
apiUrl: string;
|
||||
setApiUrl: (url: string) => void;
|
||||
loadApiUrl: () => Promise<void>;
|
||||
getDisplayUrl: () => string;
|
||||
} => {
|
||||
const [apiUrl, setApiUrl] = useState<string>(AppInfo.DEFAULT_API_URL);
|
||||
|
||||
/**
|
||||
* Load the API URL from storage.
|
||||
*/
|
||||
const loadApiUrl = async (): Promise<void> => {
|
||||
const storedUrl = await storage.getItem('local:apiUrl') as string;
|
||||
if (storedUrl && storedUrl.length > 0) {
|
||||
setApiUrl(storedUrl);
|
||||
} else {
|
||||
setApiUrl(AppInfo.DEFAULT_API_URL);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the display URL for UI presentation.
|
||||
* @returns Formatted display URL
|
||||
*/
|
||||
const getDisplayUrl = (): string => {
|
||||
const cleanUrl = apiUrl.replace('https://', '').replace('http://', '').replace(':443', '').replace('/api', '');
|
||||
return cleanUrl === 'app.aliasvault.net' ? 'aliasvault.net' : cleanUrl;
|
||||
};
|
||||
|
||||
return {
|
||||
apiUrl,
|
||||
setApiUrl,
|
||||
loadApiUrl,
|
||||
getDisplayUrl,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Utility class for handling popup window operations
|
||||
*/
|
||||
export class PopoutUtility {
|
||||
/**
|
||||
* Check if the current page is an expanded popup.
|
||||
* Uses both URL parameter detection and window width as fallback.
|
||||
*/
|
||||
public static isPopup(): boolean {
|
||||
// Primary method: Check URL parameter
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('expanded') === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback method: Check window width (popout windows are 800px wide)
|
||||
* Regular popup extension windows are typically narrower (around 375-400px)
|
||||
*/
|
||||
return window.innerWidth > 390;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the current page in a new expanded popup window.
|
||||
* @param path - The path to open in the popup (defaults to current path)
|
||||
*/
|
||||
public static openInNewPopup(path?: string): void {
|
||||
const width = 800;
|
||||
const height = 1000;
|
||||
const left = window.screen.width / 2 - width / 2;
|
||||
const top = window.screen.height / 2 - height / 2;
|
||||
|
||||
const currentPath = path || window.location.hash.replace('#', '');
|
||||
const popupUrl = `popup.html?expanded=true#${currentPath}`;
|
||||
|
||||
window.open(
|
||||
popupUrl,
|
||||
'AliasVaultPopup',
|
||||
`width=${width},height=${height},left=${left},top=${top},popup=true`
|
||||
);
|
||||
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
@@ -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.19.0';
|
||||
public static readonly VERSION = '0.20.0';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault server (API) version. If the server version is below this, the
|
||||
@@ -14,11 +14,6 @@ export class AppInfo {
|
||||
*/
|
||||
public static readonly MIN_SERVER_VERSION = '0.12.0-dev';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault client vault version.
|
||||
*/
|
||||
public static readonly MIN_VAULT_VERSION = '1.4.1';
|
||||
|
||||
/**
|
||||
* The client name to use in the X-AliasVault-Client header.
|
||||
* Detects the specific browser being used.
|
||||
@@ -61,15 +56,6 @@ export class AppInfo {
|
||||
*/
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Checks if a given vault version is supported
|
||||
* @param vaultVersion The version to check
|
||||
* @returns boolean indicating if the version is supported
|
||||
*/
|
||||
public static isVaultVersionSupported(vaultVersion: string): boolean {
|
||||
return this.versionGreaterThanOrEqualTo(vaultVersion, this.MIN_VAULT_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given server version is supported
|
||||
* @param serverVersion The version to check
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import initSqlJs, { Database } from 'sql.js';
|
||||
|
||||
import type { Credential, EncryptionKey, PasswordSettings, TotpCode } from '@/utils/dist/shared/models/vault';
|
||||
import type { VaultVersion } from '@/utils/dist/shared/vault-sql';
|
||||
import { VaultSqlGenerator } from '@/utils/dist/shared/vault-sql';
|
||||
|
||||
/**
|
||||
* Placeholder base64 image for credentials without a logo.
|
||||
@@ -385,6 +387,13 @@ export class SqliteClient {
|
||||
return this.getSetting('DefaultIdentityLanguage', 'en');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default identity gender preference from the database.
|
||||
*/
|
||||
public getDefaultIdentityGender(): string {
|
||||
return this.getSetting('DefaultIdentityGender', 'random');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the password settings from the database.
|
||||
*/
|
||||
@@ -526,7 +535,7 @@ export class SqliteClient {
|
||||
* Returns the semantic version (e.g., "1.4.1") from the latest migration.
|
||||
* Returns null if no migrations are found.
|
||||
*/
|
||||
public getDatabaseVersion(): string | null {
|
||||
public getDatabaseVersion(): VaultVersion {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
@@ -540,7 +549,7 @@ export class SqliteClient {
|
||||
LIMIT 1`);
|
||||
|
||||
if (results.length === 0) {
|
||||
return null;
|
||||
throw new Error('No migrations found in the database.');
|
||||
}
|
||||
|
||||
// Extract version using regex - matches patterns like "20240917191243_1.4.1-RenameAttachmentsPlural"
|
||||
@@ -548,17 +557,53 @@ export class SqliteClient {
|
||||
const versionRegex = /_(\d+\.\d+\.\d+)-/;
|
||||
const versionMatch = versionRegex.exec(migrationId);
|
||||
|
||||
let currentVersion = null;
|
||||
if (versionMatch?.[1]) {
|
||||
return versionMatch[1];
|
||||
currentVersion = versionMatch[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
// Get all available vault versions to get the revision number of the current version.
|
||||
const vaultSqlGenerator = new VaultSqlGenerator();
|
||||
const allVersions = vaultSqlGenerator.getAllVersions();
|
||||
const currentVersionRevision = allVersions.find(v => v.version === currentVersion);
|
||||
|
||||
if (!currentVersionRevision) {
|
||||
throw new Error('This browser extension is outdated and cannot be used to access this vault. Please update this browser extension to continue.');
|
||||
}
|
||||
|
||||
return currentVersionRevision;
|
||||
} catch (error) {
|
||||
console.error('Error getting database version:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest available database version
|
||||
* @returns The latest VaultVersion
|
||||
*/
|
||||
public async getLatestDatabaseVersion(): Promise<VaultVersion> {
|
||||
const vaultSqlGenerator = new VaultSqlGenerator();
|
||||
const allVersions = vaultSqlGenerator.getAllVersions();
|
||||
return allVersions[allVersions.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are pending migrations
|
||||
* @returns True if there are pending migrations, false otherwise
|
||||
*/
|
||||
public async hasPendingMigrations(): Promise<boolean> {
|
||||
try {
|
||||
const currentVersion = this.getDatabaseVersion();
|
||||
const latestVersion = await this.getLatestDatabaseVersion();
|
||||
|
||||
return currentVersion.revision < latestVersion.revision;
|
||||
} catch (error) {
|
||||
console.error('Error checking pending migrations:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TOTP codes for a credential
|
||||
* @param credentialId - The ID of the credential to get TOTP codes for
|
||||
@@ -931,6 +976,38 @@ export class SqliteClient {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute raw SQL command
|
||||
* @param query - The SQL command to execute
|
||||
*/
|
||||
public executeRaw(query: string): void {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
// Split the query by semicolons to handle multiple statements
|
||||
const statements = query.split(';');
|
||||
|
||||
for (const statement of statements) {
|
||||
const trimmedStatement = statement.trim();
|
||||
|
||||
// Skip empty statements and transaction control statements (handled externally)
|
||||
if (trimmedStatement.length === 0 ||
|
||||
trimmedStatement.toUpperCase().startsWith('BEGIN TRANSACTION') ||
|
||||
trimmedStatement.toUpperCase().startsWith('COMMIT') ||
|
||||
trimmedStatement.toUpperCase().startsWith('ROLLBACK')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.db.run(trimmedStatement);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error executing raw SQL:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SqliteClient;
|
||||
@@ -29,12 +29,16 @@ export class WebApiService {
|
||||
* Get the base URL for the API from settings.
|
||||
*/
|
||||
private async getBaseUrl(): Promise<string> {
|
||||
const result = await storage.getItem('local:apiUrl') as string;
|
||||
if (result && result.length > 0) {
|
||||
return result.replace(/\/$/, '') + '/v1/';
|
||||
}
|
||||
const apiUrl = await this.getApiUrl();
|
||||
return apiUrl.replace(/\/$/, '') + '/v1/';
|
||||
}
|
||||
|
||||
return AppInfo.DEFAULT_API_URL.replace(/\/$/, '') + '/v1/';
|
||||
/**
|
||||
* Check if the current server is self-hosted.
|
||||
*/
|
||||
public async isSelfHosted(): Promise<boolean> {
|
||||
const apiUrl = await this.getApiUrl();
|
||||
return apiUrl !== AppInfo.DEFAULT_API_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,14 +232,12 @@ export class WebApiService {
|
||||
// Logout and revoke tokens via WebApi.
|
||||
try {
|
||||
const refreshToken = await this.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
return;
|
||||
if (refreshToken) {
|
||||
await this.post('Auth/revoke', {
|
||||
token: await this.getAccessToken(),
|
||||
refreshToken: refreshToken,
|
||||
}, false);
|
||||
}
|
||||
|
||||
await this.post('Auth/revoke', {
|
||||
token: await this.getAccessToken(),
|
||||
refreshToken: refreshToken,
|
||||
}, false);
|
||||
} catch (err) {
|
||||
console.error('WebApi logout error:', err);
|
||||
}
|
||||
@@ -290,18 +292,19 @@ export class WebApiService {
|
||||
* Status 0 = OK, vault is ready.
|
||||
* Status 1 = Merge required, which only the web client supports.
|
||||
*/
|
||||
if (vaultResponseJson.status !== 0) {
|
||||
if (vaultResponseJson.status === 1) {
|
||||
// Note: vault merge is no longer allowed by the API as of 0.20.0, updates with the same revision number are rejected. So this check can be removed later.
|
||||
return 'Your vault needs to be updated. Please login on the AliasVault website and follow the steps.';
|
||||
}
|
||||
|
||||
if (vaultResponseJson.status === 2) {
|
||||
return 'Your vault is outdated. Please login on the AliasVault website and follow the steps.';
|
||||
}
|
||||
|
||||
if (!vaultResponseJson.vault?.blob) {
|
||||
return 'Your account does not have a vault yet. Please complete the tutorial in the AliasVault web client before using the browser extension.';
|
||||
}
|
||||
|
||||
if (!AppInfo.isVaultVersionSupported(vaultResponseJson.vault.version)) {
|
||||
return 'Your vault is outdated. Please login via the web client to update your vault.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -330,31 +333,14 @@ export class WebApiService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Blob to a Base64 string.
|
||||
* Get the API URL from settings.
|
||||
*/
|
||||
private async blobToBase64(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
private async getApiUrl(): Promise<string> {
|
||||
const result = await storage.getItem('local:apiUrl') as string;
|
||||
if (result.length === 0) {
|
||||
return AppInfo.DEFAULT_API_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the reader has finished loading, convert the result to a Base64 string.
|
||||
*/
|
||||
reader.onloadend = (): void => {
|
||||
const result = reader.result;
|
||||
if (typeof result === 'string') {
|
||||
resolve(result.split(',')[1]); // Remove the data URL prefix
|
||||
} else {
|
||||
reject(new Error('Failed to convert Blob to Base64.'));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* If the reader encounters an error, reject the promise with a proper Error object.
|
||||
*/
|
||||
reader.onerror = (): void => {
|
||||
reject(new Error('Failed to read blob as Data URL'));
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ type Identity = {
|
||||
};
|
||||
|
||||
interface IIdentityGenerator {
|
||||
generateRandomIdentity(): Identity;
|
||||
generateRandomIdentity(gender?: string | 'random'): Identity;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +42,7 @@ declare abstract class IdentityGenerator implements IIdentityGenerator {
|
||||
/**
|
||||
* Generate a random identity.
|
||||
*/
|
||||
generateRandomIdentity(): Identity;
|
||||
generateRandomIdentity(gender?: string | 'random'): Identity;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -172,7 +172,7 @@ var IdentityGenerator = class {
|
||||
/**
|
||||
* Generate a random identity.
|
||||
*/
|
||||
generateRandomIdentity() {
|
||||
generateRandomIdentity(gender) {
|
||||
const identity = {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
@@ -181,12 +181,26 @@ var IdentityGenerator = class {
|
||||
emailPrefix: "",
|
||||
nickName: ""
|
||||
};
|
||||
if (this.random() < 0.5) {
|
||||
identity.firstName = this.firstNamesMale[Math.floor(this.random() * this.firstNamesMale.length)];
|
||||
identity.gender = "Male" /* Male */;
|
||||
let selectedGender;
|
||||
if (gender === "random" || gender === void 0) {
|
||||
selectedGender = this.random() < 0.5 ? "Male" /* Male */ : "Female" /* Female */;
|
||||
} else {
|
||||
if (gender === "male") {
|
||||
selectedGender = "Male" /* Male */;
|
||||
} else if (gender === "female") {
|
||||
selectedGender = "Female" /* Female */;
|
||||
} else {
|
||||
selectedGender = "Male" /* Male */;
|
||||
}
|
||||
}
|
||||
identity.gender = selectedGender;
|
||||
if (selectedGender === "Male" /* Male */) {
|
||||
identity.firstName = this.firstNamesMale[Math.floor(this.random() * this.firstNamesMale.length)];
|
||||
} else if (selectedGender === "Female" /* Female */) {
|
||||
identity.firstName = this.firstNamesFemale[Math.floor(this.random() * this.firstNamesFemale.length)];
|
||||
identity.gender = "Female" /* Female */;
|
||||
} else {
|
||||
const usesMaleNames = this.random() < 0.5;
|
||||
identity.firstName = usesMaleNames ? this.firstNamesMale[Math.floor(this.random() * this.firstNamesMale.length)] : this.firstNamesFemale[Math.floor(this.random() * this.firstNamesFemale.length)];
|
||||
}
|
||||
identity.lastName = this.lastNames[Math.floor(this.random() * this.lastNames.length)];
|
||||
identity.birthDate = this.generateRandomDateOfBirth();
|
||||
|
||||
@@ -140,7 +140,7 @@ var IdentityGenerator = class {
|
||||
/**
|
||||
* Generate a random identity.
|
||||
*/
|
||||
generateRandomIdentity() {
|
||||
generateRandomIdentity(gender) {
|
||||
const identity = {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
@@ -149,12 +149,26 @@ var IdentityGenerator = class {
|
||||
emailPrefix: "",
|
||||
nickName: ""
|
||||
};
|
||||
if (this.random() < 0.5) {
|
||||
identity.firstName = this.firstNamesMale[Math.floor(this.random() * this.firstNamesMale.length)];
|
||||
identity.gender = "Male" /* Male */;
|
||||
let selectedGender;
|
||||
if (gender === "random" || gender === void 0) {
|
||||
selectedGender = this.random() < 0.5 ? "Male" /* Male */ : "Female" /* Female */;
|
||||
} else {
|
||||
if (gender === "male") {
|
||||
selectedGender = "Male" /* Male */;
|
||||
} else if (gender === "female") {
|
||||
selectedGender = "Female" /* Female */;
|
||||
} else {
|
||||
selectedGender = "Male" /* Male */;
|
||||
}
|
||||
}
|
||||
identity.gender = selectedGender;
|
||||
if (selectedGender === "Male" /* Male */) {
|
||||
identity.firstName = this.firstNamesMale[Math.floor(this.random() * this.firstNamesMale.length)];
|
||||
} else if (selectedGender === "Female" /* Female */) {
|
||||
identity.firstName = this.firstNamesFemale[Math.floor(this.random() * this.firstNamesFemale.length)];
|
||||
identity.gender = "Female" /* Female */;
|
||||
} else {
|
||||
const usesMaleNames = this.random() < 0.5;
|
||||
identity.firstName = usesMaleNames ? this.firstNamesMale[Math.floor(this.random() * this.firstNamesMale.length)] : this.firstNamesFemale[Math.floor(this.random() * this.firstNamesFemale.length)];
|
||||
}
|
||||
identity.lastName = this.lastNames[Math.floor(this.random() * this.lastNames.length)];
|
||||
identity.birthDate = this.generateRandomDateOfBirth();
|
||||
|
||||
9
apps/browser-extension/src/utils/dist/shared/vault-sql/README.md
vendored
Normal file
9
apps/browser-extension/src/utils/dist/shared/vault-sql/README.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# ⚠️ Auto-Generated Files
|
||||
|
||||
This folder contains the output of the shared `vault-sql` module from the `/shared` directory in the AliasVault project.
|
||||
|
||||
**Do not edit any of these files manually.**
|
||||
|
||||
To make changes:
|
||||
1. Update the source files in the `/shared/vault-sql/src` directory
|
||||
2. Run the `build.sh` script in the module directory to regenerate the outputs and copy them here.
|
||||
132
apps/browser-extension/src/utils/dist/shared/vault-sql/index.d.mts
vendored
Normal file
132
apps/browser-extension/src/utils/dist/shared/vault-sql/index.d.mts
vendored
Normal file
File diff suppressed because one or more lines are too long
132
apps/browser-extension/src/utils/dist/shared/vault-sql/index.d.ts
vendored
Normal file
132
apps/browser-extension/src/utils/dist/shared/vault-sql/index.d.ts
vendored
Normal file
File diff suppressed because one or more lines are too long
735
apps/browser-extension/src/utils/dist/shared/vault-sql/index.js
vendored
Normal file
735
apps/browser-extension/src/utils/dist/shared/vault-sql/index.js
vendored
Normal file
@@ -0,0 +1,735 @@
|
||||
// <auto-generated>
|
||||
// This file was automatically generated. Do not edit manually.
|
||||
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.ts
|
||||
var index_exports = {};
|
||||
__export(index_exports, {
|
||||
COMPLETE_SCHEMA_SQL: () => COMPLETE_SCHEMA_SQL,
|
||||
CreateVaultSqlGenerator: () => CreateVaultSqlGenerator,
|
||||
MIGRATION_SCRIPTS: () => MIGRATION_SCRIPTS,
|
||||
VAULT_VERSIONS: () => VAULT_VERSIONS,
|
||||
VaultSqlGenerator: () => VaultSqlGenerator
|
||||
});
|
||||
module.exports = __toCommonJS(index_exports);
|
||||
|
||||
// src/sql/SqlConstants.ts
|
||||
var COMPLETE_SCHEMA_SQL = `
|
||||
\uFEFFCREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
||||
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
|
||||
"ProductVersion" TEXT NOT NULL
|
||||
);
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "Aliases" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Aliases" PRIMARY KEY,
|
||||
"Gender" VARCHAR NULL,
|
||||
"FirstName" VARCHAR NULL,
|
||||
"LastName" VARCHAR NULL,
|
||||
"NickName" VARCHAR NULL,
|
||||
"BirthDate" TEXT NOT NULL,
|
||||
"AddressStreet" VARCHAR NULL,
|
||||
"AddressCity" VARCHAR NULL,
|
||||
"AddressState" VARCHAR NULL,
|
||||
"AddressZipCode" VARCHAR NULL,
|
||||
"AddressCountry" VARCHAR NULL,
|
||||
"Hobbies" TEXT NULL,
|
||||
"EmailPrefix" TEXT NULL,
|
||||
"PhoneMobile" TEXT NULL,
|
||||
"BankAccountIBAN" TEXT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "Services" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Services" PRIMARY KEY,
|
||||
"Name" TEXT NULL,
|
||||
"Url" TEXT NULL,
|
||||
"Logo" BLOB NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "Credentials" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Credentials" PRIMARY KEY,
|
||||
"AliasId" TEXT NOT NULL,
|
||||
"Notes" TEXT NULL,
|
||||
"Username" TEXT NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
"ServiceId" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_Credentials_Aliases_AliasId" FOREIGN KEY ("AliasId") REFERENCES "Aliases" ("Id") ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_Credentials_Services_ServiceId" FOREIGN KEY ("ServiceId") REFERENCES "Services" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "Attachment" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Attachment" PRIMARY KEY,
|
||||
"Filename" TEXT NOT NULL,
|
||||
"Blob" BLOB NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
"CredentialId" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_Attachment_Credentials_CredentialId" FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "Passwords" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Passwords" PRIMARY KEY,
|
||||
"Value" TEXT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
"CredentialId" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_Passwords_Credentials_CredentialId" FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_Attachment_CredentialId" ON "Attachment" ("CredentialId");
|
||||
|
||||
CREATE INDEX "IX_Credentials_AliasId" ON "Credentials" ("AliasId");
|
||||
|
||||
CREATE INDEX "IX_Credentials_ServiceId" ON "Credentials" ("ServiceId");
|
||||
|
||||
CREATE INDEX "IX_Passwords_CredentialId" ON "Passwords" ("CredentialId");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240708094944_1.0.0-InitialMigration', '9.0.4');
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240708224522_1.0.1-EmptyTestMigration', '9.0.4');
|
||||
|
||||
ALTER TABLE "Aliases" RENAME COLUMN "EmailPrefix" TO "Email";
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240711204207_1.0.2-ChangeEmailColumn', '9.0.4');
|
||||
|
||||
CREATE TABLE "EncryptionKeys" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_EncryptionKeys" PRIMARY KEY,
|
||||
"PublicKey" TEXT NOT NULL,
|
||||
"PrivateKey" TEXT NOT NULL,
|
||||
"IsPrimary" INTEGER NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240729105618_1.1.0-AddPkiTables', '9.0.4');
|
||||
|
||||
CREATE TABLE "Settings" (
|
||||
"Key" TEXT NOT NULL CONSTRAINT "PK_Settings" PRIMARY KEY,
|
||||
"Value" TEXT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240805073413_1.2.0-AddSettingsTable', '9.0.4');
|
||||
|
||||
CREATE TABLE "ef_temp_Aliases" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Aliases" PRIMARY KEY,
|
||||
"BirthDate" TEXT NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"Email" TEXT NULL,
|
||||
"FirstName" VARCHAR NULL,
|
||||
"Gender" VARCHAR NULL,
|
||||
"LastName" VARCHAR NULL,
|
||||
"NickName" VARCHAR NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_Aliases" ("Id", "BirthDate", "CreatedAt", "Email", "FirstName", "Gender", "LastName", "NickName", "UpdatedAt")
|
||||
SELECT "Id", "BirthDate", "CreatedAt", "Email", "FirstName", "Gender", "LastName", "NickName", "UpdatedAt"
|
||||
FROM "Aliases";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 0;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE "Aliases";
|
||||
|
||||
ALTER TABLE "ef_temp_Aliases" RENAME TO "Aliases";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 1;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240805122422_1.3.0-UpdateIdentityStructure', '9.0.4');
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "ef_temp_Credentials" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Credentials" PRIMARY KEY,
|
||||
"AliasId" TEXT NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"Notes" TEXT NULL,
|
||||
"ServiceId" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
"Username" TEXT NULL,
|
||||
CONSTRAINT "FK_Credentials_Aliases_AliasId" FOREIGN KEY ("AliasId") REFERENCES "Aliases" ("Id") ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_Credentials_Services_ServiceId" FOREIGN KEY ("ServiceId") REFERENCES "Services" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_Credentials" ("Id", "AliasId", "CreatedAt", "Notes", "ServiceId", "UpdatedAt", "Username")
|
||||
SELECT "Id", "AliasId", "CreatedAt", "Notes", "ServiceId", "UpdatedAt", "Username"
|
||||
FROM "Credentials";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 0;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE "Credentials";
|
||||
|
||||
ALTER TABLE "ef_temp_Credentials" RENAME TO "Credentials";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 1;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE INDEX "IX_Credentials_AliasId" ON "Credentials" ("AliasId");
|
||||
|
||||
CREATE INDEX "IX_Credentials_ServiceId" ON "Credentials" ("ServiceId");
|
||||
|
||||
COMMIT;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240812141727_1.3.1-MakeUsernameOptional', '9.0.4');
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
ALTER TABLE "Settings" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Services" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Passwords" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "EncryptionKeys" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Credentials" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Attachment" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Aliases" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240916105320_1.4.0-AddSyncSupport', '9.0.4');
|
||||
|
||||
ALTER TABLE "Attachment" RENAME TO "Attachments";
|
||||
|
||||
CREATE TABLE "ef_temp_Attachments" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Attachments" PRIMARY KEY,
|
||||
"Blob" BLOB NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"CredentialId" TEXT NOT NULL,
|
||||
"Filename" TEXT NOT NULL,
|
||||
"IsDeleted" INTEGER NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_Attachments_Credentials_CredentialId" FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_Attachments" ("Id", "Blob", "CreatedAt", "CredentialId", "Filename", "IsDeleted", "UpdatedAt")
|
||||
SELECT "Id", "Blob", "CreatedAt", "CredentialId", "Filename", "IsDeleted", "UpdatedAt"
|
||||
FROM "Attachments";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 0;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE "Attachments";
|
||||
|
||||
ALTER TABLE "ef_temp_Attachments" RENAME TO "Attachments";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 1;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE INDEX "IX_Attachments_CredentialId" ON "Attachments" ("CredentialId");
|
||||
|
||||
COMMIT;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240917191243_1.4.1-RenameAttachmentsPlural', '9.0.4');
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "TotpCodes" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_TotpCodes" PRIMARY KEY,
|
||||
"Name" TEXT NOT NULL,
|
||||
"SecretKey" TEXT NOT NULL,
|
||||
"CredentialId" TEXT NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
"IsDeleted" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_TotpCodes_Credentials_CredentialId" FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_TotpCodes_CredentialId" ON "TotpCodes" ("CredentialId");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250310131554_1.5.0-AddTotpCodes', '9.0.4');
|
||||
|
||||
COMMIT;
|
||||
`;
|
||||
var MIGRATION_SCRIPTS = {
|
||||
1: `\uFEFFBEGIN TRANSACTION;
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240708224522_1.0.1-EmptyTestMigration', '9.0.4');
|
||||
|
||||
COMMIT;`,
|
||||
2: `\uFEFFBEGIN TRANSACTION;
|
||||
ALTER TABLE "Aliases" RENAME COLUMN "EmailPrefix" TO "Email";
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240711204207_1.0.2-ChangeEmailColumn', '9.0.4');
|
||||
|
||||
COMMIT;`,
|
||||
3: `\uFEFFBEGIN TRANSACTION;
|
||||
CREATE TABLE "EncryptionKeys" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_EncryptionKeys" PRIMARY KEY,
|
||||
"PublicKey" TEXT NOT NULL,
|
||||
"PrivateKey" TEXT NOT NULL,
|
||||
"IsPrimary" INTEGER NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240729105618_1.1.0-AddPkiTables', '9.0.4');
|
||||
|
||||
COMMIT;`,
|
||||
4: `\uFEFFBEGIN TRANSACTION;
|
||||
CREATE TABLE "Settings" (
|
||||
"Key" TEXT NOT NULL CONSTRAINT "PK_Settings" PRIMARY KEY,
|
||||
"Value" TEXT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240805073413_1.2.0-AddSettingsTable', '9.0.4');
|
||||
|
||||
COMMIT;`,
|
||||
5: `\uFEFFBEGIN TRANSACTION;
|
||||
CREATE TABLE "ef_temp_Aliases" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Aliases" PRIMARY KEY,
|
||||
"BirthDate" TEXT NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"Email" TEXT NULL,
|
||||
"FirstName" VARCHAR NULL,
|
||||
"Gender" VARCHAR NULL,
|
||||
"LastName" VARCHAR NULL,
|
||||
"NickName" VARCHAR NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_Aliases" ("Id", "BirthDate", "CreatedAt", "Email", "FirstName", "Gender", "LastName", "NickName", "UpdatedAt")
|
||||
SELECT "Id", "BirthDate", "CreatedAt", "Email", "FirstName", "Gender", "LastName", "NickName", "UpdatedAt"
|
||||
FROM "Aliases";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 0;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE "Aliases";
|
||||
|
||||
ALTER TABLE "ef_temp_Aliases" RENAME TO "Aliases";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 1;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240805122422_1.3.0-UpdateIdentityStructure', '9.0.4');`,
|
||||
6: `\uFEFFBEGIN TRANSACTION;
|
||||
CREATE TABLE "ef_temp_Credentials" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Credentials" PRIMARY KEY,
|
||||
"AliasId" TEXT NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"Notes" TEXT NULL,
|
||||
"ServiceId" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
"Username" TEXT NULL,
|
||||
CONSTRAINT "FK_Credentials_Aliases_AliasId" FOREIGN KEY ("AliasId") REFERENCES "Aliases" ("Id") ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_Credentials_Services_ServiceId" FOREIGN KEY ("ServiceId") REFERENCES "Services" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_Credentials" ("Id", "AliasId", "CreatedAt", "Notes", "ServiceId", "UpdatedAt", "Username")
|
||||
SELECT "Id", "AliasId", "CreatedAt", "Notes", "ServiceId", "UpdatedAt", "Username"
|
||||
FROM "Credentials";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 0;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE "Credentials";
|
||||
|
||||
ALTER TABLE "ef_temp_Credentials" RENAME TO "Credentials";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 1;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE INDEX "IX_Credentials_AliasId" ON "Credentials" ("AliasId");
|
||||
|
||||
CREATE INDEX "IX_Credentials_ServiceId" ON "Credentials" ("ServiceId");
|
||||
|
||||
COMMIT;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240812141727_1.3.1-MakeUsernameOptional', '9.0.4');`,
|
||||
7: `\uFEFFBEGIN TRANSACTION;
|
||||
ALTER TABLE "Settings" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Services" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Passwords" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "EncryptionKeys" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Credentials" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Attachment" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Aliases" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240916105320_1.4.0-AddSyncSupport', '9.0.4');
|
||||
|
||||
COMMIT;`,
|
||||
8: `\uFEFFBEGIN TRANSACTION;
|
||||
ALTER TABLE "Attachment" RENAME TO "Attachments";
|
||||
|
||||
CREATE TABLE "ef_temp_Attachments" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Attachments" PRIMARY KEY,
|
||||
"Blob" BLOB NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"CredentialId" TEXT NOT NULL,
|
||||
"Filename" TEXT NOT NULL,
|
||||
"IsDeleted" INTEGER NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_Attachments_Credentials_CredentialId" FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_Attachments" ("Id", "Blob", "CreatedAt", "CredentialId", "Filename", "IsDeleted", "UpdatedAt")
|
||||
SELECT "Id", "Blob", "CreatedAt", "CredentialId", "Filename", "IsDeleted", "UpdatedAt"
|
||||
FROM "Attachments";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 0;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE "Attachments";
|
||||
|
||||
ALTER TABLE "ef_temp_Attachments" RENAME TO "Attachments";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 1;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE INDEX "IX_Attachments_CredentialId" ON "Attachments" ("CredentialId");
|
||||
|
||||
COMMIT;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240917191243_1.4.1-RenameAttachmentsPlural', '9.0.4');`,
|
||||
9: `\uFEFFBEGIN TRANSACTION;
|
||||
CREATE TABLE "TotpCodes" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_TotpCodes" PRIMARY KEY,
|
||||
"Name" TEXT NOT NULL,
|
||||
"SecretKey" TEXT NOT NULL,
|
||||
"CredentialId" TEXT NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
"IsDeleted" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_TotpCodes_Credentials_CredentialId" FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_TotpCodes_CredentialId" ON "TotpCodes" ("CredentialId");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250310131554_1.5.0-AddTotpCodes', '9.0.4');
|
||||
|
||||
COMMIT;`
|
||||
};
|
||||
|
||||
// src/sql/VaultVersions.ts
|
||||
var VAULT_VERSIONS = [
|
||||
{
|
||||
revision: 1,
|
||||
version: "1.0.0",
|
||||
description: "Initial Migration",
|
||||
releaseVersion: "0.1.0"
|
||||
},
|
||||
{
|
||||
revision: 2,
|
||||
version: "1.0.1",
|
||||
description: "Empty Test Migration",
|
||||
releaseVersion: "0.2.0"
|
||||
},
|
||||
{
|
||||
revision: 3,
|
||||
version: "1.0.2",
|
||||
description: "Change Email Column",
|
||||
releaseVersion: "0.3.0"
|
||||
},
|
||||
{
|
||||
revision: 4,
|
||||
version: "1.1.0",
|
||||
description: "Add Pki Tables",
|
||||
releaseVersion: "0.4.0"
|
||||
},
|
||||
{
|
||||
revision: 5,
|
||||
version: "1.2.0",
|
||||
description: "Add Settings Table",
|
||||
releaseVersion: "0.4.0"
|
||||
},
|
||||
{
|
||||
revision: 6,
|
||||
version: "1.3.0",
|
||||
description: "Update Identity Structure",
|
||||
releaseVersion: "0.5.0"
|
||||
},
|
||||
{
|
||||
revision: 7,
|
||||
version: "1.3.1",
|
||||
description: "Make Username Optional",
|
||||
releaseVersion: "0.5.0"
|
||||
},
|
||||
{
|
||||
revision: 8,
|
||||
version: "1.4.0",
|
||||
description: "Add Sync Support",
|
||||
releaseVersion: "0.6.0"
|
||||
},
|
||||
{
|
||||
revision: 9,
|
||||
version: "1.4.1",
|
||||
description: "Rename Attachments Plural",
|
||||
releaseVersion: "0.6.0"
|
||||
},
|
||||
{
|
||||
revision: 10,
|
||||
version: "1.5.0",
|
||||
description: "Add 2FA Tokens to credentials",
|
||||
releaseVersion: "0.14.0"
|
||||
}
|
||||
];
|
||||
|
||||
// src/sql/VaultSqlGenerator.ts
|
||||
var VaultSqlGenerator = class {
|
||||
/**
|
||||
* Get SQL commands to create a new vault with the latest schema
|
||||
*/
|
||||
getCreateVaultSql() {
|
||||
try {
|
||||
const sqlCommands = [
|
||||
COMPLETE_SCHEMA_SQL
|
||||
];
|
||||
return {
|
||||
success: true,
|
||||
sqlCommands,
|
||||
version: VAULT_VERSIONS[VAULT_VERSIONS.length - 1].version,
|
||||
migrationNumber: VAULT_VERSIONS[VAULT_VERSIONS.length - 1].revision
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
sqlCommands: [],
|
||||
version: "0.0.0",
|
||||
migrationNumber: 0,
|
||||
error: error instanceof Error ? error.message : "Unknown error creating vault SQL"
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get SQL commands to upgrade vault from current version to target version
|
||||
*/
|
||||
getUpgradeVaultSql(currentMigrationNumber, targetMigrationNumber) {
|
||||
try {
|
||||
const targetMigration = targetMigrationNumber ?? VAULT_VERSIONS[VAULT_VERSIONS.length - 1].revision;
|
||||
const targetVersionInfo = VAULT_VERSIONS.find((v) => v.revision === targetMigration);
|
||||
if (!targetVersionInfo) {
|
||||
return {
|
||||
success: false,
|
||||
sqlCommands: [],
|
||||
version: "0.0.0",
|
||||
migrationNumber: 0,
|
||||
error: `Target migration number ${targetMigration} not found`
|
||||
};
|
||||
}
|
||||
if (currentMigrationNumber >= targetMigration) {
|
||||
return {
|
||||
success: true,
|
||||
sqlCommands: [],
|
||||
version: targetVersionInfo.version,
|
||||
migrationNumber: targetMigration
|
||||
};
|
||||
}
|
||||
const migrationsToApply = VAULT_VERSIONS.filter(
|
||||
(v) => v.revision > currentMigrationNumber && v.revision <= targetMigration
|
||||
);
|
||||
const sqlCommands = [];
|
||||
for (const migration of migrationsToApply) {
|
||||
const migrationKey = migration.revision - 1;
|
||||
const migrationSql = MIGRATION_SCRIPTS[migrationKey];
|
||||
if (migrationSql) {
|
||||
sqlCommands.push(migrationSql);
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
sqlCommands,
|
||||
version: targetVersionInfo.version,
|
||||
migrationNumber: targetMigration
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
sqlCommands: [],
|
||||
version: "0.0.0",
|
||||
migrationNumber: 0,
|
||||
error: error instanceof Error ? error.message : "Unknown error generating upgrade SQL"
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get SQL commands to upgrade vault to latest version
|
||||
*/
|
||||
getUpgradeToLatestSql(currentMigrationNumber) {
|
||||
return this.getUpgradeVaultSql(currentMigrationNumber);
|
||||
}
|
||||
/**
|
||||
* Get SQL commands to upgrade vault to a specific version
|
||||
*/
|
||||
getUpgradeToVersionSql(currentMigrationNumber, targetVersion) {
|
||||
const targetVersionInfo = VAULT_VERSIONS.find((v) => v.version === targetVersion);
|
||||
if (!targetVersionInfo) {
|
||||
return {
|
||||
success: false,
|
||||
sqlCommands: [],
|
||||
version: "0.0.0",
|
||||
migrationNumber: 0,
|
||||
error: `Target version ${targetVersion} not found`
|
||||
};
|
||||
}
|
||||
return this.getUpgradeVaultSql(currentMigrationNumber, targetVersionInfo.revision);
|
||||
}
|
||||
/**
|
||||
* Get SQL commands to check current vault version
|
||||
*/
|
||||
getVersionCheckSql() {
|
||||
return [
|
||||
// Check if Settings table exists
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='Settings';",
|
||||
// Get vault version
|
||||
"SELECT Value FROM Settings WHERE Key = 'vault_version' AND IsDeleted = 0 LIMIT 1;",
|
||||
// Get migration number
|
||||
"SELECT Value FROM Settings WHERE Key = 'vault_migration_number' AND IsDeleted = 0 LIMIT 1;"
|
||||
];
|
||||
}
|
||||
/**
|
||||
* Get SQL command to validate vault structure
|
||||
*/
|
||||
getVaultValidationSql() {
|
||||
return `SELECT name FROM sqlite_master WHERE type='table' AND name IN
|
||||
('Aliases', 'Services', 'Credentials', 'Passwords', 'Attachments', 'EncryptionKeys', 'Settings', 'TotpCodes');`;
|
||||
}
|
||||
/**
|
||||
* Parse vault version information from query results
|
||||
*/
|
||||
parseVaultVersionInfo(settingsTableExists, versionResult, migrationResult) {
|
||||
let currentVersion = "0.0.0";
|
||||
let currentMigrationNumber = 0;
|
||||
if (settingsTableExists) {
|
||||
if (versionResult) {
|
||||
currentVersion = versionResult;
|
||||
} else {
|
||||
currentVersion = "1.0.0";
|
||||
currentMigrationNumber = 1;
|
||||
}
|
||||
if (migrationResult) {
|
||||
currentMigrationNumber = parseInt(migrationResult, 10);
|
||||
}
|
||||
}
|
||||
const latestVersion = VAULT_VERSIONS[VAULT_VERSIONS.length - 1];
|
||||
const needsUpgrade = currentMigrationNumber < latestVersion.revision;
|
||||
const availableUpgrades = VAULT_VERSIONS.filter((v) => v.revision > currentMigrationNumber);
|
||||
return {
|
||||
currentVersion,
|
||||
currentMigrationNumber,
|
||||
targetVersion: latestVersion.version,
|
||||
targetMigrationNumber: latestVersion.revision,
|
||||
needsUpgrade,
|
||||
availableUpgrades
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Validate vault structure from table names
|
||||
*/
|
||||
validateVaultStructure(tableNames) {
|
||||
const requiredTables = ["Aliases", "Services", "Credentials", "Passwords", "Attachments", "EncryptionKeys", "Settings", "TotpCodes"];
|
||||
const foundTables = tableNames.filter((name) => requiredTables.includes(name));
|
||||
return foundTables.length >= 5;
|
||||
}
|
||||
/**
|
||||
* Get all available vault versions
|
||||
*/
|
||||
getAllVersions() {
|
||||
return [...VAULT_VERSIONS];
|
||||
}
|
||||
/**
|
||||
* Get current/latest vault version info
|
||||
*/
|
||||
getLatestVersion() {
|
||||
return VAULT_VERSIONS[VAULT_VERSIONS.length - 1];
|
||||
}
|
||||
/**
|
||||
* Get specific migration SQL by migration number
|
||||
*/
|
||||
getMigrationSql(migrationNumber) {
|
||||
return MIGRATION_SCRIPTS[migrationNumber];
|
||||
}
|
||||
/**
|
||||
* Get complete schema SQL for creating new vault
|
||||
*/
|
||||
getCompleteSchemaSql() {
|
||||
return COMPLETE_SCHEMA_SQL;
|
||||
}
|
||||
};
|
||||
|
||||
// src/factories/VaultSqlGeneratorFactory.ts
|
||||
var CreateVaultSqlGenerator = () => {
|
||||
return new VaultSqlGenerator();
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
COMPLETE_SCHEMA_SQL,
|
||||
CreateVaultSqlGenerator,
|
||||
MIGRATION_SCRIPTS,
|
||||
VAULT_VERSIONS,
|
||||
VaultSqlGenerator
|
||||
});
|
||||
//# sourceMappingURL=index.js.map
|
||||
705
apps/browser-extension/src/utils/dist/shared/vault-sql/index.mjs
vendored
Normal file
705
apps/browser-extension/src/utils/dist/shared/vault-sql/index.mjs
vendored
Normal file
@@ -0,0 +1,705 @@
|
||||
// <auto-generated>
|
||||
// This file was automatically generated. Do not edit manually.
|
||||
|
||||
|
||||
// src/sql/SqlConstants.ts
|
||||
var COMPLETE_SCHEMA_SQL = `
|
||||
\uFEFFCREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
||||
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
|
||||
"ProductVersion" TEXT NOT NULL
|
||||
);
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "Aliases" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Aliases" PRIMARY KEY,
|
||||
"Gender" VARCHAR NULL,
|
||||
"FirstName" VARCHAR NULL,
|
||||
"LastName" VARCHAR NULL,
|
||||
"NickName" VARCHAR NULL,
|
||||
"BirthDate" TEXT NOT NULL,
|
||||
"AddressStreet" VARCHAR NULL,
|
||||
"AddressCity" VARCHAR NULL,
|
||||
"AddressState" VARCHAR NULL,
|
||||
"AddressZipCode" VARCHAR NULL,
|
||||
"AddressCountry" VARCHAR NULL,
|
||||
"Hobbies" TEXT NULL,
|
||||
"EmailPrefix" TEXT NULL,
|
||||
"PhoneMobile" TEXT NULL,
|
||||
"BankAccountIBAN" TEXT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "Services" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Services" PRIMARY KEY,
|
||||
"Name" TEXT NULL,
|
||||
"Url" TEXT NULL,
|
||||
"Logo" BLOB NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "Credentials" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Credentials" PRIMARY KEY,
|
||||
"AliasId" TEXT NOT NULL,
|
||||
"Notes" TEXT NULL,
|
||||
"Username" TEXT NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
"ServiceId" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_Credentials_Aliases_AliasId" FOREIGN KEY ("AliasId") REFERENCES "Aliases" ("Id") ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_Credentials_Services_ServiceId" FOREIGN KEY ("ServiceId") REFERENCES "Services" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "Attachment" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Attachment" PRIMARY KEY,
|
||||
"Filename" TEXT NOT NULL,
|
||||
"Blob" BLOB NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
"CredentialId" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_Attachment_Credentials_CredentialId" FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "Passwords" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Passwords" PRIMARY KEY,
|
||||
"Value" TEXT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
"CredentialId" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_Passwords_Credentials_CredentialId" FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_Attachment_CredentialId" ON "Attachment" ("CredentialId");
|
||||
|
||||
CREATE INDEX "IX_Credentials_AliasId" ON "Credentials" ("AliasId");
|
||||
|
||||
CREATE INDEX "IX_Credentials_ServiceId" ON "Credentials" ("ServiceId");
|
||||
|
||||
CREATE INDEX "IX_Passwords_CredentialId" ON "Passwords" ("CredentialId");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240708094944_1.0.0-InitialMigration', '9.0.4');
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240708224522_1.0.1-EmptyTestMigration', '9.0.4');
|
||||
|
||||
ALTER TABLE "Aliases" RENAME COLUMN "EmailPrefix" TO "Email";
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240711204207_1.0.2-ChangeEmailColumn', '9.0.4');
|
||||
|
||||
CREATE TABLE "EncryptionKeys" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_EncryptionKeys" PRIMARY KEY,
|
||||
"PublicKey" TEXT NOT NULL,
|
||||
"PrivateKey" TEXT NOT NULL,
|
||||
"IsPrimary" INTEGER NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240729105618_1.1.0-AddPkiTables', '9.0.4');
|
||||
|
||||
CREATE TABLE "Settings" (
|
||||
"Key" TEXT NOT NULL CONSTRAINT "PK_Settings" PRIMARY KEY,
|
||||
"Value" TEXT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240805073413_1.2.0-AddSettingsTable', '9.0.4');
|
||||
|
||||
CREATE TABLE "ef_temp_Aliases" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Aliases" PRIMARY KEY,
|
||||
"BirthDate" TEXT NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"Email" TEXT NULL,
|
||||
"FirstName" VARCHAR NULL,
|
||||
"Gender" VARCHAR NULL,
|
||||
"LastName" VARCHAR NULL,
|
||||
"NickName" VARCHAR NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_Aliases" ("Id", "BirthDate", "CreatedAt", "Email", "FirstName", "Gender", "LastName", "NickName", "UpdatedAt")
|
||||
SELECT "Id", "BirthDate", "CreatedAt", "Email", "FirstName", "Gender", "LastName", "NickName", "UpdatedAt"
|
||||
FROM "Aliases";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 0;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE "Aliases";
|
||||
|
||||
ALTER TABLE "ef_temp_Aliases" RENAME TO "Aliases";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 1;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240805122422_1.3.0-UpdateIdentityStructure', '9.0.4');
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "ef_temp_Credentials" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Credentials" PRIMARY KEY,
|
||||
"AliasId" TEXT NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"Notes" TEXT NULL,
|
||||
"ServiceId" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
"Username" TEXT NULL,
|
||||
CONSTRAINT "FK_Credentials_Aliases_AliasId" FOREIGN KEY ("AliasId") REFERENCES "Aliases" ("Id") ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_Credentials_Services_ServiceId" FOREIGN KEY ("ServiceId") REFERENCES "Services" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_Credentials" ("Id", "AliasId", "CreatedAt", "Notes", "ServiceId", "UpdatedAt", "Username")
|
||||
SELECT "Id", "AliasId", "CreatedAt", "Notes", "ServiceId", "UpdatedAt", "Username"
|
||||
FROM "Credentials";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 0;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE "Credentials";
|
||||
|
||||
ALTER TABLE "ef_temp_Credentials" RENAME TO "Credentials";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 1;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE INDEX "IX_Credentials_AliasId" ON "Credentials" ("AliasId");
|
||||
|
||||
CREATE INDEX "IX_Credentials_ServiceId" ON "Credentials" ("ServiceId");
|
||||
|
||||
COMMIT;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240812141727_1.3.1-MakeUsernameOptional', '9.0.4');
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
ALTER TABLE "Settings" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Services" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Passwords" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "EncryptionKeys" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Credentials" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Attachment" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Aliases" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240916105320_1.4.0-AddSyncSupport', '9.0.4');
|
||||
|
||||
ALTER TABLE "Attachment" RENAME TO "Attachments";
|
||||
|
||||
CREATE TABLE "ef_temp_Attachments" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Attachments" PRIMARY KEY,
|
||||
"Blob" BLOB NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"CredentialId" TEXT NOT NULL,
|
||||
"Filename" TEXT NOT NULL,
|
||||
"IsDeleted" INTEGER NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_Attachments_Credentials_CredentialId" FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_Attachments" ("Id", "Blob", "CreatedAt", "CredentialId", "Filename", "IsDeleted", "UpdatedAt")
|
||||
SELECT "Id", "Blob", "CreatedAt", "CredentialId", "Filename", "IsDeleted", "UpdatedAt"
|
||||
FROM "Attachments";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 0;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE "Attachments";
|
||||
|
||||
ALTER TABLE "ef_temp_Attachments" RENAME TO "Attachments";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 1;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE INDEX "IX_Attachments_CredentialId" ON "Attachments" ("CredentialId");
|
||||
|
||||
COMMIT;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240917191243_1.4.1-RenameAttachmentsPlural', '9.0.4');
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "TotpCodes" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_TotpCodes" PRIMARY KEY,
|
||||
"Name" TEXT NOT NULL,
|
||||
"SecretKey" TEXT NOT NULL,
|
||||
"CredentialId" TEXT NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
"IsDeleted" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_TotpCodes_Credentials_CredentialId" FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_TotpCodes_CredentialId" ON "TotpCodes" ("CredentialId");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250310131554_1.5.0-AddTotpCodes', '9.0.4');
|
||||
|
||||
COMMIT;
|
||||
`;
|
||||
var MIGRATION_SCRIPTS = {
|
||||
1: `\uFEFFBEGIN TRANSACTION;
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240708224522_1.0.1-EmptyTestMigration', '9.0.4');
|
||||
|
||||
COMMIT;`,
|
||||
2: `\uFEFFBEGIN TRANSACTION;
|
||||
ALTER TABLE "Aliases" RENAME COLUMN "EmailPrefix" TO "Email";
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240711204207_1.0.2-ChangeEmailColumn', '9.0.4');
|
||||
|
||||
COMMIT;`,
|
||||
3: `\uFEFFBEGIN TRANSACTION;
|
||||
CREATE TABLE "EncryptionKeys" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_EncryptionKeys" PRIMARY KEY,
|
||||
"PublicKey" TEXT NOT NULL,
|
||||
"PrivateKey" TEXT NOT NULL,
|
||||
"IsPrimary" INTEGER NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240729105618_1.1.0-AddPkiTables', '9.0.4');
|
||||
|
||||
COMMIT;`,
|
||||
4: `\uFEFFBEGIN TRANSACTION;
|
||||
CREATE TABLE "Settings" (
|
||||
"Key" TEXT NOT NULL CONSTRAINT "PK_Settings" PRIMARY KEY,
|
||||
"Value" TEXT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240805073413_1.2.0-AddSettingsTable', '9.0.4');
|
||||
|
||||
COMMIT;`,
|
||||
5: `\uFEFFBEGIN TRANSACTION;
|
||||
CREATE TABLE "ef_temp_Aliases" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Aliases" PRIMARY KEY,
|
||||
"BirthDate" TEXT NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"Email" TEXT NULL,
|
||||
"FirstName" VARCHAR NULL,
|
||||
"Gender" VARCHAR NULL,
|
||||
"LastName" VARCHAR NULL,
|
||||
"NickName" VARCHAR NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_Aliases" ("Id", "BirthDate", "CreatedAt", "Email", "FirstName", "Gender", "LastName", "NickName", "UpdatedAt")
|
||||
SELECT "Id", "BirthDate", "CreatedAt", "Email", "FirstName", "Gender", "LastName", "NickName", "UpdatedAt"
|
||||
FROM "Aliases";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 0;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE "Aliases";
|
||||
|
||||
ALTER TABLE "ef_temp_Aliases" RENAME TO "Aliases";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 1;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240805122422_1.3.0-UpdateIdentityStructure', '9.0.4');`,
|
||||
6: `\uFEFFBEGIN TRANSACTION;
|
||||
CREATE TABLE "ef_temp_Credentials" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Credentials" PRIMARY KEY,
|
||||
"AliasId" TEXT NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"Notes" TEXT NULL,
|
||||
"ServiceId" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
"Username" TEXT NULL,
|
||||
CONSTRAINT "FK_Credentials_Aliases_AliasId" FOREIGN KEY ("AliasId") REFERENCES "Aliases" ("Id") ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_Credentials_Services_ServiceId" FOREIGN KEY ("ServiceId") REFERENCES "Services" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_Credentials" ("Id", "AliasId", "CreatedAt", "Notes", "ServiceId", "UpdatedAt", "Username")
|
||||
SELECT "Id", "AliasId", "CreatedAt", "Notes", "ServiceId", "UpdatedAt", "Username"
|
||||
FROM "Credentials";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 0;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE "Credentials";
|
||||
|
||||
ALTER TABLE "ef_temp_Credentials" RENAME TO "Credentials";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 1;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE INDEX "IX_Credentials_AliasId" ON "Credentials" ("AliasId");
|
||||
|
||||
CREATE INDEX "IX_Credentials_ServiceId" ON "Credentials" ("ServiceId");
|
||||
|
||||
COMMIT;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240812141727_1.3.1-MakeUsernameOptional', '9.0.4');`,
|
||||
7: `\uFEFFBEGIN TRANSACTION;
|
||||
ALTER TABLE "Settings" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Services" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Passwords" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "EncryptionKeys" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Credentials" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Attachment" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "Aliases" ADD "IsDeleted" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240916105320_1.4.0-AddSyncSupport', '9.0.4');
|
||||
|
||||
COMMIT;`,
|
||||
8: `\uFEFFBEGIN TRANSACTION;
|
||||
ALTER TABLE "Attachment" RENAME TO "Attachments";
|
||||
|
||||
CREATE TABLE "ef_temp_Attachments" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Attachments" PRIMARY KEY,
|
||||
"Blob" BLOB NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"CredentialId" TEXT NOT NULL,
|
||||
"Filename" TEXT NOT NULL,
|
||||
"IsDeleted" INTEGER NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_Attachments_Credentials_CredentialId" FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_Attachments" ("Id", "Blob", "CreatedAt", "CredentialId", "Filename", "IsDeleted", "UpdatedAt")
|
||||
SELECT "Id", "Blob", "CreatedAt", "CredentialId", "Filename", "IsDeleted", "UpdatedAt"
|
||||
FROM "Attachments";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 0;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE "Attachments";
|
||||
|
||||
ALTER TABLE "ef_temp_Attachments" RENAME TO "Attachments";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 1;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE INDEX "IX_Attachments_CredentialId" ON "Attachments" ("CredentialId");
|
||||
|
||||
COMMIT;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240917191243_1.4.1-RenameAttachmentsPlural', '9.0.4');`,
|
||||
9: `\uFEFFBEGIN TRANSACTION;
|
||||
CREATE TABLE "TotpCodes" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_TotpCodes" PRIMARY KEY,
|
||||
"Name" TEXT NOT NULL,
|
||||
"SecretKey" TEXT NOT NULL,
|
||||
"CredentialId" TEXT NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
"IsDeleted" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_TotpCodes_Credentials_CredentialId" FOREIGN KEY ("CredentialId") REFERENCES "Credentials" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_TotpCodes_CredentialId" ON "TotpCodes" ("CredentialId");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250310131554_1.5.0-AddTotpCodes', '9.0.4');
|
||||
|
||||
COMMIT;`
|
||||
};
|
||||
|
||||
// src/sql/VaultVersions.ts
|
||||
var VAULT_VERSIONS = [
|
||||
{
|
||||
revision: 1,
|
||||
version: "1.0.0",
|
||||
description: "Initial Migration",
|
||||
releaseVersion: "0.1.0"
|
||||
},
|
||||
{
|
||||
revision: 2,
|
||||
version: "1.0.1",
|
||||
description: "Empty Test Migration",
|
||||
releaseVersion: "0.2.0"
|
||||
},
|
||||
{
|
||||
revision: 3,
|
||||
version: "1.0.2",
|
||||
description: "Change Email Column",
|
||||
releaseVersion: "0.3.0"
|
||||
},
|
||||
{
|
||||
revision: 4,
|
||||
version: "1.1.0",
|
||||
description: "Add Pki Tables",
|
||||
releaseVersion: "0.4.0"
|
||||
},
|
||||
{
|
||||
revision: 5,
|
||||
version: "1.2.0",
|
||||
description: "Add Settings Table",
|
||||
releaseVersion: "0.4.0"
|
||||
},
|
||||
{
|
||||
revision: 6,
|
||||
version: "1.3.0",
|
||||
description: "Update Identity Structure",
|
||||
releaseVersion: "0.5.0"
|
||||
},
|
||||
{
|
||||
revision: 7,
|
||||
version: "1.3.1",
|
||||
description: "Make Username Optional",
|
||||
releaseVersion: "0.5.0"
|
||||
},
|
||||
{
|
||||
revision: 8,
|
||||
version: "1.4.0",
|
||||
description: "Add Sync Support",
|
||||
releaseVersion: "0.6.0"
|
||||
},
|
||||
{
|
||||
revision: 9,
|
||||
version: "1.4.1",
|
||||
description: "Rename Attachments Plural",
|
||||
releaseVersion: "0.6.0"
|
||||
},
|
||||
{
|
||||
revision: 10,
|
||||
version: "1.5.0",
|
||||
description: "Add 2FA Tokens to credentials",
|
||||
releaseVersion: "0.14.0"
|
||||
}
|
||||
];
|
||||
|
||||
// src/sql/VaultSqlGenerator.ts
|
||||
var VaultSqlGenerator = class {
|
||||
/**
|
||||
* Get SQL commands to create a new vault with the latest schema
|
||||
*/
|
||||
getCreateVaultSql() {
|
||||
try {
|
||||
const sqlCommands = [
|
||||
COMPLETE_SCHEMA_SQL
|
||||
];
|
||||
return {
|
||||
success: true,
|
||||
sqlCommands,
|
||||
version: VAULT_VERSIONS[VAULT_VERSIONS.length - 1].version,
|
||||
migrationNumber: VAULT_VERSIONS[VAULT_VERSIONS.length - 1].revision
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
sqlCommands: [],
|
||||
version: "0.0.0",
|
||||
migrationNumber: 0,
|
||||
error: error instanceof Error ? error.message : "Unknown error creating vault SQL"
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get SQL commands to upgrade vault from current version to target version
|
||||
*/
|
||||
getUpgradeVaultSql(currentMigrationNumber, targetMigrationNumber) {
|
||||
try {
|
||||
const targetMigration = targetMigrationNumber ?? VAULT_VERSIONS[VAULT_VERSIONS.length - 1].revision;
|
||||
const targetVersionInfo = VAULT_VERSIONS.find((v) => v.revision === targetMigration);
|
||||
if (!targetVersionInfo) {
|
||||
return {
|
||||
success: false,
|
||||
sqlCommands: [],
|
||||
version: "0.0.0",
|
||||
migrationNumber: 0,
|
||||
error: `Target migration number ${targetMigration} not found`
|
||||
};
|
||||
}
|
||||
if (currentMigrationNumber >= targetMigration) {
|
||||
return {
|
||||
success: true,
|
||||
sqlCommands: [],
|
||||
version: targetVersionInfo.version,
|
||||
migrationNumber: targetMigration
|
||||
};
|
||||
}
|
||||
const migrationsToApply = VAULT_VERSIONS.filter(
|
||||
(v) => v.revision > currentMigrationNumber && v.revision <= targetMigration
|
||||
);
|
||||
const sqlCommands = [];
|
||||
for (const migration of migrationsToApply) {
|
||||
const migrationKey = migration.revision - 1;
|
||||
const migrationSql = MIGRATION_SCRIPTS[migrationKey];
|
||||
if (migrationSql) {
|
||||
sqlCommands.push(migrationSql);
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
sqlCommands,
|
||||
version: targetVersionInfo.version,
|
||||
migrationNumber: targetMigration
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
sqlCommands: [],
|
||||
version: "0.0.0",
|
||||
migrationNumber: 0,
|
||||
error: error instanceof Error ? error.message : "Unknown error generating upgrade SQL"
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get SQL commands to upgrade vault to latest version
|
||||
*/
|
||||
getUpgradeToLatestSql(currentMigrationNumber) {
|
||||
return this.getUpgradeVaultSql(currentMigrationNumber);
|
||||
}
|
||||
/**
|
||||
* Get SQL commands to upgrade vault to a specific version
|
||||
*/
|
||||
getUpgradeToVersionSql(currentMigrationNumber, targetVersion) {
|
||||
const targetVersionInfo = VAULT_VERSIONS.find((v) => v.version === targetVersion);
|
||||
if (!targetVersionInfo) {
|
||||
return {
|
||||
success: false,
|
||||
sqlCommands: [],
|
||||
version: "0.0.0",
|
||||
migrationNumber: 0,
|
||||
error: `Target version ${targetVersion} not found`
|
||||
};
|
||||
}
|
||||
return this.getUpgradeVaultSql(currentMigrationNumber, targetVersionInfo.revision);
|
||||
}
|
||||
/**
|
||||
* Get SQL commands to check current vault version
|
||||
*/
|
||||
getVersionCheckSql() {
|
||||
return [
|
||||
// Check if Settings table exists
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='Settings';",
|
||||
// Get vault version
|
||||
"SELECT Value FROM Settings WHERE Key = 'vault_version' AND IsDeleted = 0 LIMIT 1;",
|
||||
// Get migration number
|
||||
"SELECT Value FROM Settings WHERE Key = 'vault_migration_number' AND IsDeleted = 0 LIMIT 1;"
|
||||
];
|
||||
}
|
||||
/**
|
||||
* Get SQL command to validate vault structure
|
||||
*/
|
||||
getVaultValidationSql() {
|
||||
return `SELECT name FROM sqlite_master WHERE type='table' AND name IN
|
||||
('Aliases', 'Services', 'Credentials', 'Passwords', 'Attachments', 'EncryptionKeys', 'Settings', 'TotpCodes');`;
|
||||
}
|
||||
/**
|
||||
* Parse vault version information from query results
|
||||
*/
|
||||
parseVaultVersionInfo(settingsTableExists, versionResult, migrationResult) {
|
||||
let currentVersion = "0.0.0";
|
||||
let currentMigrationNumber = 0;
|
||||
if (settingsTableExists) {
|
||||
if (versionResult) {
|
||||
currentVersion = versionResult;
|
||||
} else {
|
||||
currentVersion = "1.0.0";
|
||||
currentMigrationNumber = 1;
|
||||
}
|
||||
if (migrationResult) {
|
||||
currentMigrationNumber = parseInt(migrationResult, 10);
|
||||
}
|
||||
}
|
||||
const latestVersion = VAULT_VERSIONS[VAULT_VERSIONS.length - 1];
|
||||
const needsUpgrade = currentMigrationNumber < latestVersion.revision;
|
||||
const availableUpgrades = VAULT_VERSIONS.filter((v) => v.revision > currentMigrationNumber);
|
||||
return {
|
||||
currentVersion,
|
||||
currentMigrationNumber,
|
||||
targetVersion: latestVersion.version,
|
||||
targetMigrationNumber: latestVersion.revision,
|
||||
needsUpgrade,
|
||||
availableUpgrades
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Validate vault structure from table names
|
||||
*/
|
||||
validateVaultStructure(tableNames) {
|
||||
const requiredTables = ["Aliases", "Services", "Credentials", "Passwords", "Attachments", "EncryptionKeys", "Settings", "TotpCodes"];
|
||||
const foundTables = tableNames.filter((name) => requiredTables.includes(name));
|
||||
return foundTables.length >= 5;
|
||||
}
|
||||
/**
|
||||
* Get all available vault versions
|
||||
*/
|
||||
getAllVersions() {
|
||||
return [...VAULT_VERSIONS];
|
||||
}
|
||||
/**
|
||||
* Get current/latest vault version info
|
||||
*/
|
||||
getLatestVersion() {
|
||||
return VAULT_VERSIONS[VAULT_VERSIONS.length - 1];
|
||||
}
|
||||
/**
|
||||
* Get specific migration SQL by migration number
|
||||
*/
|
||||
getMigrationSql(migrationNumber) {
|
||||
return MIGRATION_SCRIPTS[migrationNumber];
|
||||
}
|
||||
/**
|
||||
* Get complete schema SQL for creating new vault
|
||||
*/
|
||||
getCompleteSchemaSql() {
|
||||
return COMPLETE_SCHEMA_SQL;
|
||||
}
|
||||
};
|
||||
|
||||
// src/factories/VaultSqlGeneratorFactory.ts
|
||||
var CreateVaultSqlGenerator = () => {
|
||||
return new VaultSqlGenerator();
|
||||
};
|
||||
export {
|
||||
COMPLETE_SCHEMA_SQL,
|
||||
CreateVaultSqlGenerator,
|
||||
MIGRATION_SCRIPTS,
|
||||
VAULT_VERSIONS,
|
||||
VaultSqlGenerator
|
||||
};
|
||||
//# sourceMappingURL=index.mjs.map
|
||||
@@ -0,0 +1,8 @@
|
||||
export type IdentitySettingsResponse = {
|
||||
success: boolean,
|
||||
error?: string,
|
||||
settings?: {
|
||||
language: string,
|
||||
gender: string
|
||||
}
|
||||
};
|
||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
||||
manifest: {
|
||||
name: "AliasVault",
|
||||
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
|
||||
version: "0.19.0",
|
||||
version: "0.20.0",
|
||||
content_security_policy: {
|
||||
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
|
||||
},
|
||||
|
||||
@@ -1,50 +1,24 @@
|
||||
# Welcome to your Expo app 👋
|
||||
# Mobile App
|
||||
|
||||
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
|
||||
This folder contains the source code for the mobile app for AliasVault.
|
||||
|
||||
## Get started
|
||||
The mobile app is built using React Native and Expo:
|
||||
- [React Native](https://reactnative.dev/) is a framework for building native apps using React.
|
||||
- [Expo](https://expo.dev/) is a platform for React Native that provides tools and services.
|
||||
|
||||
1. Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Start the app
|
||||
|
||||
```bash
|
||||
npx expo start
|
||||
```
|
||||
|
||||
In the output, you'll find options to open the app in a
|
||||
|
||||
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
|
||||
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
|
||||
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
|
||||
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
|
||||
|
||||
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
|
||||
|
||||
## Get a fresh project
|
||||
|
||||
When you're ready, run:
|
||||
To build and run the mobile app, run the following commands in this directory:
|
||||
|
||||
### Install dependencies
|
||||
```bash
|
||||
npm run reset-project
|
||||
npm install
|
||||
```
|
||||
|
||||
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
|
||||
### Start the development server
|
||||
```bash
|
||||
npx expo start
|
||||
```
|
||||
|
||||
## Learn more
|
||||
|
||||
To learn more about developing your project with Expo, look at the following resources:
|
||||
|
||||
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
|
||||
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
|
||||
|
||||
## Join the community
|
||||
|
||||
Join our community of developers creating universal apps.
|
||||
|
||||
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
|
||||
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
|
||||
This will open the Expo development tools where you can run the app on:
|
||||
- iOS Simulator
|
||||
- Android Emulator
|
||||
- Physical device using Expo Go app
|
||||
|
||||
@@ -93,8 +93,8 @@ android {
|
||||
applicationId 'net.aliasvault.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 5
|
||||
versionName "0.19.0"
|
||||
versionCode 10
|
||||
versionName "0.20.0"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
|
||||
@@ -47,37 +47,70 @@ class AutofillService : AutofillService() {
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: FillCallback,
|
||||
) {
|
||||
Log.d(TAG, "onFillRequest called")
|
||||
var callbackCalled = false
|
||||
|
||||
// Check if request was cancelled
|
||||
if (cancellationSignal.isCanceled) {
|
||||
return
|
||||
fun safeCallback(response: FillResponse? = null) {
|
||||
if (!callbackCalled) {
|
||||
callbackCalled = true
|
||||
callback.onSuccess(response)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the autofill contexts for this request
|
||||
val contexts = request.fillContexts
|
||||
val context = contexts.last()
|
||||
val structure = context.structure
|
||||
try {
|
||||
Log.d(TAG, "onFillRequest called")
|
||||
|
||||
// Find any autofillable fields in the form
|
||||
val fieldFinder = FieldFinder(structure)
|
||||
fieldFinder.parseStructure()
|
||||
// Check if request was cancelled
|
||||
if (cancellationSignal.isCanceled) {
|
||||
return
|
||||
}
|
||||
|
||||
// If no password field was found, return an empty response
|
||||
if (!fieldFinder.foundPasswordField && !fieldFinder.foundUsernameField) {
|
||||
Log.d(TAG, "No password or username field found, skipping autofill")
|
||||
callback.onSuccess(null)
|
||||
return
|
||||
// Get the autofill contexts for this request
|
||||
val contexts = request.fillContexts
|
||||
val context = contexts.last()
|
||||
val structure = context.structure
|
||||
|
||||
// Find any autofillable fields in the form
|
||||
val fieldFinder = FieldFinder(structure)
|
||||
fieldFinder.parseStructure()
|
||||
|
||||
// If no password field was found, return an empty response
|
||||
if (!fieldFinder.foundPasswordField && !fieldFinder.foundUsernameField) {
|
||||
Log.d(TAG, "No password or username field found, skipping autofill")
|
||||
safeCallback()
|
||||
return
|
||||
}
|
||||
|
||||
launchActivityForAutofill(fieldFinder) { response -> safeCallback(response) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unexpected error in onFillRequest", e)
|
||||
// Provide a simple fallback response to prevent white flash
|
||||
try {
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
val presentation = RemoteViews(packageName, R.layout.autofill_dataset_item_logo)
|
||||
presentation.setTextViewText(R.id.text, "Failed to retrieve, open app")
|
||||
|
||||
val dataSetBuilder = Dataset.Builder(presentation)
|
||||
|
||||
// Add a click listener to open AliasVault app
|
||||
val intent = Intent(this@AutofillService, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
putExtra("OPEN_CREDENTIALS", true)
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this@AutofillService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
dataSetBuilder.setAuthentication(pendingIntent.intentSender)
|
||||
|
||||
responseBuilder.addDataset(dataSetBuilder.build())
|
||||
safeCallback(responseBuilder.build())
|
||||
} catch (fallbackError: Exception) {
|
||||
Log.e(TAG, "Error creating fallback response", fallbackError)
|
||||
safeCallback()
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a password field but no username field, and we have a last field,
|
||||
// assume it's the username field
|
||||
/*if (!fieldFinder.foundUsernameField && fieldFinder.lastField != null) {
|
||||
fieldFinder.autofillableFields.add(Pair(fieldFinder.lastField!!, FieldType.USERNAME))
|
||||
Log.d(TAG, "Using last field as username field: ${fieldFinder.lastField}")
|
||||
}*/
|
||||
|
||||
launchActivityForAutofill(fieldFinder, callback)
|
||||
}
|
||||
|
||||
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
||||
@@ -90,7 +123,7 @@ class AutofillService : AutofillService() {
|
||||
callback.onSuccess()
|
||||
}
|
||||
|
||||
private fun launchActivityForAutofill(fieldFinder: FieldFinder, callback: FillCallback) {
|
||||
private fun launchActivityForAutofill(fieldFinder: FieldFinder, callback: (FillResponse?) -> Unit) {
|
||||
Log.d(TAG, "Launching activity for autofill authentication")
|
||||
|
||||
// Get the app/website information from assist structure.
|
||||
@@ -100,7 +133,7 @@ class AutofillService : AutofillService() {
|
||||
// Ignore requests from our own unlock page as this would cause a loop
|
||||
if (appInfo == "net.aliasvault.app") {
|
||||
Log.d(TAG, "Skipping autofill request from AliasVault app itself")
|
||||
callback.onSuccess(null)
|
||||
callback(null)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -116,7 +149,7 @@ class AutofillService : AutofillService() {
|
||||
if (result.isEmpty()) {
|
||||
// No credentials available
|
||||
Log.d(TAG, "No credentials available")
|
||||
callback.onSuccess(null)
|
||||
callback(null)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -153,16 +186,22 @@ class AutofillService : AutofillService() {
|
||||
responseBuilder.addDataset(createOpenAppDataset(fieldFinder))
|
||||
}
|
||||
|
||||
callback.onSuccess(responseBuilder.build())
|
||||
callback(responseBuilder.build())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error parsing credentials", e)
|
||||
callback.onSuccess(null)
|
||||
// Show "Failed to retrieve, open app" option instead of failing
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
responseBuilder.addDataset(createFailedToRetrieveDataset(fieldFinder))
|
||||
callback(responseBuilder.build())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Exception) {
|
||||
Log.e(TAG, "Error getting credentials", e)
|
||||
callback.onSuccess(null)
|
||||
// Show "Failed to retrieve, open app" option instead of failing
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
responseBuilder.addDataset(createFailedToRetrieveDataset(fieldFinder))
|
||||
callback(responseBuilder.build())
|
||||
}
|
||||
})
|
||||
) {
|
||||
@@ -178,7 +217,7 @@ class AutofillService : AutofillService() {
|
||||
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
responseBuilder.addDataset(createVaultLockedDataset(fieldFinder))
|
||||
callback.onSuccess(responseBuilder.build())
|
||||
callback(responseBuilder.build())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -424,4 +463,45 @@ class AutofillService : AutofillService() {
|
||||
|
||||
return dataSetBuilder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dataset for the "failed to retrieve" option.
|
||||
* @param fieldFinder The field finder
|
||||
* @return The dataset
|
||||
*/
|
||||
private fun createFailedToRetrieveDataset(fieldFinder: FieldFinder): Dataset {
|
||||
// Create presentation for the "failed to retrieve" option
|
||||
val presentation = RemoteViews(packageName, R.layout.autofill_dataset_item_logo)
|
||||
presentation.setTextViewText(
|
||||
R.id.text,
|
||||
"Failed to retrieve, open app",
|
||||
)
|
||||
|
||||
val dataSetBuilder = Dataset.Builder(presentation)
|
||||
|
||||
// Create deep link URL
|
||||
val deepLinkUrl = "net.aliasvault.app://reinitialize"
|
||||
|
||||
// Add a click listener to open AliasVault app with deep link
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
data = android.net.Uri.parse(deepLinkUrl)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this@AutofillService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
dataSetBuilder.setAuthentication(pendingIntent.intentSender)
|
||||
|
||||
// Add a placeholder value to both username and password fields to satisfy the requirement that at least one value must be set
|
||||
if (fieldFinder.autofillableFields.isNotEmpty()) {
|
||||
for (field in fieldFinder.autofillableFields) {
|
||||
dataSetBuilder.setValue(field.first, AutofillValue.forText(""))
|
||||
}
|
||||
}
|
||||
|
||||
return dataSetBuilder.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,6 +383,22 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a raw SQL query on the vault without parameters.
|
||||
* @param query The raw SQL query
|
||||
* @param promise The promise to resolve
|
||||
*/
|
||||
@ReactMethod
|
||||
override fun executeRaw(query: String, promise: Promise) {
|
||||
try {
|
||||
vaultStore.executeRaw(query)
|
||||
promise.resolve(null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error executing raw query", e)
|
||||
promise.reject("ERR_EXECUTE_RAW", "Failed to execute raw query: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin a transaction on the vault.
|
||||
* @param promise The promise to resolve
|
||||
|
||||
@@ -380,6 +380,33 @@ class VaultStore(
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a raw SQL command on the vault without parameters (for DDL operations like CREATE TABLE).
|
||||
* @param query The SQL query
|
||||
*/
|
||||
fun executeRaw(query: String) {
|
||||
dbConnection?.let { db ->
|
||||
// Split the query by semicolons to handle multiple statements
|
||||
val statements = query.split(";")
|
||||
|
||||
for (statement in statements) {
|
||||
// Remove problematic invisible characters from string
|
||||
val trimmedStatement = statement.smartTrim()
|
||||
|
||||
// Skip empty statements and transaction control statements (handled externally)
|
||||
if (trimmedStatement.isEmpty() ||
|
||||
trimmedStatement.uppercase().startsWith("BEGIN") ||
|
||||
trimmedStatement.uppercase().startsWith("COMMIT") ||
|
||||
trimmedStatement.uppercase().startsWith("ROLLBACK")
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
db.execSQL(trimmedStatement)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin a SQL transaction on the vault.
|
||||
*/
|
||||
@@ -950,4 +977,13 @@ class VaultStore(
|
||||
Log.e(TAG, "Error parsing date: $dateString")
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove problematic invisible characters from string.
|
||||
* @return The trimmed string
|
||||
*/
|
||||
private fun String.smartTrim(): String {
|
||||
val invisible = "[\\uFEFF\\u200B\\u00A0\\u202A-\\u202E\\u2060\\u180E]"
|
||||
return this.replace(Regex("^($invisible)+|($invisible)+$"), "").trim()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"expo": {
|
||||
"name": "AliasVault",
|
||||
"slug": "AliasVault",
|
||||
"version": "0.19.0",
|
||||
"version": "0.20.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "net.aliasvault.app",
|
||||
|
||||
@@ -132,9 +132,9 @@ export default function CredentialDetailsScreen() : React.ReactNode {
|
||||
</ThemedView>
|
||||
<EmailPreview email={credential.Alias.Email} />
|
||||
<TotpSection credential={credential} />
|
||||
<NotesSection credential={credential} />
|
||||
<LoginCredentials credential={credential} />
|
||||
<AliasDetails credential={credential} />
|
||||
<NotesSection credential={credential} />
|
||||
</ThemedScrollView>
|
||||
</ThemedContainer>
|
||||
);
|
||||
|
||||
@@ -157,7 +157,12 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
const generateRandomAlias = useCallback(async (): Promise<void> => {
|
||||
const { identityGenerator, passwordGenerator } = await initializeGenerators();
|
||||
|
||||
const identity = identityGenerator.generateRandomIdentity();
|
||||
// Get gender preference from database
|
||||
const genderPreference = await dbContext.sqliteClient!.getDefaultIdentityGender();
|
||||
|
||||
// Generate identity with gender preference
|
||||
const identity = identityGenerator.generateRandomIdentity(genderPreference);
|
||||
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
const defaultEmailDomain = await dbContext.sqliteClient!.getDefaultEmailDomain();
|
||||
const email = defaultEmailDomain ? `${identity.emailPrefix}@${defaultEmailDomain}` : identity.emailPrefix;
|
||||
@@ -316,7 +321,14 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
const generateRandomUsername = async () : Promise<void> => {
|
||||
try {
|
||||
const { identityGenerator } = await initializeGenerators();
|
||||
const identity = identityGenerator.generateRandomIdentity();
|
||||
|
||||
// Get gender preference from database
|
||||
const genderPreference = await dbContext.sqliteClient!.getDefaultIdentityGender();
|
||||
|
||||
// Generate identity with gender preference
|
||||
const identity = identityGenerator.generateRandomIdentity(genderPreference);
|
||||
|
||||
// Set the username to the identity's nickname
|
||||
setValue('Username', identity.nickName);
|
||||
} catch (error) {
|
||||
console.error('Error generating random username:', error);
|
||||
|
||||
@@ -175,6 +175,12 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
// Logout user
|
||||
await webApi.logout(error);
|
||||
},
|
||||
/**
|
||||
* On upgrade required.
|
||||
*/
|
||||
onUpgradeRequired: () : void => {
|
||||
router.replace('/upgrade');
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error refreshing credentials:', err);
|
||||
@@ -186,7 +192,7 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
text2: err instanceof Error ? err.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
}, [syncVault, loadCredentials, setIsLoadingCredentials, setRefreshing, webApi, authContext]);
|
||||
}, [syncVault, loadCredentials, setIsLoadingCredentials, setRefreshing, webApi, authContext, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated || !isDatabaseAvailable) {
|
||||
|
||||
@@ -56,6 +56,13 @@ export default function SettingsLayout(): React.ReactNode {
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="identity-generator"
|
||||
options={{
|
||||
title: 'Identity Generator',
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="security/index"
|
||||
options={{
|
||||
|
||||
189
apps/mobile-app/app/(tabs)/settings/identity-generator.tsx
Normal file
189
apps/mobile-app/app/(tabs)/settings/identity-generator.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useFocusEffect } from 'expo-router';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { StyleSheet, View, Alert, TouchableOpacity } from 'react-native';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useVaultMutate } from '@/hooks/useVaultMutate';
|
||||
|
||||
import { ThemedContainer } from '@/components/themed/ThemedContainer';
|
||||
import { ThemedScrollView } from '@/components/themed/ThemedScrollView';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
|
||||
const LANGUAGE_OPTIONS = [
|
||||
{ label: 'English', value: 'en' },
|
||||
{ label: 'Dutch', value: 'nl' }
|
||||
];
|
||||
|
||||
const GENDER_OPTIONS = [
|
||||
{ label: 'Random', value: 'random' },
|
||||
{ label: 'Male', value: 'male' },
|
||||
{ label: 'Female', value: 'female' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Identity Generator Settings screen.
|
||||
*/
|
||||
export default function IdentityGeneratorSettingsScreen(): React.ReactNode {
|
||||
const colors = useColors();
|
||||
const dbContext = useDb();
|
||||
const { executeVaultMutation } = useVaultMutate();
|
||||
|
||||
const [language, setLanguage] = useState<string>('en');
|
||||
const [gender, setGender] = useState<string>('random');
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
/**
|
||||
* Load the identity generator settings.
|
||||
*/
|
||||
const loadSettings = async (): Promise<void> => {
|
||||
try {
|
||||
const [currentLanguage, currentGender] = await Promise.all([
|
||||
dbContext.sqliteClient!.getDefaultIdentityLanguage(),
|
||||
dbContext.sqliteClient!.getDefaultIdentityGender()
|
||||
]);
|
||||
|
||||
setLanguage(currentLanguage);
|
||||
setGender(currentGender);
|
||||
} catch (error) {
|
||||
console.error('Error loading identity generator settings:', error);
|
||||
Alert.alert('Error', 'Failed to load identity generator settings.');
|
||||
}
|
||||
};
|
||||
|
||||
loadSettings();
|
||||
}, [dbContext.sqliteClient])
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle language change.
|
||||
*/
|
||||
const handleLanguageChange = useCallback(async (newLanguage: string): Promise<void> => {
|
||||
try {
|
||||
executeVaultMutation(async () => {
|
||||
// Update the default language setting
|
||||
await dbContext.sqliteClient!.updateSetting('DefaultIdentityLanguage', newLanguage);
|
||||
});
|
||||
setLanguage(newLanguage);
|
||||
} catch (error) {
|
||||
console.error('Error updating language setting:', error);
|
||||
Alert.alert('Error', 'Failed to update language setting.');
|
||||
}
|
||||
}, [executeVaultMutation, dbContext.sqliteClient]);
|
||||
|
||||
/**
|
||||
* Handle gender change.
|
||||
*/
|
||||
const handleGenderChange = useCallback(async (newGender: string): Promise<void> => {
|
||||
try {
|
||||
executeVaultMutation(async () => {
|
||||
await dbContext.sqliteClient!.updateSetting('DefaultIdentityGender', newGender);
|
||||
});
|
||||
setGender(newGender);
|
||||
} catch (error) {
|
||||
console.error('Error updating gender setting:', error);
|
||||
Alert.alert('Error', 'Failed to update gender setting.');
|
||||
}
|
||||
}, [executeVaultMutation, dbContext.sqliteClient]);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
descriptionText: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
},
|
||||
headerText: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 13,
|
||||
marginBottom: 8,
|
||||
},
|
||||
option: {
|
||||
alignItems: 'center',
|
||||
borderBottomColor: colors.accentBorder,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
},
|
||||
optionContainer: {
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderRadius: 10,
|
||||
marginTop: 8,
|
||||
},
|
||||
optionLast: {
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
optionText: {
|
||||
color: colors.text,
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: colors.text,
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
marginBottom: 8,
|
||||
marginTop: 16,
|
||||
},
|
||||
selectedIcon: {
|
||||
color: colors.primary,
|
||||
marginLeft: 8,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemedContainer>
|
||||
<ThemedScrollView>
|
||||
<ThemedText style={styles.headerText}>
|
||||
Configure the default language and gender preference for generating new identities.
|
||||
</ThemedText>
|
||||
|
||||
<ThemedText style={styles.sectionTitle}>Language</ThemedText>
|
||||
<ThemedText style={styles.descriptionText}>
|
||||
Set the language that will be used when generating new identities.
|
||||
</ThemedText>
|
||||
<View style={styles.optionContainer}>
|
||||
{LANGUAGE_OPTIONS.map((option, index) => {
|
||||
const isLast = index === LANGUAGE_OPTIONS.length - 1;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={option.value}
|
||||
style={[styles.option, isLast && styles.optionLast]}
|
||||
onPress={() => handleLanguageChange(option.value)}
|
||||
>
|
||||
<ThemedText style={styles.optionText}>{option.label}</ThemedText>
|
||||
{language === option.value && (
|
||||
<Ionicons name="checkmark" size={20} style={styles.selectedIcon} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
<ThemedText style={styles.sectionTitle}>Gender</ThemedText>
|
||||
<ThemedText style={styles.descriptionText}>
|
||||
Set the gender preference for generating new identities.
|
||||
</ThemedText>
|
||||
<View style={styles.optionContainer}>
|
||||
{GENDER_OPTIONS.map((option, index) => {
|
||||
const isLast = index === GENDER_OPTIONS.length - 1;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={option.value}
|
||||
style={[styles.option, isLast && styles.optionLast]}
|
||||
onPress={() => handleGenderChange(option.value)}
|
||||
>
|
||||
<ThemedText style={styles.optionText}>{option.label}</ThemedText>
|
||||
{gender === option.value && (
|
||||
<Ionicons name="checkmark" size={20} style={styles.selectedIcon} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</ThemedScrollView>
|
||||
</ThemedContainer>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { router, useFocusEffect } from 'expo-router';
|
||||
import { useRef, useState, useCallback } from 'react';
|
||||
import { StyleSheet, View, ScrollView, TouchableOpacity, Animated, Platform, Alert } from 'react-native';
|
||||
|
||||
import { useApiUrl } from '@/utils/ApiUrlUtility';
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
@@ -25,6 +26,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
const colors = useColors();
|
||||
const { getAuthMethodDisplay, shouldShowAutofillReminder } = useAuth();
|
||||
const { getAutoLockTimeout } = useAuth();
|
||||
const { loadApiUrl, getDisplayUrl } = useApiUrl();
|
||||
const scrollY = useRef(new Animated.Value(0)).current;
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
const [autoLockDisplay, setAutoLockDisplay] = useState<string>('');
|
||||
@@ -73,12 +75,12 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
* Load all settings data.
|
||||
*/
|
||||
const loadData = async () : Promise<void> => {
|
||||
await Promise.all([loadAutoLockDisplay(), loadAuthMethodDisplay()]);
|
||||
await Promise.all([loadAutoLockDisplay(), loadAuthMethodDisplay(), loadApiUrl()]);
|
||||
setIsFirstLoad(false);
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [getAutoLockTimeout, getAuthMethodDisplay, setIsFirstLoad])
|
||||
}, [getAutoLockTimeout, getAuthMethodDisplay, setIsFirstLoad, loadApiUrl])
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -132,6 +134,13 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
router.push('/(tabs)/settings/android-autofill');
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the identity generator settings press.
|
||||
*/
|
||||
const handleIdentityGeneratorPress = () : void => {
|
||||
router.push('/(tabs)/settings/identity-generator');
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scrollContent: {
|
||||
paddingBottom: 40,
|
||||
@@ -315,6 +324,19 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<TouchableOpacity
|
||||
style={styles.settingItem}
|
||||
onPress={handleIdentityGeneratorPress}
|
||||
>
|
||||
<View style={styles.settingItemIcon}>
|
||||
<Ionicons name="person-outline" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>Identity Generator</ThemedText>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.separator} />
|
||||
<TouchableOpacity
|
||||
style={styles.settingItem}
|
||||
onPress={() => router.push('/(tabs)/settings/security')}
|
||||
@@ -323,7 +345,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
<Ionicons name="shield-checkmark" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>Security Settings</ThemedText>
|
||||
<ThemedText style={styles.settingItemText}>Security</ThemedText>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
@@ -344,7 +366,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
</View>
|
||||
|
||||
<View style={styles.versionContainer}>
|
||||
<ThemedText style={styles.versionText}>App version {AppInfo.VERSION}</ThemedText>
|
||||
<ThemedText style={styles.versionText}>App version {AppInfo.VERSION} ({getDisplayUrl()})</ThemedText>
|
||||
</View>
|
||||
</Animated.ScrollView>
|
||||
</ThemedContainer>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useFonts } from 'expo-font';
|
||||
import { Href, Stack, useRouter } from 'expo-router';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Linking, StyleSheet, Alert } from 'react-native';
|
||||
import { Linking, StyleSheet, Alert, Platform } from 'react-native';
|
||||
import 'react-native-reanimated';
|
||||
import 'react-native-get-random-values';
|
||||
import { install } from 'react-native-quick-crypto';
|
||||
@@ -71,6 +71,14 @@ function RootLayoutNav() : React.ReactNode {
|
||||
await new Promise(resolve => setTimeout(resolve, 750));
|
||||
setStatus('Decrypting vault');
|
||||
await new Promise(resolve => setTimeout(resolve, 750));
|
||||
|
||||
// Check if the vault is up to date, if not, redirect to the upgrade page.
|
||||
if (await dbContext.hasPendingMigrations()) {
|
||||
setRedirectTarget('/upgrade');
|
||||
setBootComplete(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setBootComplete(true);
|
||||
return;
|
||||
}
|
||||
@@ -160,7 +168,14 @@ function RootLayoutNav() : React.ReactNode {
|
||||
await webApi.logout(error);
|
||||
setRedirectTarget('/login');
|
||||
setBootComplete(true);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* On upgrade required.
|
||||
*/
|
||||
onUpgradeRequired: () : void => {
|
||||
setRedirectTarget('/upgrade');
|
||||
setBootComplete(true);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -247,7 +262,7 @@ function RootLayoutNav() : React.ReactNode {
|
||||
screenOptions={{
|
||||
headerShown: true,
|
||||
animation: 'none',
|
||||
headerTransparent: true,
|
||||
headerTransparent: Platform.OS === 'ios',
|
||||
headerStyle: {
|
||||
backgroundColor: colors.accentBackground,
|
||||
},
|
||||
@@ -262,6 +277,7 @@ function RootLayoutNav() : React.ReactNode {
|
||||
<Stack.Screen name="login-settings" options={{ title: 'Login Settings' }} />
|
||||
<Stack.Screen name="reinitialize" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="unlock" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="upgrade" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="+not-found" options={{ title: 'Not Found' }} />
|
||||
</Stack>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Redirect } from 'expo-router';
|
||||
|
||||
/**
|
||||
* App index which is the entry point of the app and redirects to the sync screen, which will
|
||||
* redirect to the login screen if the user is not logged in or to the main tabs screen if the user is logged in.
|
||||
* App index which is the entry point of the app and redirects to the credentials screen.
|
||||
* If user is not logged in, they will automatically be redirected to the login screen instead
|
||||
* by global navigation handlers.
|
||||
*/
|
||||
export default function AppIndex() : React.ReactNode {
|
||||
return <Redirect href={'/credentials'} />
|
||||
return <Redirect href={'/credentials'} />;
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { StyleSheet, View, Text, SafeAreaView, TextInput, TouchableOpacity, ActivityIndicator } from 'react-native';
|
||||
import { StyleSheet, View, Text, TextInput, TouchableOpacity, ActivityIndicator } from 'react-native';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
import { ThemedContainer } from '@/components/themed/ThemedContainer';
|
||||
import { ThemedScrollView } from '@/components/themed/ThemedScrollView';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
|
||||
type ApiOption = {
|
||||
@@ -72,13 +74,8 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: colors.background,
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
formContainer: {
|
||||
gap: 16,
|
||||
@@ -138,58 +135,61 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
<ThemedContainer>
|
||||
<ThemedScrollView>
|
||||
<View style={styles.content}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
</View>
|
||||
</ThemedScrollView>
|
||||
</ThemedContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<ThemedView style={styles.content}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>API Connection</Text>
|
||||
</View>
|
||||
<ThemedContainer>
|
||||
<ThemedScrollView>
|
||||
<ThemedView style={styles.content}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>API Connection</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.formContainer}>
|
||||
{DEFAULT_OPTIONS.map(option => (
|
||||
<TouchableOpacity
|
||||
key={option.value}
|
||||
style={[
|
||||
styles.optionButton,
|
||||
selectedOption === option.value && styles.optionButtonSelected
|
||||
]}
|
||||
onPress={() => handleOptionChange(option.value)}
|
||||
>
|
||||
<Text style={[
|
||||
styles.optionButtonText,
|
||||
selectedOption === option.value && styles.optionButtonTextSelected
|
||||
]}>
|
||||
{option.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
<View style={styles.formContainer}>
|
||||
{DEFAULT_OPTIONS.map(option => (
|
||||
<TouchableOpacity
|
||||
key={option.value}
|
||||
style={[
|
||||
styles.optionButton,
|
||||
selectedOption === option.value && styles.optionButtonSelected
|
||||
]}
|
||||
onPress={() => handleOptionChange(option.value)}
|
||||
>
|
||||
<Text style={[
|
||||
styles.optionButtonText,
|
||||
selectedOption === option.value && styles.optionButtonTextSelected
|
||||
]}>
|
||||
{option.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
|
||||
{selectedOption === 'custom' && (
|
||||
<View>
|
||||
<Text style={styles.label}>Custom API URL</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={customUrl}
|
||||
onChangeText={handleCustomUrlChange}
|
||||
placeholder="https://my-aliasvault-instance.com/api"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Text style={styles.versionText}>Version: {AppInfo.VERSION}</Text>
|
||||
</ThemedView>
|
||||
</SafeAreaView>
|
||||
{selectedOption === 'custom' && (
|
||||
<View>
|
||||
<Text style={styles.label}>Custom API URL</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={customUrl}
|
||||
onChangeText={handleCustomUrlChange}
|
||||
placeholder="https://my-aliasvault-instance.com/api"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.versionText}>Version: {AppInfo.VERSION}</Text>
|
||||
</ThemedView>
|
||||
</ThemedScrollView>
|
||||
</ThemedContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { router } from 'expo-router';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { StyleSheet, View, Text, SafeAreaView, TextInput, TouchableOpacity, ActivityIndicator, Animated, ScrollView, KeyboardAvoidingView, Platform, Dimensions, Alert } from 'react-native';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
import { useApiUrl } from '@/utils/ApiUrlUtility';
|
||||
import ConversionUtility from '@/utils/ConversionUtility';
|
||||
import type { EncryptionKeyDerivationParams } from '@/utils/dist/shared/models/metadata';
|
||||
import type { LoginResponse, VaultResponse } from '@/utils/dist/shared/models/webapi';
|
||||
@@ -33,19 +32,7 @@ import { useWebApi } from '@/context/WebApiContext';
|
||||
export default function LoginScreen() : React.ReactNode {
|
||||
const colors = useColors();
|
||||
const [fadeAnim] = useState(new Animated.Value(0));
|
||||
const [apiUrl, setApiUrl] = useState<string>(AppInfo.DEFAULT_API_URL);
|
||||
|
||||
/**
|
||||
* Load the API URL.
|
||||
*/
|
||||
const loadApiUrl = async () : Promise<void> => {
|
||||
const storedUrl = await AsyncStorage.getItem('apiUrl');
|
||||
if (storedUrl && storedUrl.length > 0) {
|
||||
setApiUrl(storedUrl);
|
||||
} else {
|
||||
setApiUrl(AppInfo.DEFAULT_API_URL);
|
||||
}
|
||||
};
|
||||
const { loadApiUrl, getDisplayUrl } = useApiUrl();
|
||||
|
||||
useEffect(() => {
|
||||
Animated.timing(fadeAnim, {
|
||||
@@ -54,26 +41,17 @@ export default function LoginScreen() : React.ReactNode {
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
loadApiUrl();
|
||||
}, [fadeAnim]);
|
||||
}, [fadeAnim, loadApiUrl]);
|
||||
|
||||
// Update URL when returning from settings
|
||||
useFocusEffect(() => {
|
||||
loadApiUrl();
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the display URL.
|
||||
*/
|
||||
const getDisplayUrl = () : string => {
|
||||
const cleanUrl = apiUrl.replace('https://', '').replace('/api', '');
|
||||
return cleanUrl === 'app.aliasvault.net' ? 'aliasvault.net' : cleanUrl;
|
||||
};
|
||||
|
||||
const [credentials, setCredentials] = useState({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
const [rememberMe, setRememberMe] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [twoFactorRequired, setTwoFactorRequired] = useState(false);
|
||||
@@ -192,6 +170,7 @@ export default function LoginScreen() : React.ReactNode {
|
||||
await dbContext.storeEncryptionKeyDerivationParams(encryptionKeyDerivationParams);
|
||||
await dbContext.initializeDatabase(vaultResponseJson);
|
||||
|
||||
let checkSuccess = true;
|
||||
/**
|
||||
* After setting auth tokens, execute a server status check immediately
|
||||
* which takes care of certain sanity checks such as ensuring client/server
|
||||
@@ -203,12 +182,33 @@ export default function LoginScreen() : React.ReactNode {
|
||||
* Handle the status update.
|
||||
*/
|
||||
onError: (message) => {
|
||||
checkSuccess = false;
|
||||
|
||||
// Show modal with error message
|
||||
Alert.alert('Error', message);
|
||||
webApi.logout(message);
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
/**
|
||||
* On upgrade required.
|
||||
*/
|
||||
onUpgradeRequired: async () : Promise<void> => {
|
||||
checkSuccess = false;
|
||||
|
||||
// Still login to ensure the user is logged in.
|
||||
await authContext.login();
|
||||
|
||||
// But after login, redirect to upgrade screen immediately.
|
||||
router.replace('/upgrade');
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
||||
if (!checkSuccess) {
|
||||
// If the syncvault checks have failed, we can't continue with the login process.
|
||||
return;
|
||||
}
|
||||
|
||||
await authContext.login();
|
||||
|
||||
authContext.setOfflineMode(false);
|
||||
@@ -259,7 +259,7 @@ export default function LoginScreen() : React.ReactNode {
|
||||
const validationResponse = await srpUtil.validateLogin(
|
||||
ConversionUtility.normalizeUsername(credentials.username),
|
||||
passwordHashString,
|
||||
rememberMe,
|
||||
true,
|
||||
initiateLoginResponse
|
||||
);
|
||||
|
||||
@@ -334,7 +334,7 @@ export default function LoginScreen() : React.ReactNode {
|
||||
const validationResponse = await srpUtil.validateLogin2Fa(
|
||||
ConversionUtility.normalizeUsername(credentials.username),
|
||||
passwordHashString,
|
||||
rememberMe,
|
||||
true,
|
||||
initiateLoginResponse,
|
||||
parseInt(twoFactorCode)
|
||||
);
|
||||
@@ -506,15 +506,7 @@ export default function LoginScreen() : React.ReactNode {
|
||||
},
|
||||
primaryButton: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
rememberMeContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
},
|
||||
rememberMeText: {
|
||||
color: colors.text,
|
||||
fontSize: 14,
|
||||
marginTop: 16,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
@@ -665,15 +657,6 @@ export default function LoginScreen() : React.ReactNode {
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.rememberMeContainer}>
|
||||
<TouchableOpacity
|
||||
style={styles.checkbox}
|
||||
onPress={() => setRememberMe(!rememberMe)}
|
||||
>
|
||||
<View style={[styles.checkboxInner, rememberMe && styles.checkboxChecked]} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.rememberMeText}>Remember me</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.primaryButton]}
|
||||
onPress={handleSubmit}
|
||||
|
||||
@@ -95,6 +95,13 @@ export default function ReinitializeScreen() : React.ReactNode {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
setStatus('Decrypting vault');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Check if the vault is up to date, if not, redirect to the upgrade page.
|
||||
if (await dbContext.hasPendingMigrations()) {
|
||||
router.replace('/upgrade');
|
||||
return;
|
||||
}
|
||||
|
||||
redirectToReturnUrl();
|
||||
return;
|
||||
}
|
||||
@@ -118,6 +125,12 @@ export default function ReinitializeScreen() : React.ReactNode {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we already have an unlocked vault, we can skip the sync and go straight to the credentials screen
|
||||
if (await NativeVaultManager.isVaultUnlocked()) {
|
||||
router.replace('/(tabs)/credentials');
|
||||
return;
|
||||
}
|
||||
|
||||
// First perform vault sync
|
||||
await syncVault({
|
||||
initialSync: true,
|
||||
@@ -163,7 +176,13 @@ export default function ReinitializeScreen() : React.ReactNode {
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* On upgrade required.
|
||||
*/
|
||||
onUpgradeRequired: () : void => {
|
||||
router.replace('/upgrade');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
*/
|
||||
export default function UnlockScreen() : React.ReactNode {
|
||||
const { isLoggedIn, username, isBiometricsEnabled } = useAuth();
|
||||
const { testDatabaseConnection } = useDb();
|
||||
const dbContext = useDb();
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isBiometricsAvailable, setIsBiometricsAvailable] = useState(false);
|
||||
@@ -100,7 +100,13 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
const passwordHashBase64 = Buffer.from(passwordHash).toString('base64');
|
||||
|
||||
// Initialize the database with the vault response and password
|
||||
if (await testDatabaseConnection(passwordHashBase64)) {
|
||||
if (await dbContext.testDatabaseConnection(passwordHashBase64)) {
|
||||
// Check if the vault is up to date, if not, redirect to the upgrade page.
|
||||
if (await dbContext.hasPendingMigrations()) {
|
||||
router.replace('/upgrade');
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to credentials
|
||||
router.replace('/(tabs)/credentials');
|
||||
} else {
|
||||
|
||||
459
apps/mobile-app/app/upgrade.tsx
Normal file
459
apps/mobile-app/app/upgrade.tsx
Normal file
@@ -0,0 +1,459 @@
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { router } from 'expo-router';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { StyleSheet, View, TouchableOpacity, Alert, KeyboardAvoidingView, Platform, ScrollView, Dimensions, TouchableWithoutFeedback, Keyboard, Text } from 'react-native';
|
||||
|
||||
import type { VaultVersion } from '@/utils/dist/shared/vault-sql';
|
||||
import { VaultSqlGenerator } from '@/utils/dist/shared/vault-sql';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useVaultMutate } from '@/hooks/useVaultMutate';
|
||||
import { useVaultSync } from '@/hooks/useVaultSync';
|
||||
|
||||
import Logo from '@/assets/images/logo.svg';
|
||||
import LoadingIndicator from '@/components/LoadingIndicator';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
|
||||
/**
|
||||
* Upgrade screen.
|
||||
*/
|
||||
export default function UpgradeScreen() : React.ReactNode {
|
||||
const { username } = useAuth();
|
||||
const { sqliteClient } = useDb();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentVersion, setCurrentVersion] = useState<VaultVersion | null>(null);
|
||||
const [latestVersion, setLatestVersion] = useState<VaultVersion | null>(null);
|
||||
const [upgradeStatus, setUpgradeStatus] = useState('Preparing upgrade...');
|
||||
const colors = useColors();
|
||||
const webApi = useWebApi();
|
||||
const { executeVaultMutation, isLoading: isVaultMutationLoading, syncStatus } = useVaultMutate();
|
||||
const { syncVault } = useVaultSync();
|
||||
|
||||
/**
|
||||
* Load version information from the database.
|
||||
*/
|
||||
const loadVersionInfo = useCallback(async () => {
|
||||
try {
|
||||
if (sqliteClient) {
|
||||
const current = await sqliteClient.getDatabaseVersion();
|
||||
const latest = await sqliteClient.getLatestDatabaseVersion();
|
||||
setCurrentVersion(current);
|
||||
setLatestVersion(latest);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load version information:', error);
|
||||
}
|
||||
}, [sqliteClient]);
|
||||
|
||||
useEffect(() => {
|
||||
loadVersionInfo();
|
||||
}, [loadVersionInfo]);
|
||||
|
||||
/**
|
||||
* Handle the vault upgrade.
|
||||
*/
|
||||
const handleUpgrade = async (): Promise<void> => {
|
||||
if (!sqliteClient || !currentVersion || !latestVersion) {
|
||||
Alert.alert('Error', 'Unable to get version information. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a self-hosted instance and show warning if needed
|
||||
if (await webApi.isSelfHosted()) {
|
||||
Alert.alert(
|
||||
'Self-Hosted Server',
|
||||
"If you're using a self-hosted server, make sure to also update your self-hosted instance as otherwise logging in to the web client will stop working.",
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Continue Upgrade',
|
||||
style: 'default',
|
||||
/**
|
||||
* Continue upgrade.
|
||||
*/
|
||||
onPress: async () : Promise<void> => {
|
||||
await performUpgrade();
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
} else {
|
||||
await performUpgrade();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform the actual vault upgrade.
|
||||
*/
|
||||
const performUpgrade = async (): Promise<void> => {
|
||||
if (!sqliteClient || !currentVersion || !latestVersion) {
|
||||
Alert.alert('Error', 'Unable to get version information. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setUpgradeStatus('Preparing upgrade...');
|
||||
|
||||
try {
|
||||
// Get upgrade SQL commands from vault-sql shared library
|
||||
setUpgradeStatus('Generating upgrade SQL...');
|
||||
const vaultSqlGenerator = new VaultSqlGenerator();
|
||||
const upgradeResult = vaultSqlGenerator.getUpgradeVaultSql(currentVersion.revision, latestVersion.revision);
|
||||
|
||||
if (!upgradeResult.success) {
|
||||
throw new Error(upgradeResult.error ?? 'Failed to generate upgrade SQL');
|
||||
}
|
||||
|
||||
if (upgradeResult.sqlCommands.length === 0) {
|
||||
// No upgrade needed, vault is already up to date
|
||||
setUpgradeStatus('Vault is already up to date');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await handleUpgradeSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the useVaultMutate hook to handle the upgrade and vault upload
|
||||
await executeVaultMutation(async () => {
|
||||
// Begin transaction
|
||||
setUpgradeStatus('Starting database transaction...');
|
||||
await NativeVaultManager.beginTransaction();
|
||||
|
||||
// Execute each SQL command
|
||||
setUpgradeStatus('Applying database migrations...');
|
||||
for (let i = 0; i < upgradeResult.sqlCommands.length; i++) {
|
||||
const sqlCommand = upgradeResult.sqlCommands[i];
|
||||
setUpgradeStatus(`Applying migration ${i + 1} of ${upgradeResult.sqlCommands.length}...`);
|
||||
|
||||
try {
|
||||
await NativeVaultManager.executeRaw(sqlCommand);
|
||||
} catch (error) {
|
||||
console.error(`Error executing SQL command ${i + 1}:`, sqlCommand, error);
|
||||
await NativeVaultManager.rollbackTransaction();
|
||||
throw new Error(`Failed to apply migration (${i + 1} of ${upgradeResult.sqlCommands.length})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
setUpgradeStatus('Committing changes...');
|
||||
await NativeVaultManager.commitTransaction();
|
||||
}, {
|
||||
skipSyncCheck: true, // Skip sync check during upgrade to prevent loop
|
||||
/**
|
||||
* Handle successful upgrade completion.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
void handleUpgradeSuccess();
|
||||
},
|
||||
/**
|
||||
* Handle upgrade error.
|
||||
*/
|
||||
onError: (error: Error) => {
|
||||
console.error('Upgrade failed:', error);
|
||||
Alert.alert('Upgrade Failed', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Upgrade failed:', error);
|
||||
Alert.alert('Upgrade Failed', error instanceof Error ? error.message : 'An unknown error occurred during the upgrade. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setUpgradeStatus('Preparing upgrade...');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle successful upgrade completion.
|
||||
*/
|
||||
const handleUpgradeSuccess = async () : Promise<void> => {
|
||||
try {
|
||||
// Sync vault to ensure we have the latest data
|
||||
await syncVault({
|
||||
/**
|
||||
* Handle the status update.
|
||||
*/
|
||||
onStatus: (message) => setUpgradeStatus(message),
|
||||
/**
|
||||
* Handle successful vault sync and navigate to credentials.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
// Navigate to credentials index
|
||||
router.replace('/(tabs)/credentials');
|
||||
},
|
||||
/**
|
||||
* Handle sync error and still navigate to credentials.
|
||||
*/
|
||||
onError: (error) => {
|
||||
console.error('Sync error after upgrade:', error);
|
||||
// Still navigate to credentials even if sync fails
|
||||
router.replace('/(tabs)/credentials');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error during post-upgrade sync:', error);
|
||||
// Navigate to credentials even if sync fails
|
||||
router.replace('/(tabs)/credentials');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the logout.
|
||||
*/
|
||||
const handleLogout = async () : Promise<void> => {
|
||||
/*
|
||||
* Clear any stored tokens or session data
|
||||
* This will be handled by the auth context
|
||||
*/
|
||||
await webApi.logout();
|
||||
router.replace('/login');
|
||||
};
|
||||
|
||||
/**
|
||||
* Show native dialog with version description.
|
||||
*/
|
||||
const showVersionDialog = (): void => {
|
||||
Alert.alert(
|
||||
"What's New",
|
||||
`An upgrade is required to support the following changes:\n\n${latestVersion?.description ?? 'No description available for this version.'}`,
|
||||
[
|
||||
{ text: 'Okay', style: 'default' }
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
appName: {
|
||||
color: colors.text,
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
},
|
||||
avatarContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
button: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
height: 50,
|
||||
justifyContent: 'center',
|
||||
marginBottom: 16,
|
||||
width: '100%',
|
||||
},
|
||||
buttonText: {
|
||||
color: colors.primarySurfaceText,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderRadius: 10,
|
||||
padding: 20,
|
||||
width: '100%',
|
||||
},
|
||||
currentVersionValue: {
|
||||
color: colors.primary,
|
||||
},
|
||||
gradientContainer: {
|
||||
height: Dimensions.get('window').height * 0.4,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
},
|
||||
headerSection: {
|
||||
paddingBottom: 24,
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 24,
|
||||
},
|
||||
helpButton: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderRadius: 20,
|
||||
height: 24,
|
||||
justifyContent: 'center',
|
||||
marginLeft: 8,
|
||||
width: 24,
|
||||
},
|
||||
helpButtonText: {
|
||||
color: colors.text,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
keyboardAvoidingView: {
|
||||
flex: 1,
|
||||
},
|
||||
latestVersionValue: {
|
||||
color: colors.greenBackground,
|
||||
},
|
||||
loadingContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
logoContainer: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
logoutButton: {
|
||||
alignSelf: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: 16,
|
||||
},
|
||||
logoutButtonText: {
|
||||
color: colors.red,
|
||||
fontSize: 16,
|
||||
},
|
||||
mainContent: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingBottom: 40,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
subtitle: {
|
||||
color: colors.text,
|
||||
fontSize: 14,
|
||||
marginBottom: 24,
|
||||
opacity: 0.7,
|
||||
textAlign: 'center',
|
||||
},
|
||||
username: {
|
||||
color: colors.text,
|
||||
fontSize: 18,
|
||||
opacity: 0.8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
versionContainer: {
|
||||
backgroundColor: colors.background,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
},
|
||||
versionHeader: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
versionLabel: {
|
||||
color: colors.text,
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
opacity: 0.7,
|
||||
},
|
||||
versionRow: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 8,
|
||||
},
|
||||
versionTitle: {
|
||||
color: colors.text,
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
versionValue: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
{(isLoading || isVaultMutationLoading) ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<LoadingIndicator status={syncStatus || upgradeStatus} />
|
||||
</View>
|
||||
) : (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.keyboardAvoidingView}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.loginHeader, colors.background]}
|
||||
style={styles.gradientContainer}
|
||||
/>
|
||||
<View style={styles.mainContent}>
|
||||
<View style={styles.headerSection}>
|
||||
<View style={styles.logoContainer}>
|
||||
<Logo width={80} height={80} />
|
||||
<Text style={styles.appName}>Upgrade Vault</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.avatarContainer}>
|
||||
<Avatar />
|
||||
<ThemedText style={styles.username}>{username}</ThemedText>
|
||||
</View>
|
||||
<ThemedText style={styles.subtitle}>AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.</ThemedText>
|
||||
<View style={styles.versionContainer}>
|
||||
<View style={styles.versionHeader}>
|
||||
<ThemedText style={styles.versionTitle}>Version Information</ThemedText>
|
||||
<TouchableOpacity
|
||||
style={styles.helpButton}
|
||||
onPress={showVersionDialog}
|
||||
>
|
||||
<ThemedText style={styles.helpButtonText}>?</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.versionRow}>
|
||||
<ThemedText style={styles.versionLabel}>Your vault:</ThemedText>
|
||||
<ThemedText style={[styles.versionValue, styles.currentVersionValue]}>
|
||||
{currentVersion?.releaseVersion ?? '...'}
|
||||
</ThemedText>
|
||||
</View>
|
||||
<View style={styles.versionRow}>
|
||||
<ThemedText style={styles.versionLabel}>New version:</ThemedText>
|
||||
<ThemedText style={[styles.versionValue, styles.latestVersionValue]}>
|
||||
{latestVersion?.releaseVersion ?? '...'}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={handleUpgrade}
|
||||
disabled={isLoading || isVaultMutationLoading}
|
||||
>
|
||||
<ThemedText style={styles.buttonText}>
|
||||
{isLoading || isVaultMutationLoading ? (syncStatus || 'Upgrading...') : 'Upgrade'}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.logoutButton}
|
||||
onPress={handleLogout}
|
||||
>
|
||||
<ThemedText style={styles.logoutButtonText}>Logout</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</TouchableWithoutFeedback>
|
||||
</KeyboardAvoidingView>
|
||||
)}
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) : React.Rea
|
||||
const [isSpamOk, setIsSpamOk] = useState(false);
|
||||
const [isComponentVisible, setIsComponentVisible] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSupportedDomain, setIsSupportedDomain] = useState(false);
|
||||
const webApi = useWebApi();
|
||||
const dbContext = useDb();
|
||||
const authContext = useAuth();
|
||||
@@ -48,6 +49,19 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) : React.Rea
|
||||
return metadata.publicEmailDomains.includes(emailAddress.split('@')[1]);
|
||||
}, [dbContext]);
|
||||
|
||||
/**
|
||||
* Check if the email is a private domain.
|
||||
*/
|
||||
const isPrivateDomain = useCallback(async (emailAddress: string): Promise<boolean> => {
|
||||
// Get private domains from stored metadata
|
||||
const metadata = await dbContext?.sqliteClient?.getVaultMetadata();
|
||||
if (!metadata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return metadata.privateEmailDomains.includes(emailAddress.split('@')[1]);
|
||||
}, [dbContext]);
|
||||
|
||||
// Handle app state changes
|
||||
useEffect(() => {
|
||||
const subscription = AppState.addEventListener('change', (nextAppState): void => {
|
||||
@@ -86,7 +100,15 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) : React.Rea
|
||||
}
|
||||
|
||||
const isPublic = await isPublicDomain(email);
|
||||
const isPrivate = await isPrivateDomain(email);
|
||||
const isSupported = isPublic || isPrivate;
|
||||
|
||||
setIsSpamOk(isPublic);
|
||||
setIsSupportedDomain(isSupported);
|
||||
|
||||
if (!isSupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPublic) {
|
||||
// For public domains (SpamOK), use the SpamOK API directly
|
||||
@@ -116,7 +138,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) : React.Rea
|
||||
}
|
||||
|
||||
setEmails(latestMails);
|
||||
} else {
|
||||
} else if (isPrivate) {
|
||||
// For private domains, use existing encrypted email logic
|
||||
if (!dbContext?.sqliteClient) {
|
||||
return;
|
||||
@@ -186,7 +208,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) : React.Rea
|
||||
clearInterval(interval);
|
||||
}
|
||||
};
|
||||
}, [email, loading, webApi, dbContext, isPublicDomain, authContext.isOffline, isComponentVisible]);
|
||||
}, [email, loading, webApi, dbContext, isPublicDomain, isPrivateDomain, authContext.isOffline, isComponentVisible]);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
date: {
|
||||
@@ -244,6 +266,11 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) : React.Rea
|
||||
return null;
|
||||
}
|
||||
|
||||
// Don't render anything if the domain is not supported
|
||||
if (!isSupportedDomain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ThemedView style={styles.section}>
|
||||
|
||||
@@ -13,6 +13,7 @@ type DbContextType = {
|
||||
storeEncryptionKey: (derivedKey: string) => Promise<void>;
|
||||
storeEncryptionKeyDerivationParams: (keyDerivationParams: EncryptionKeyDerivationParams) => Promise<void>;
|
||||
initializeDatabase: (vaultResponse: VaultResponse) => Promise<void>;
|
||||
hasPendingMigrations: () => Promise<boolean>;
|
||||
clearDatabase: () => void;
|
||||
getVaultMetadata: () => Promise<VaultMetadata | null>;
|
||||
testDatabaseConnection: (derivedKey: string) => Promise<boolean>;
|
||||
@@ -97,6 +98,17 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
setDbAvailable(true);
|
||||
}, [sqliteClient, unlockVault]);
|
||||
|
||||
/**
|
||||
* Check if there are any pending migrations. This method also checks if the current vault version is known to the client.
|
||||
* If the current vault version is not known to the client, the method will throw an exception which causes the app to logout.
|
||||
*/
|
||||
const hasPendingMigrations = useCallback(async () => {
|
||||
const currentVersion = await sqliteClient.getDatabaseVersion();
|
||||
const latestVersion = await sqliteClient.getLatestDatabaseVersion();
|
||||
|
||||
return currentVersion.revision < latestVersion.revision;
|
||||
}, [sqliteClient]);
|
||||
|
||||
const checkStoredVault = useCallback(async () => {
|
||||
try {
|
||||
const hasEncryptedDatabase = await NativeVaultManager.hasEncryptedDatabase();
|
||||
@@ -166,7 +178,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
|
||||
// Try to get the database version as a simple test query
|
||||
const version = await sqliteClient.getDatabaseVersion();
|
||||
if (version && version.length > 0) {
|
||||
if (version && version.version && version.version.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -182,13 +194,14 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
dbInitialized,
|
||||
dbAvailable,
|
||||
initializeDatabase,
|
||||
hasPendingMigrations,
|
||||
clearDatabase,
|
||||
getVaultMetadata,
|
||||
testDatabaseConnection,
|
||||
unlockVault,
|
||||
storeEncryptionKey,
|
||||
storeEncryptionKeyDerivationParams,
|
||||
}), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, clearDatabase, getVaultMetadata, testDatabaseConnection, unlockVault, storeEncryptionKey, storeEncryptionKeyDerivationParams]);
|
||||
}), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, hasPendingMigrations, clearDatabase, getVaultMetadata, testDatabaseConnection, unlockVault, storeEncryptionKey, storeEncryptionKeyDerivationParams]);
|
||||
|
||||
return (
|
||||
<DbContext.Provider value={contextValue}>
|
||||
|
||||
@@ -21,6 +21,7 @@ type VaultPostResponse = {
|
||||
type VaultMutationOptions = {
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
skipSyncCheck?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +86,7 @@ export function useVaultMutate() : {
|
||||
client: '', // Empty on purpose, API will not use this for vault updates
|
||||
updatedAt: new Date().toISOString(),
|
||||
username: username,
|
||||
version: await dbContext.sqliteClient!.getDatabaseVersion() ?? '0.0.0'
|
||||
version: (await dbContext.sqliteClient!.getDatabaseVersion())?.version ?? '0.0.0'
|
||||
};
|
||||
}, [dbContext, authContext]);
|
||||
|
||||
@@ -115,9 +116,12 @@ export function useVaultMutate() : {
|
||||
await NativeVaultManager.setCurrentVaultRevisionNumber(response.newRevisionNumber);
|
||||
options.onSuccess?.();
|
||||
} else if (response.status === 1) {
|
||||
// Note: vault merge is no longer allowed by the API as of 0.20.0, updates with the same revision number are rejected. So this check can be removed later.
|
||||
throw new Error('Vault merge required. Please login via the web app to merge the multiple pending updates to your vault.');
|
||||
} else if (response.status === 2) {
|
||||
throw new Error('Your vault is outdated. Please login on the AliasVault website and follow the steps.');
|
||||
} else {
|
||||
throw new Error('Failed to upload vault to server');
|
||||
throw new Error('Failed to upload vault to server. Please try again by re-opening the app.');
|
||||
}
|
||||
} catch (error) {
|
||||
// Check if it's a network error
|
||||
@@ -245,6 +249,13 @@ export function useVaultMutate() : {
|
||||
setIsLoading(true);
|
||||
setSyncStatus('Checking for vault updates');
|
||||
|
||||
// Skip sync check if requested (e.g., during upgrade operations)
|
||||
if (options.skipSyncCheck) {
|
||||
setSyncStatus('Executing operation...');
|
||||
await executeMutateOperation(operation, options);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're in offline mode, try to sync once to see if we can get back online
|
||||
if (authContext.isOffline) {
|
||||
await syncVault({
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { VaultResponse } from '@/utils/dist/shared/models/webapi';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
|
||||
/**
|
||||
* Utility function to ensure a minimum time has elapsed for an operation
|
||||
@@ -37,6 +38,7 @@ type VaultSyncOptions = {
|
||||
onError?: (error: string) => void;
|
||||
onStatus?: (message: string) => void;
|
||||
onOffline?: () => void;
|
||||
onUpgradeRequired?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +52,7 @@ export const useVaultSync = () : {
|
||||
const webApi = useWebApi();
|
||||
|
||||
const syncVault = useCallback(async (options: VaultSyncOptions = {}) => {
|
||||
const { initialSync = false, onSuccess, onError, onStatus, onOffline } = options;
|
||||
const { initialSync = false, onSuccess, onError, onStatus, onOffline, onUpgradeRequired } = options;
|
||||
|
||||
// For the initial sync, we add an artifical delay to various steps which makes it feel more fluid.
|
||||
const enableDelay = initialSync;
|
||||
@@ -112,14 +114,27 @@ export const useVaultSync = () : {
|
||||
|
||||
try {
|
||||
await dbContext.initializeDatabase(vaultResponseJson as VaultResponse);
|
||||
|
||||
// Check if the current vault version is known and up to date, if not known trigger an exception, if not up to date redirect to the upgrade page.
|
||||
if (await NativeVaultManager.isVaultUnlocked() && await dbContext.hasPendingMigrations()) {
|
||||
onUpgradeRequired?.();
|
||||
return false;
|
||||
}
|
||||
|
||||
onSuccess?.(true);
|
||||
return true;
|
||||
} catch {
|
||||
// Vault could not be decrypted, throw an error
|
||||
throw new Error('Vault could not be decrypted, if problem persists please logout and login again.');
|
||||
throw new Error('Vault could not be decrypted, if the problem persists please logout and login again.');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the vault is up to date, if not, redirect to the upgrade page.
|
||||
if (await NativeVaultManager.isVaultUnlocked() && await dbContext.hasPendingMigrations()) {
|
||||
onUpgradeRequired?.();
|
||||
return false;
|
||||
}
|
||||
|
||||
await withMinimumDelay(() => Promise.resolve(onSuccess?.(false)), 300, enableDelay);
|
||||
return false;
|
||||
} catch (err) {
|
||||
|
||||
@@ -1041,7 +1041,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
@@ -1056,7 +1056,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.19.0;
|
||||
MARKETING_VERSION = 0.20.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1081,7 +1081,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
INFOPLIST_FILE = AliasVault/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = AliasVault;
|
||||
@@ -1091,7 +1091,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.19.0;
|
||||
MARKETING_VERSION = 0.20.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1235,7 +1235,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1288,7 +1288,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1337,7 +1337,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1372,7 +1372,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1405,7 +1405,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1458,7 +1458,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1507,7 +1507,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1559,7 +1559,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1610,7 +1610,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = autofill/autofill.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1626,7 +1626,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 0.19.0;
|
||||
MARKETING_VERSION = 0.20.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
@@ -1655,7 +1655,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = autofill/autofill.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1671,7 +1671,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 0.19.0;
|
||||
MARKETING_VERSION = 0.20.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.app.autofill;
|
||||
|
||||
@@ -47,11 +47,29 @@ public class CredentialProviderViewController: ASCredentialProviderViewControlle
|
||||
|
||||
// Only set up the view if we haven't already
|
||||
if hostingController == nil {
|
||||
setupView(vaultStore: vaultStore)
|
||||
do {
|
||||
try setupView(vaultStore: vaultStore)
|
||||
} catch {
|
||||
print("Failed to setup view: \(error)")
|
||||
let alert = UIAlertController(
|
||||
title: "Loading Error",
|
||||
message: "Loading credentials went wrong. Please open the AliasVault app to check for updates.",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
|
||||
self?.extensionContext.cancelRequest(withError: NSError(
|
||||
domain: ASExtensionErrorDomain,
|
||||
code: ASExtensionError.failed.rawValue,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Failed to load credentials"]
|
||||
))
|
||||
})
|
||||
present(alert, animated: true)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupView(vaultStore: VaultStore) {
|
||||
private func setupView(vaultStore: VaultStore) throws {
|
||||
// Create the ViewModel with injected behaviors
|
||||
let viewModel = CredentialProviderViewModel(
|
||||
loader: {
|
||||
|
||||
@@ -45,6 +45,10 @@
|
||||
[vaultManager executeUpdate:query params:params resolver:resolve rejecter:reject];
|
||||
}
|
||||
|
||||
- (void)executeRaw:(NSString *)query resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
||||
[vaultManager executeRaw:query resolver:resolve rejecter:reject];
|
||||
}
|
||||
|
||||
- (void)beginTransaction:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
||||
[vaultManager beginTransaction:resolve rejecter:reject];
|
||||
}
|
||||
|
||||
@@ -163,6 +163,19 @@ public class VaultManager: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func executeRaw(_ query: String,
|
||||
resolver resolve: @escaping RCTPromiseResolveBlock,
|
||||
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
||||
do {
|
||||
// Execute the raw query through the vault store
|
||||
try vaultStore.executeRaw(query)
|
||||
resolve(nil)
|
||||
} catch {
|
||||
reject("RAW_ERROR", "Failed to execute raw query: \(error.localizedDescription)", error)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func clearVault() {
|
||||
do {
|
||||
|
||||
18
apps/mobile-app/ios/VaultStoreKit/StringExtensions.swift
Normal file
18
apps/mobile-app/ios/VaultStoreKit/StringExtensions.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
import Foundation
|
||||
|
||||
/// This class contains common string extension methods.
|
||||
extension String {
|
||||
/// Trims standard and invisible characters only from the beginning and end of the string.
|
||||
func smartTrim() -> String {
|
||||
let invisiblePattern = #"^[\u{FEFF}\u{200B}\u{00A0}\u{202A}-\u{202E}\u{2060}\u{180E}]+|[\u{FEFF}\u{200B}\u{00A0}\u{202A}-\u{202E}\u{2060}\u{180E}]+$"#
|
||||
|
||||
guard let regex = try? NSRegularExpression(pattern: invisiblePattern, options: []) else {
|
||||
// Fallback to trimming only whitespace if regex creation fails
|
||||
return self.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
let range = NSRange(location: 0, length: self.utf16.count)
|
||||
let cleaned = regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "")
|
||||
return cleaned.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,30 @@ extension VaultStore {
|
||||
return dbConnection.changes
|
||||
}
|
||||
|
||||
/// Execute a raw SQL command on the database without parameters (for DDL operations like CREATE TABLE).
|
||||
public func executeRaw(_ query: String) throws {
|
||||
guard let dbConnection = self.dbConnection else {
|
||||
throw NSError(domain: "VaultStore", code: 4, userInfo: [NSLocalizedDescriptionKey: "Database not initialized"])
|
||||
}
|
||||
|
||||
// Split the query by semicolons to handle multiple statements
|
||||
let statements = query.components(separatedBy: ";")
|
||||
|
||||
for statement in statements {
|
||||
let trimmedStatement = statement.smartTrim()
|
||||
|
||||
// Skip empty statements and transaction control statements (handled externally)
|
||||
if trimmedStatement.isEmpty ||
|
||||
trimmedStatement.uppercased().hasPrefix("BEGIN TRANSACTION") ||
|
||||
trimmedStatement.uppercased().hasPrefix("COMMIT") ||
|
||||
trimmedStatement.uppercased().hasPrefix("ROLLBACK") {
|
||||
continue
|
||||
}
|
||||
|
||||
try dbConnection.execute(trimmedStatement)
|
||||
}
|
||||
}
|
||||
|
||||
/// Begin a transaction on the database. This is required for all database operations that modify the database.
|
||||
public func beginTransaction() throws {
|
||||
guard let dbConnection = self.dbConnection else {
|
||||
|
||||
@@ -5,10 +5,17 @@ import VaultModels
|
||||
public struct CredentialCard: View {
|
||||
let credential: Credential
|
||||
let action: () -> Void
|
||||
let onCopy: () -> Void
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@State private var showCopyToast = false
|
||||
@State private var copyToastMessage = ""
|
||||
|
||||
public init(credential: Credential, action: @escaping () -> Void, onCopy: @escaping () -> Void) {
|
||||
self.credential = credential
|
||||
self.action = action
|
||||
self.onCopy = onCopy
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 16) {
|
||||
@@ -42,39 +49,56 @@ public struct CredentialCard: View {
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.contextMenu(menuItems: {
|
||||
Button(action: {
|
||||
if let username = credential.username {
|
||||
if let username = credential.username, !username.isEmpty {
|
||||
Button(action: {
|
||||
UIPasteboard.general.string = username
|
||||
copyToastMessage = "Username copied"
|
||||
showCopyToast = true
|
||||
}
|
||||
}, label: {
|
||||
Label("Copy Username", systemImage: "person")
|
||||
})
|
||||
Button(action: {
|
||||
if let password = credential.password?.value {
|
||||
// Delay for 1 second before calling onCopy which dismisses the view
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
onCopy()
|
||||
}
|
||||
}, label: {
|
||||
Label("Copy Username", systemImage: "person")
|
||||
})
|
||||
}
|
||||
|
||||
if let password = credential.password?.value, !password.isEmpty {
|
||||
Button(action: {
|
||||
UIPasteboard.general.string = password
|
||||
copyToastMessage = "Password copied"
|
||||
showCopyToast = true
|
||||
}
|
||||
}, label: {
|
||||
Label("Copy Password", systemImage: "key")
|
||||
})
|
||||
// Delay for 1 second before calling onCopy which dismisses the view
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
onCopy()
|
||||
}
|
||||
}, label: {
|
||||
Label("Copy Password", systemImage: "key")
|
||||
})
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
if let email = credential.alias?.email {
|
||||
if let email = credential.alias?.email, !email.isEmpty {
|
||||
Button(action: {
|
||||
UIPasteboard.general.string = email
|
||||
copyToastMessage = "Email copied"
|
||||
showCopyToast = true
|
||||
}
|
||||
}, label: {
|
||||
Label("Copy Email", systemImage: "envelope")
|
||||
})
|
||||
// Delay for 1 second before calling onCopy which dismisses the view
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
onCopy()
|
||||
}
|
||||
}, label: {
|
||||
Label("Copy Email", systemImage: "envelope")
|
||||
})
|
||||
}
|
||||
|
||||
Divider()
|
||||
if (credential.username != nil && !credential.username!.isEmpty) ||
|
||||
(credential.password?.value != nil && !credential.password!.value.isEmpty) ||
|
||||
(credential.alias?.email != nil && !credential.alias!.email!.isEmpty) {
|
||||
Divider()
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
if let url = URL(string: "aliasvault://credentials/\(credential.id.uuidString)") {
|
||||
if let url = URL(string: "net.aliasvault.app://credentials/\(credential.id.uuidString)") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}, label: {
|
||||
@@ -82,7 +106,7 @@ public struct CredentialCard: View {
|
||||
})
|
||||
|
||||
Button(action: {
|
||||
if let url = URL(string: "aliasvault://credentials/add-edit-page?id=\(credential.id.uuidString)") {
|
||||
if let url = URL(string: "net.aliasvault.app://credentials/add-edit-page?id=\(credential.id.uuidString)") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}, label: {
|
||||
@@ -97,7 +121,7 @@ public struct CredentialCard: View {
|
||||
Text(copyToastMessage)
|
||||
.padding()
|
||||
.background(Color.black.opacity(0.7))
|
||||
.foregroundColor(colorScheme == .dark ? ColorConstants.Dark.accentBackground : ColorConstants.Light.accentBackground)
|
||||
.foregroundColor(colorScheme == .dark ? ColorConstants.Dark.text : ColorConstants.Light.text)
|
||||
.cornerRadius(8)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
@@ -176,6 +200,7 @@ public func truncateText(_ text: String?, limit: Int) -> String {
|
||||
updatedAt: Date(),
|
||||
isDeleted: false
|
||||
),
|
||||
action: {}
|
||||
action: {},
|
||||
onCopy: {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ public struct SearchBarView: View {
|
||||
|
||||
TextField("Search credentials...", text: $text)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.foregroundColor(colorScheme == .dark ? ColorConstants.Dark.text : ColorConstants.Light.text)
|
||||
.padding(.leading, 4)
|
||||
.padding(.trailing, 28) // Space for clear button
|
||||
|
||||
@@ -81,9 +81,11 @@ public struct CredentialProviderView: View {
|
||||
} else {
|
||||
LazyVStack(spacing: 8) {
|
||||
ForEach(viewModel.filteredCredentials, id: \.service) { credential in
|
||||
CredentialCard(credential: credential) {
|
||||
CredentialCard(credential: credential, action: {
|
||||
viewModel.selectCredential(credential)
|
||||
}
|
||||
}, onCopy: {
|
||||
viewModel.cancel()
|
||||
})
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
@@ -238,7 +240,9 @@ public class CredentialProviderViewModel: ObservableObject {
|
||||
filterCredentials()
|
||||
isLoading = false
|
||||
} catch {
|
||||
handleError(error)
|
||||
isLoading = false
|
||||
errorMessage = "Failed to load credentials. Please open the AliasVault app to check for updates."
|
||||
showError = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,14 +379,6 @@ public class CredentialProviderViewModel: ObservableObject {
|
||||
func dismissError() {
|
||||
showError = false
|
||||
}
|
||||
|
||||
private func handleError(_ error: Error) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.isLoading = false
|
||||
self?.errorMessage = error.localizedDescription
|
||||
self?.showError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview Helpers
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface Spec extends TurboModule {
|
||||
// SQL operations
|
||||
executeQuery(query: string, params: (string | number | null)[]): Promise<string[]>;
|
||||
executeUpdate(query: string, params:(string | number | null)[]): Promise<number>;
|
||||
executeRaw(query: string): Promise<void>;
|
||||
beginTransaction(): Promise<void>;
|
||||
commitTransaction(): Promise<void>;
|
||||
rollbackTransaction(): Promise<void>;
|
||||
|
||||
45
apps/mobile-app/utils/ApiUrlUtility.ts
Normal file
45
apps/mobile-app/utils/ApiUrlUtility.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
|
||||
/**
|
||||
* Hook to manage API URL state and display logic.
|
||||
* @returns Object containing apiUrl state and utility functions
|
||||
*/
|
||||
export const useApiUrl = (): {
|
||||
apiUrl: string;
|
||||
setApiUrl: (url: string) => void;
|
||||
loadApiUrl: () => Promise<void>;
|
||||
getDisplayUrl: () => string;
|
||||
} => {
|
||||
const [apiUrl, setApiUrl] = useState<string>(AppInfo.DEFAULT_API_URL);
|
||||
|
||||
/**
|
||||
* Load the API URL from storage.
|
||||
*/
|
||||
const loadApiUrl = async (): Promise<void> => {
|
||||
const storedUrl = await AsyncStorage.getItem('apiUrl');
|
||||
if (storedUrl && storedUrl.length > 0) {
|
||||
setApiUrl(storedUrl);
|
||||
} else {
|
||||
setApiUrl(AppInfo.DEFAULT_API_URL);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the display URL for UI presentation.
|
||||
* @returns Formatted display URL
|
||||
*/
|
||||
const getDisplayUrl = (): string => {
|
||||
const cleanUrl = apiUrl.replace('https://', '').replace('http://', '').replace(':443', '').replace('/api', '');
|
||||
return cleanUrl === 'app.aliasvault.net' ? 'aliasvault.net' : cleanUrl;
|
||||
};
|
||||
|
||||
return {
|
||||
apiUrl,
|
||||
setApiUrl,
|
||||
loadApiUrl,
|
||||
getDisplayUrl,
|
||||
};
|
||||
};
|
||||
@@ -8,7 +8,7 @@ export class AppInfo {
|
||||
/**
|
||||
* The current extension version. This should be updated with each release of the extension.
|
||||
*/
|
||||
public static readonly VERSION = '0.19.0';
|
||||
public static readonly VERSION = '0.20.0';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault server (API) version. If the server version is below this, the
|
||||
@@ -16,11 +16,6 @@ export class AppInfo {
|
||||
*/
|
||||
public static readonly MIN_SERVER_VERSION = '0.12.0-dev';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault client vault version.
|
||||
*/
|
||||
public static readonly MIN_VAULT_VERSION = '1.4.1';
|
||||
|
||||
/**
|
||||
* The client name to use in the X-AliasVault-Client header.
|
||||
* Detects the specific browser being used.
|
||||
@@ -54,15 +49,6 @@ export class AppInfo {
|
||||
*/
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Checks if a given vault version is supported
|
||||
* @param vaultVersion The version to check
|
||||
* @returns boolean indicating if the version is supported
|
||||
*/
|
||||
public static isVaultVersionSupported(vaultVersion: string): boolean {
|
||||
return this.versionGreaterThanOrEqualTo(vaultVersion, this.MIN_VAULT_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given server version is supported
|
||||
* @param serverVersion The version to check
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user