Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c7b44c04a | ||
|
|
b41449f892 | ||
|
|
934d0d9e56 | ||
|
|
99d0da1119 | ||
|
|
c74e05d400 | ||
|
|
844bdab92f | ||
|
|
1345e3c657 | ||
|
|
4fdf7ce92c | ||
|
|
852d9b5e98 | ||
|
|
3c72fa3fde | ||
|
|
b61b747e4b | ||
|
|
1b4389c7d7 | ||
|
|
499d2759ce | ||
|
|
d0140a8ddb | ||
|
|
76dc465032 | ||
|
|
84420104ee | ||
|
|
1109bde521 | ||
|
|
134a173148 | ||
|
|
83be492b3a | ||
|
|
fac72e5a11 | ||
|
|
5eb885da20 | ||
|
|
da4f286757 | ||
|
|
f6db447ad4 | ||
|
|
b472ba749c | ||
|
|
ef68b3b265 | ||
|
|
08d4a8b656 | ||
|
|
93ac131508 | ||
|
|
a7d1536140 | ||
|
|
4fa3fedea2 | ||
|
|
038e8babb1 | ||
|
|
0845477041 | ||
|
|
90156dd1f8 | ||
|
|
fe4b11cf4d | ||
|
|
2cbf234d05 | ||
|
|
a53575b4bf | ||
|
|
697abc6828 | ||
|
|
e96cfa3940 | ||
|
|
61a88e6715 | ||
|
|
e07a35b214 | ||
|
|
4a79fafbb9 | ||
|
|
02b9bff64e | ||
|
|
55e02478b4 | ||
|
|
a576908ae2 | ||
|
|
95510f793b | ||
|
|
20a4a82b1b | ||
|
|
61ba6e1a3c | ||
|
|
f28f1f07b8 | ||
|
|
7f186f1345 | ||
|
|
129b50afba | ||
|
|
bad0f485a9 | ||
|
|
5d9ae7d189 | ||
|
|
ef8ab63b66 | ||
|
|
469466995c | ||
|
|
62c5edc7dc | ||
|
|
ba625a30ea | ||
|
|
bcdcbef912 | ||
|
|
a64ed4817a | ||
|
|
919a33defb | ||
|
|
7e08f64175 | ||
|
|
e525bd1c2d | ||
|
|
7298f8914d | ||
|
|
c476c53101 | ||
|
|
b6c7e88000 | ||
|
|
26624e165a | ||
|
|
c079b830b5 | ||
|
|
165a89e946 | ||
|
|
5042e1b696 | ||
|
|
472a79a12b | ||
|
|
97730cd721 | ||
|
|
d5400faf95 | ||
|
|
9b8da64858 | ||
|
|
9ce776be2b | ||
|
|
d674c77216 | ||
|
|
e41c4b3213 | ||
|
|
f88670787f | ||
|
|
261be3ab34 | ||
|
|
0bace49e95 | ||
|
|
bb82952c74 | ||
|
|
fd5244a686 | ||
|
|
09bc4286d9 | ||
|
|
4c45047d23 | ||
|
|
5251ea53ca | ||
|
|
2da9955213 | ||
|
|
fab12daacf | ||
|
|
9ba467479a | ||
|
|
8e698a21fa | ||
|
|
28a0c7eb1f | ||
|
|
fcbe8da1e6 | ||
|
|
a0a3a2e14a | ||
|
|
4fff14480b | ||
|
|
c7ad42a63e | ||
|
|
6df3c03682 | ||
|
|
7da5557b98 | ||
|
|
38399e00cb | ||
|
|
b30338de37 | ||
|
|
ceaa7731fe | ||
|
|
b66c41e4c9 | ||
|
|
9e478c94f9 | ||
|
|
b415043b4e | ||
|
|
10f6525e94 | ||
|
|
5fb12f26fe | ||
|
|
6047c8f80d | ||
|
|
1b6e220c5a | ||
|
|
b2093b5892 | ||
|
|
b81eabc583 | ||
|
|
0c4be1398d | ||
|
|
4aa0e5f8a1 | ||
|
|
63c737b6cc | ||
|
|
44c2331b42 | ||
|
|
8f9058e1b8 | ||
|
|
613fb7db12 | ||
|
|
c4738637f1 | ||
|
|
151cb19de8 | ||
|
|
b0c53ca7b4 | ||
|
|
586285c5e8 | ||
|
|
5ca8fb92c8 |
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for AliasVault
|
||||
title: '[Feature Request] '
|
||||
labels: enhancement
|
||||
labels: '⚡️ enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,21 +1,15 @@
|
||||
## Description
|
||||
|
||||
Please include a summary of the changes and the related issue(s).
|
||||
- [ ] Bug fix
|
||||
- [ ] Feature enhancement
|
||||
- [ ] Documentation update
|
||||
- [ ] Other (please describe):
|
||||
|
||||
## Related Issues
|
||||
|
||||
Link to any issues that this PR addresses:
|
||||
Fixes #[issue-number]
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Code adheres to project standards and guidelines.
|
||||
- [ ] Documentation has been updated where applicable.
|
||||
|
||||
## Additional Information
|
||||
|
||||
Add any additional context, screenshots, or explanations here.
|
||||
Add any additional context, screenshots, or explanations here.
|
||||
|
||||
239
.github/workflows/browser-extension-build.yml
vendored
Normal file
@@ -0,0 +1,239 @@
|
||||
name: Browser Extension Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-chrome-extension:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: browser-extension
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get short SHA
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: browser-extension/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build extension
|
||||
run: npm run build:chrome
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
|
||||
- name: Zip Chrome Extension
|
||||
run: npm run zip:chrome
|
||||
|
||||
- name: Unzip for artifact
|
||||
run: |
|
||||
mkdir -p dist/chrome-unpacked
|
||||
unzip dist/aliasvault-browser-extension-*-chrome.zip -d dist/chrome-unpacked
|
||||
|
||||
- name: Upload dist artifact Chrome
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', steps.vars.outputs.sha_short) || steps.vars.outputs.sha_short) }}-chrome
|
||||
path: browser-extension/dist/chrome-unpacked
|
||||
|
||||
outputs:
|
||||
sha_short: ${{ steps.vars.outputs.sha_short }}
|
||||
|
||||
build-firefox-extension:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: browser-extension
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get short SHA
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: browser-extension/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build extension
|
||||
run: npm run build:firefox
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
|
||||
- name: Zip Firefox Extension
|
||||
run: npm run zip:firefox
|
||||
|
||||
- name: Unzip for artifact
|
||||
run: |
|
||||
mkdir -p dist/firefox-unpacked
|
||||
unzip dist/aliasvault-browser-extension-*-firefox.zip -d dist/firefox-unpacked
|
||||
mkdir -p dist/sources-unpacked
|
||||
unzip dist/aliasvault-browser-extension-*-sources.zip -d dist/sources-unpacked
|
||||
|
||||
- name: Upload dist artifact Firefox
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', steps.vars.outputs.sha_short) || steps.vars.outputs.sha_short) }}-firefox
|
||||
path: browser-extension/dist/firefox-unpacked
|
||||
|
||||
- name: Upload dist artifact Firefox sources
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', steps.vars.outputs.sha_short) || steps.vars.outputs.sha_short) }}-sources
|
||||
path: browser-extension/dist/sources-unpacked
|
||||
|
||||
outputs:
|
||||
sha_short: ${{ steps.vars.outputs.sha_short }}
|
||||
|
||||
build-edge-extension:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: browser-extension
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get short SHA
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: browser-extension/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build extension
|
||||
run: npm run build:edge
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
|
||||
- name: Zip Edge Extension
|
||||
run: npm run zip:edge
|
||||
|
||||
- name: Unzip for artifact
|
||||
run: |
|
||||
mkdir -p dist/edge-unpacked
|
||||
unzip dist/aliasvault-browser-extension-*-edge.zip -d dist/edge-unpacked
|
||||
|
||||
- name: Upload dist artifact Edge
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', steps.vars.outputs.sha_short) || steps.vars.outputs.sha_short) }}-edge
|
||||
path: browser-extension/dist/edge-unpacked
|
||||
|
||||
outputs:
|
||||
sha_short: ${{ steps.vars.outputs.sha_short }}
|
||||
|
||||
upload-chrome-release-assets:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-chrome-extension]
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
steps:
|
||||
- name: Download built artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', needs.build-chrome-extension.outputs.sha_short) || needs.build-chrome-extension.outputs.sha_short) }}-chrome
|
||||
path: browser-extension/dist/chrome-unpacked
|
||||
|
||||
- name: Zip Chrome Extension for release
|
||||
run: |
|
||||
cd browser-extension/dist
|
||||
zip -r aliasvault-browser-extension-${{ github.ref_name }}-chrome.zip chrome-unpacked/*
|
||||
|
||||
- name: Upload Chrome Extension ZIP to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: browser-extension/dist/aliasvault-browser-extension-*-chrome.zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
upload-firefox-release-assets:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-firefox-extension]
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
steps:
|
||||
- name: Download built artifact Firefox
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', needs.build-firefox-extension.outputs.sha_short) || needs.build-firefox-extension.outputs.sha_short) }}-firefox
|
||||
path: browser-extension/dist/firefox-unpacked
|
||||
|
||||
- name: Download built artifact Firefox sources
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', needs.build-firefox-extension.outputs.sha_short) || needs.build-firefox-extension.outputs.sha_short) }}-sources
|
||||
path: browser-extension/dist/sources-unpacked
|
||||
|
||||
- name: Zip Firefox Extensions for release
|
||||
run: |
|
||||
cd browser-extension/dist
|
||||
zip -r aliasvault-browser-extension-${{ github.ref_name }}-firefox.zip firefox-unpacked/*
|
||||
zip -r aliasvault-browser-extension-${{ github.ref_name }}-sources.zip sources-unpacked/*
|
||||
|
||||
- name: Upload Firefox Extension ZIP to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: browser-extension/dist/aliasvault-browser-extension-*{-firefox,-sources}.zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
upload-edge-release-assets:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-edge-extension]
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
steps:
|
||||
- name: Download built artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', needs.build-edge-extension.outputs.sha_short) || needs.build-edge-extension.outputs.sha_short) }}-edge
|
||||
path: browser-extension/dist/edge-unpacked
|
||||
|
||||
- name: Zip Edge Extension for release
|
||||
run: |
|
||||
cd browser-extension/dist
|
||||
zip -r aliasvault-browser-extension-${{ github.ref_name }}-edge.zip edge-unpacked/*
|
||||
|
||||
- name: Upload Edge Extension ZIP to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: browser-extension/dist/aliasvault-browser-extension-*-edge.zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
70
.github/workflows/browser-extension-tests.yml
vendored
@@ -1,70 +0,0 @@
|
||||
name: Browser Extension Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
chrome-extension:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: browser-extensions/chrome
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: browser-extensions/chrome/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build extension
|
||||
run: npm run build
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
|
||||
- name: Upload dist artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-chrome-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', github.sha) || github.sha) }}
|
||||
path: browser-extensions/chrome/dist/
|
||||
|
||||
upload-release-assets:
|
||||
runs-on: ubuntu-latest
|
||||
needs: chrome-extension
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download built artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: aliasvault-chrome-extension-${{ github.ref_name }}
|
||||
path: browser-extensions/chrome/dist/
|
||||
|
||||
- name: Zip Chrome Extension
|
||||
run: |
|
||||
cd browser-extensions/chrome/dist
|
||||
zip -r ../../../aliasvault-chrome-extension-${{ github.ref_name }}.zip .
|
||||
|
||||
- name: Upload Chrome Extension ZIP to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: aliasvault-chrome-extension-${{ github.ref_name }}.zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
74
README.md
@@ -1,41 +1,51 @@
|
||||
<div align="center">
|
||||
|
||||
🌟 **If you find this project useful, please consider giving it a star!** 🌟
|
||||
|
||||
<h1><img src="https://github.com/user-attachments/assets/933c8b45-a190-4df6-913e-b7c64ad9938b" width="40" /> AliasVault</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://app.aliasvault.net">Try 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-host">Self-host instructions ⚙️</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<strong>Open-source password and (email) alias manager</strong>
|
||||
</p>
|
||||
# AliasVault: password & (email) alias manager [<img src="https://github.com/user-attachments/assets/933c8b45-a190-4df6-913e-b7c64ad9938b" width="100" align="right" alt="AliasVault">](https://github.com/lanedirt/AliasVault)
|
||||
|
||||
[<img src="https://img.shields.io/github/v/release/lanedirt/AliasVault?include_prereleases&logo=github">](https://github.com/lanedirt/AliasVault/releases)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/docker-compose-build.yml?label=docker-compose%20build">](https://github.com/lanedirt/AliasVault/actions/workflows/docker-compose-build.yml)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-unit-tests.yml?label=unit tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-build-run-tests.yml)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-integration-tests.yml?label=integration tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-build-run-tests.yml)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-e2e-client-tests.yml?label=e2e tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-e2e-client-tests.yml)
|
||||
[<img src="https://img.shields.io/sonar/coverage/lanedirt_AliasVault?server=https%3A%2F%2Fsonarcloud.io&label=test code coverage">](https://sonarcloud.io/summary/new_code?id=lanedirt_AliasVault)
|
||||
[<img src="https://img.shields.io/sonar/quality_gate/lanedirt_AliasVault?server=https%3A%2F%2Fsonarcloud.io&label=sonarcloud&logo=sonarcloud">](https://sonarcloud.io/summary/new_code?id=lanedirt_AliasVault)
|
||||
</div>
|
||||
[<img alt="Discord" src="https://img.shields.io/discord/1309300619026235422?logo=discord&logoColor=%237289da&label=discord&color=%237289da">](https://discord.gg/DsaXMTEtpF)
|
||||
|
||||
<div align="center">
|
||||
> AliasVault is an end-to-end encrypted password and (email) alias manager that protects your privacy by creating alternative identities, passwords and email addresses for every website you use. Use the official supported cloud version or self-host AliasVault on your own server with Docker.
|
||||
|
||||
[<img alt="Discord" src="https://img.shields.io/discord/1309300619026235422?logo=discord&logoColor=%237289da&label=join%20discord%20chat&color=%237289da">](https://discord.gg/DsaXMTEtpF)
|
||||
|
||||
</div>
|
||||
|
||||
AliasVault is an end-to-end encrypted password and (email) alias manager that protects your privacy by creating alternative identities, passwords and email addresses for every website you use. AliasVault can be self-hosted on your own server with Docker.
|
||||
## Quick links
|
||||
- <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> - <a href="https://aliasvault.net/plugins?utm_source=gh-readme">Browser Extensions 🔌</a>
|
||||
|
||||
### What makes AliasVault unique:
|
||||
- **Zero-knowledge architecture**: All data is end-to-end encrypted on the client and stored in encrypted state on the server. Your master password never leaves your device and the server never has access to your data.
|
||||
- **Built-in email server**: AliasVault includes its own email server that allows you to generate virtual email addresses for each alias. Emails sent to these addresses are instantly visible in the AliasVault app.
|
||||
- **Built-in email server**: AliasVault includes its own email server that allows you to generate real working email addresses for each alias. Emails sent to these addresses are instantly visible in the AliasVault app and browser extension.
|
||||
- **Alias generation**: Generate aliases and assign them to a website, allowing you to use different email addresses and usernames for each website. Keeping your online identities separate and secure, making it harder for bad actors to link your accounts.
|
||||
- **Open-source**: The source code is available on GitHub and can be self-hosted on your own server.
|
||||
- **Open-source**: The source code is available on GitHub and AliasVault can be self-hosted on your own server via an easy install script.
|
||||
|
||||
> Note: AliasVault is currently in active development and some features may not yet have been (fully) implemented. If you run into any issues, please create an issue on GitHub.
|
||||
## Screenshots
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th align="center">Browser Extension</th>
|
||||
<th align="center">Generate email and aliases</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://github.com/user-attachments/assets/d9ffd3dc-08a0-462d-8148-e8da5ec5a520" alt="Browser Autofill" />
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://github.com/user-attachments/assets/86752994-d469-4b0e-b633-c089e0aed12b" alt="Generate Aliases" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th align="center">Strong security</th>
|
||||
<th align="center">Easy self-host</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://github.com/user-attachments/assets/26b66379-10a5-4b8b-9c69-e64b553a10be" alt="Strong security" />
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://github.com/user-attachments/assets/47c7002a-e326-4507-8801-194e134e00dd" alt="Easy self-host installation" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Official Cloud Version
|
||||
The official cloud version of AliasVault is freely available at [app.aliasvault.net](https://app.aliasvault.net). This fully supported platform is always up to date with our latest release. Create an account to protect your privacy today.
|
||||
@@ -57,7 +67,7 @@ This method uses pre-built Docker images and works on minimal hardware specifica
|
||||
|
||||
```bash
|
||||
# Download install script from latest stable release
|
||||
curl -o install.sh https://raw.githubusercontent.com/lanedirt/AliasVault/0.12.3/install.sh
|
||||
curl -o install.sh https://raw.githubusercontent.com/lanedirt/AliasVault/0.14.0/install.sh
|
||||
|
||||
# Make install script executable and run it. This will create the .env file, pull the Docker images, and start the AliasVault containers.
|
||||
chmod +x install.sh
|
||||
@@ -95,10 +105,11 @@ AliasVault is under active development with new features being added regularly.
|
||||
- [x] Built-in email server for aliases
|
||||
- [x] Single-command Docker-based installation
|
||||
- [x] Chrome browser extension
|
||||
- [ ] Firefox browser extension (https://github.com/lanedirt/AliasVault/issues/581)
|
||||
- [ ] Add and associate TOTP MFA tokens to credentials (https://github.com/lanedirt/AliasVault/issues/181)
|
||||
- [ ] Add support for connecting custom user domains to cloud hosted version (https://github.com/lanedirt/AliasVault/issues/485)
|
||||
- [x] Firefox and MS Edge browser extension
|
||||
- [x] Safari and Brave browser extension
|
||||
- [x] Add and associate TOTP MFA tokens to credentials
|
||||
- [ ] Import passwords from existing password managers (https://github.com/lanedirt/AliasVault/issues/542)
|
||||
- [ ] Add support for connecting custom user domains to cloud hosted version (https://github.com/lanedirt/AliasVault/issues/485)
|
||||
|
||||
### Future Plans
|
||||
- [ ] Mobile apps (iOS, Android)
|
||||
@@ -107,6 +118,11 @@ AliasVault is under active development with new features being added regularly.
|
||||
|
||||
Want to suggest a feature? Join our [Discord](https://discord.gg/DsaXMTEtpF) or create an issue on GitHub.
|
||||
|
||||
### 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>
|
||||
|
||||
## Tech Stack & Security
|
||||
|
||||
AliasVault is built with a modern, secure, and scalable technology stack, ensuring robust encryption and privacy protection.
|
||||
|
||||
33
browser-extension/.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.output
|
||||
dist
|
||||
stats.html
|
||||
stats-*.json
|
||||
.wxt
|
||||
web-ext.config.ts
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Dictionaries
|
||||
# During build these are copied from the ../dictionaries folder because firefox zip requires all files to be in the root of the zip.
|
||||
# Therefore this copied folder is not committed to the repo the original folder is already available outside this directory.
|
||||
# See vite-plugins/identity-gen-dict-loader.ts for more details.
|
||||
dictionaries
|
||||
25
browser-extension/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
This folder contains the source code for the browser extensions for AliasVault.
|
||||
|
||||
The browser extension is built using WXT and React:
|
||||
- [WXT](https://wxt.dev/) is a build tool for browser extensions.
|
||||
- [React](https://reactjs.org/) is a JavaScript library for building user interfaces.
|
||||
|
||||
To build the browser extension, run the following command in this directory:
|
||||
|
||||
### Build the browser extension
|
||||
```bash
|
||||
npm install
|
||||
|
||||
# Build the Chrome extension (saves in dist/chrome-mv3)
|
||||
npm run zip:chrome
|
||||
|
||||
# Build the Firefox extension (creates two zip files in dist)
|
||||
npm run zip:firefox
|
||||
|
||||
# Build the Edge extension (saves in dist/edge-mv3)
|
||||
npm run zip:edge
|
||||
|
||||
# Build the Safari extension (saves in dist/safari-mv2 which is referenced by the dist/safari-xcode/AliasVault.xcodeproj project)
|
||||
npm run build:safari
|
||||
# Open the dist/safari-xcode/AliasVault.xcodeproj project in MacOS Xcode and run the project. This will install the extension to your Safari browser locally.
|
||||
```
|
||||
@@ -119,9 +119,9 @@ export default [
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
NodeJS: true,
|
||||
...globals.node,
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
NodeJS: true,
|
||||
chrome: 'readonly',
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,64 @@
|
||||
{
|
||||
"name": "chrome",
|
||||
"version": "1.0.0",
|
||||
"main": "index.tsx",
|
||||
"name": "aliasvault-browser-extension",
|
||||
"description": "AliasVault Browser Extension",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:chrome": "wxt -b chrome",
|
||||
"dev:firefox": "wxt -b firefox",
|
||||
"dev:edge": "wxt -b edge",
|
||||
"build:chrome": "wxt build -b chrome",
|
||||
"build:firefox": "wxt build -b firefox",
|
||||
"build:edge": "wxt build -b edge",
|
||||
"build:safari": "wxt build -b safari",
|
||||
"test": "vitest",
|
||||
"dev": "vite dev",
|
||||
"preview": "vite preview",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"lint": "eslint src",
|
||||
"lint:custom": "eslint",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"build": "vite build"
|
||||
"zip": "wxt zip",
|
||||
"zip:chrome": "wxt zip -b chrome",
|
||||
"zip:firefox": "wxt zip -b firefox",
|
||||
"zip:edge": "wxt zip -b edge",
|
||||
"compile": "tsc --noEmit",
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@types/node": "^22.13.10",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"buffer": "^6.0.3",
|
||||
"eslint-plugin-jsdoc": "^50.6.3",
|
||||
"globals": "^15.14.0",
|
||||
"jsdoc": "^4.0.4",
|
||||
"globals": "^16.0.0",
|
||||
"otpauth": "^9.3.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.4",
|
||||
"secure-remote-password": "github:LinusU/secure-remote-password#73e5f732b6ca0cdbdc19da1a0c5f48cdbad2cbf0",
|
||||
"sql.js": "^1.12.0",
|
||||
"vitest": "^3.0.4"
|
||||
"vitest": "^3.0.8",
|
||||
"webext-bridge": "^6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/react": "^19.0.7",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/sql.js": "^1.4.9",
|
||||
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
||||
"@typescript-eslint/parser": "^8.21.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/ui": "^3.0.4",
|
||||
"@vitest/coverage-v8": "^3.0.8",
|
||||
"@wxt-dev/module-react": "^1.1.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsdoc": "^50.6.3",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.11",
|
||||
"typescript": "^5.6.3",
|
||||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"vite-plugin-web-extension": "^4.4.3"
|
||||
"wxt": "^0.19.13"
|
||||
}
|
||||
}
|
||||
6
browser-extension/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
62
browser-extension/safari-xcode/.gitignore
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
||||
## App packaging
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
# Package.pins
|
||||
# Package.resolved
|
||||
# *.xcodeproj
|
||||
#
|
||||
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
|
||||
# hence it is not needed unless you have added a package configuration file to your project
|
||||
# .swiftpm
|
||||
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# Pods/
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||
# *.xcworkspace
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build/
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo.
|
||||
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots/**/*.png
|
||||
fastlane/test_output
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.Safari.web-extension</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SafariWebExtensionHandler</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// SafariWebExtensionHandler.swift
|
||||
// AliasVault Extension
|
||||
//
|
||||
// Created by Leendert de Borst on 12/03/2025.
|
||||
//
|
||||
|
||||
import SafariServices
|
||||
import os.log
|
||||
|
||||
class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
|
||||
|
||||
func beginRequest(with context: NSExtensionContext) {
|
||||
let request = context.inputItems.first as? NSExtensionItem
|
||||
|
||||
let profile: UUID?
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
profile = request?.userInfo?[SFExtensionProfileKey] as? UUID
|
||||
} else {
|
||||
profile = request?.userInfo?["profile"] as? UUID
|
||||
}
|
||||
|
||||
let message: Any?
|
||||
if #available(iOS 15.0, macOS 11.0, *) {
|
||||
message = request?.userInfo?[SFExtensionMessageKey]
|
||||
} else {
|
||||
message = request?.userInfo?["message"]
|
||||
}
|
||||
|
||||
os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@ (profile: %@)", String(describing: message), profile?.uuidString ?? "none")
|
||||
|
||||
let response = NSExtensionItem()
|
||||
if #available(iOS 15.0, macOS 11.0, *) {
|
||||
response.userInfo = [ SFExtensionMessageKey: [ "echo": message ] ]
|
||||
} else {
|
||||
response.userInfo = [ "message": [ "echo": message ] ]
|
||||
}
|
||||
|
||||
context.completeRequest(returningItems: [ response ], completionHandler: nil)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 56;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
CE0CAFA72D81A9F7006174AB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CAFA62D81A9F7006174AB /* AppDelegate.swift */; };
|
||||
CE0CAFAB2D81A9F7006174AB /* Base in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFAA2D81A9F7006174AB /* Base */; };
|
||||
CE0CAFAD2D81A9F7006174AB /* Icon.png in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFAC2D81A9F7006174AB /* Icon.png */; };
|
||||
CE0CAFAF2D81A9F7006174AB /* Style.css in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFAE2D81A9F7006174AB /* Style.css */; };
|
||||
CE0CAFB12D81A9F7006174AB /* Script.js in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFB02D81A9F7006174AB /* Script.js */; };
|
||||
CE0CAFB32D81A9F7006174AB /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CAFB22D81A9F7006174AB /* ViewController.swift */; };
|
||||
CE0CAFB62D81A9F7006174AB /* Base in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFB52D81A9F7006174AB /* Base */; };
|
||||
CE0CAFB82D81A9F8006174AB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFB72D81A9F8006174AB /* Assets.xcassets */; };
|
||||
CE0CAFC12D81A9F8006174AB /* AliasVault Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = CE0CAFC02D81A9F8006174AB /* AliasVault Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
CE0CAFC62D81A9F8006174AB /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CAFC52D81A9F8006174AB /* SafariWebExtensionHandler.swift */; };
|
||||
CE0CAFDB2D81A9F8006174AB /* background.js in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD32D81A9F8006174AB /* background.js */; };
|
||||
CE0CAFDC2D81A9F8006174AB /* popup.html in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD42D81A9F8006174AB /* popup.html */; };
|
||||
CE0CAFDD2D81A9F8006174AB /* chunks in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD52D81A9F8006174AB /* chunks */; };
|
||||
CE0CAFDE2D81A9F8006174AB /* content-scripts in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD62D81A9F8006174AB /* content-scripts */; };
|
||||
CE0CAFDF2D81A9F8006174AB /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD72D81A9F8006174AB /* manifest.json */; };
|
||||
CE0CAFE02D81A9F8006174AB /* icon in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD82D81A9F8006174AB /* icon */; };
|
||||
CE0CAFE12D81A9F8006174AB /* assets in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD92D81A9F8006174AB /* assets */; };
|
||||
CE0CAFE22D81A9F8006174AB /* src in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFDA2D81A9F8006174AB /* src */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
CE0CAFC22D81A9F8006174AB /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = CE0CAF9B2D81A9F7006174AB /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = CE0CAFBF2D81A9F8006174AB;
|
||||
remoteInfo = "AliasVault Extension";
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
CE0CAFCE2D81A9F8006174AB /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
CE0CAFC12D81A9F8006174AB /* AliasVault Extension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
CE0CAFA32D81A9F7006174AB /* AliasVault.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AliasVault.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CE0CAFA62D81A9F7006174AB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
CE0CAFAA2D81A9F7006174AB /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = Base; path = ../Base.lproj/Main.html; sourceTree = "<group>"; };
|
||||
CE0CAFAC2D81A9F7006174AB /* Icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Icon.png; sourceTree = "<group>"; };
|
||||
CE0CAFAE2D81A9F7006174AB /* Style.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = Style.css; sourceTree = "<group>"; };
|
||||
CE0CAFB02D81A9F7006174AB /* Script.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = Script.js; sourceTree = "<group>"; };
|
||||
CE0CAFB22D81A9F7006174AB /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
CE0CAFB52D81A9F7006174AB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
CE0CAFB72D81A9F8006174AB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
CE0CAFB92D81A9F8006174AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
CE0CAFBA2D81A9F8006174AB /* AliasVault.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AliasVault.entitlements; sourceTree = "<group>"; };
|
||||
CE0CAFBB2D81A9F8006174AB /* AliasVault.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AliasVault.entitlements; sourceTree = "<group>"; };
|
||||
CE0CAFC02D81A9F8006174AB /* AliasVault Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "AliasVault Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CE0CAFC52D81A9F8006174AB /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = "<group>"; };
|
||||
CE0CAFC72D81A9F8006174AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
CE0CAFC82D81A9F8006174AB /* AliasVault_Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AliasVault_Extension.entitlements; sourceTree = "<group>"; };
|
||||
CE0CAFD32D81A9F8006174AB /* background.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = background.js; path = "../../../dist/safari-mv2/background.js"; sourceTree = "<group>"; };
|
||||
CE0CAFD42D81A9F8006174AB /* popup.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = popup.html; path = "../../../dist/safari-mv2/popup.html"; sourceTree = "<group>"; };
|
||||
CE0CAFD52D81A9F8006174AB /* chunks */ = {isa = PBXFileReference; lastKnownFileType = folder; name = chunks; path = "../../../dist/safari-mv2/chunks"; sourceTree = "<group>"; };
|
||||
CE0CAFD62D81A9F8006174AB /* content-scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "content-scripts"; path = "../../../dist/safari-mv2/content-scripts"; sourceTree = "<group>"; };
|
||||
CE0CAFD72D81A9F8006174AB /* manifest.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = manifest.json; path = "../../../dist/safari-mv2/manifest.json"; sourceTree = "<group>"; };
|
||||
CE0CAFD82D81A9F8006174AB /* icon */ = {isa = PBXFileReference; lastKnownFileType = folder; name = icon; path = "../../../dist/safari-mv2/icon"; sourceTree = "<group>"; };
|
||||
CE0CAFD92D81A9F8006174AB /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = "../../../dist/safari-mv2/assets"; sourceTree = "<group>"; };
|
||||
CE0CAFDA2D81A9F8006174AB /* src */ = {isa = PBXFileReference; lastKnownFileType = folder; name = src; path = "../../../dist/safari-mv2/src"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
CE0CAFA02D81A9F7006174AB /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CE0CAFBD2D81A9F8006174AB /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
CE0CAF9A2D81A9F7006174AB = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFA52D81A9F7006174AB /* AliasVault */,
|
||||
CE0CAFC42D81A9F8006174AB /* AliasVault Extension */,
|
||||
CE0CAFA42D81A9F7006174AB /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFA42D81A9F7006174AB /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFA32D81A9F7006174AB /* AliasVault.app */,
|
||||
CE0CAFC02D81A9F8006174AB /* AliasVault Extension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFA52D81A9F7006174AB /* AliasVault */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFA62D81A9F7006174AB /* AppDelegate.swift */,
|
||||
CE0CAFB22D81A9F7006174AB /* ViewController.swift */,
|
||||
CE0CAFB42D81A9F7006174AB /* Main.storyboard */,
|
||||
CE0CAFB72D81A9F8006174AB /* Assets.xcassets */,
|
||||
CE0CAFB92D81A9F8006174AB /* Info.plist */,
|
||||
CE0CAFBA2D81A9F8006174AB /* AliasVault.entitlements */,
|
||||
CE0CAFBB2D81A9F8006174AB /* AliasVault.entitlements */,
|
||||
CE0CAFA82D81A9F7006174AB /* Resources */,
|
||||
);
|
||||
path = AliasVault;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFA82D81A9F7006174AB /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFA92D81A9F7006174AB /* Main.html */,
|
||||
CE0CAFAC2D81A9F7006174AB /* Icon.png */,
|
||||
CE0CAFAE2D81A9F7006174AB /* Style.css */,
|
||||
CE0CAFB02D81A9F7006174AB /* Script.js */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFC42D81A9F8006174AB /* AliasVault Extension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFD22D81A9F8006174AB /* Resources */,
|
||||
CE0CAFC52D81A9F8006174AB /* SafariWebExtensionHandler.swift */,
|
||||
CE0CAFC72D81A9F8006174AB /* Info.plist */,
|
||||
CE0CAFC82D81A9F8006174AB /* AliasVault_Extension.entitlements */,
|
||||
);
|
||||
path = "AliasVault Extension";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFD22D81A9F8006174AB /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFD32D81A9F8006174AB /* background.js */,
|
||||
CE0CAFD42D81A9F8006174AB /* popup.html */,
|
||||
CE0CAFD52D81A9F8006174AB /* chunks */,
|
||||
CE0CAFD62D81A9F8006174AB /* content-scripts */,
|
||||
CE0CAFD72D81A9F8006174AB /* manifest.json */,
|
||||
CE0CAFD82D81A9F8006174AB /* icon */,
|
||||
CE0CAFD92D81A9F8006174AB /* assets */,
|
||||
CE0CAFDA2D81A9F8006174AB /* src */,
|
||||
);
|
||||
name = Resources;
|
||||
path = "AliasVault Extension";
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
CE0CAFA22D81A9F7006174AB /* AliasVault */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = CE0CAFCF2D81A9F8006174AB /* Build configuration list for PBXNativeTarget "AliasVault" */;
|
||||
buildPhases = (
|
||||
CE0CAF9F2D81A9F7006174AB /* Sources */,
|
||||
CE0CAFA02D81A9F7006174AB /* Frameworks */,
|
||||
CE0CAFA12D81A9F7006174AB /* Resources */,
|
||||
CE0CAFCE2D81A9F8006174AB /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
CE0CAFC32D81A9F8006174AB /* PBXTargetDependency */,
|
||||
);
|
||||
name = AliasVault;
|
||||
productName = AliasVault;
|
||||
productReference = CE0CAFA32D81A9F7006174AB /* AliasVault.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
CE0CAFBF2D81A9F8006174AB /* AliasVault Extension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = CE0CAFCB2D81A9F8006174AB /* Build configuration list for PBXNativeTarget "AliasVault Extension" */;
|
||||
buildPhases = (
|
||||
CE0CAFBC2D81A9F8006174AB /* Sources */,
|
||||
CE0CAFBD2D81A9F8006174AB /* Frameworks */,
|
||||
CE0CAFBE2D81A9F8006174AB /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "AliasVault Extension";
|
||||
productName = "AliasVault Extension";
|
||||
productReference = CE0CAFC02D81A9F8006174AB /* AliasVault Extension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
CE0CAF9B2D81A9F7006174AB /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1540;
|
||||
LastUpgradeCheck = 1540;
|
||||
TargetAttributes = {
|
||||
CE0CAFA22D81A9F7006174AB = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
};
|
||||
CE0CAFBF2D81A9F8006174AB = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = CE0CAF9E2D81A9F7006174AB /* Build configuration list for PBXProject "AliasVault" */;
|
||||
compatibilityVersion = "Xcode 14.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = CE0CAF9A2D81A9F7006174AB;
|
||||
productRefGroup = CE0CAFA42D81A9F7006174AB /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
CE0CAFA22D81A9F7006174AB /* AliasVault */,
|
||||
CE0CAFBF2D81A9F8006174AB /* AliasVault Extension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
CE0CAFA12D81A9F7006174AB /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE0CAFAD2D81A9F7006174AB /* Icon.png in Resources */,
|
||||
CE0CAFB12D81A9F7006174AB /* Script.js in Resources */,
|
||||
CE0CAFAB2D81A9F7006174AB /* Base in Resources */,
|
||||
CE0CAFAF2D81A9F7006174AB /* Style.css in Resources */,
|
||||
CE0CAFB82D81A9F8006174AB /* Assets.xcassets in Resources */,
|
||||
CE0CAFB62D81A9F7006174AB /* Base in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CE0CAFBE2D81A9F8006174AB /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE0CAFDD2D81A9F8006174AB /* chunks in Resources */,
|
||||
CE0CAFE02D81A9F8006174AB /* icon in Resources */,
|
||||
CE0CAFE12D81A9F8006174AB /* assets in Resources */,
|
||||
CE0CAFE22D81A9F8006174AB /* src in Resources */,
|
||||
CE0CAFDB2D81A9F8006174AB /* background.js in Resources */,
|
||||
CE0CAFDF2D81A9F8006174AB /* manifest.json in Resources */,
|
||||
CE0CAFDC2D81A9F8006174AB /* popup.html in Resources */,
|
||||
CE0CAFDE2D81A9F8006174AB /* content-scripts in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
CE0CAF9F2D81A9F7006174AB /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE0CAFB32D81A9F7006174AB /* ViewController.swift in Sources */,
|
||||
CE0CAFA72D81A9F7006174AB /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CE0CAFBC2D81A9F8006174AB /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE0CAFC62D81A9F8006174AB /* SafariWebExtensionHandler.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
CE0CAFC32D81A9F8006174AB /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = CE0CAFBF2D81A9F8006174AB /* AliasVault Extension */;
|
||||
targetProxy = CE0CAFC22D81A9F8006174AB /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
CE0CAFA92D81A9F7006174AB /* Main.html */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
CE0CAFAA2D81A9F7006174AB /* Base */,
|
||||
);
|
||||
name = Main.html;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFB42D81A9F7006174AB /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
CE0CAFB52D81A9F7006174AB /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
CE0CAFC92D81A9F8006174AB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CE0CAFCA2D81A9F8006174AB /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.5;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
CE0CAFCC2D81A9F8006174AB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "AliasVault Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "AliasVault Extension";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.safari.extension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CE0CAFCD2D81A9F8006174AB /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "AliasVault Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "AliasVault Extension";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.safari.Extension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
CE0CAFD02D81A9F8006174AB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = AliasVault/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = AliasVault;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.14.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
"-framework",
|
||||
WebKit,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.safari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CE0CAFD12D81A9F8006174AB /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = AliasVault/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = AliasVault;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.14.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
"-framework",
|
||||
WebKit,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.safari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
CE0CAF9E2D81A9F7006174AB /* Build configuration list for PBXProject "AliasVault" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CE0CAFC92D81A9F8006174AB /* Debug */,
|
||||
CE0CAFCA2D81A9F8006174AB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
CE0CAFCB2D81A9F8006174AB /* Build configuration list for PBXNativeTarget "AliasVault Extension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CE0CAFCC2D81A9F8006174AB /* Debug */,
|
||||
CE0CAFCD2D81A9F8006174AB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
CE0CAFCF2D81A9F8006174AB /* Build configuration list for PBXNativeTarget "AliasVault" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CE0CAFD02D81A9F8006174AB /* Debug */,
|
||||
CE0CAFD12D81A9F8006174AB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = CE0CAF9B2D81A9F7006174AB /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// AliasVault
|
||||
//
|
||||
// Created by Leendert de Borst on 12/03/2025.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
@main
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
// Override point for customization after application launch.
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-16@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-16@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-32@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-32@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-128@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-128@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-256@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-256@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-512@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-512@2x.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 160 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>AliasVault</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
|
||||
<link rel="stylesheet" href="../Style.css">
|
||||
<script src="../Script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<img src="../Icon.png" width="128" height="128" alt="AliasVault Icon">
|
||||
<p class="state-unknown">To enable AliasVault’s browser extension, go to the Safari Extensions preferences.</p>
|
||||
<p class="state-on">AliasVault’s browser extension is currently enabled in Safari. If you wish to turn it off, go to the Safari Extensions preferences.</p>
|
||||
<p class="state-off">AliasVault’s browser extension is currently disabled in Safari. If you wish to turn it on, go to the Safari Extensions preferences.</p>
|
||||
<button class="open-preferences">Open Safari Extensions Preferences…</button>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19085" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19085"/>
|
||||
<plugIn identifier="com.apple.WebKit2IBPlugin" version="19085"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
<scene sceneID="JPo-4y-FX3">
|
||||
<objects>
|
||||
<application id="hnw-xV-0zn" sceneMemberID="viewController">
|
||||
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="AliasVault" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="AliasVault" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About AliasVault" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Hide AliasVault" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Quit AliasVault" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||
<items>
|
||||
<menuItem title="AliasVault Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||
<connections>
|
||||
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModuleProvider="target"/>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="76" y="-134"/>
|
||||
</scene>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="R2V-B0-nI4">
|
||||
<objects>
|
||||
<windowController showSeguePresentationStyle="single" id="B8D-0N-5wS" sceneMemberID="viewController">
|
||||
<window key="window" title="AliasVault" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" animationBehavior="default" id="IQv-IB-iLA">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
|
||||
<windowCollectionBehavior key="collectionBehavior" fullScreenNone="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="425" height="325"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="250"/>
|
||||
</scene>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="hIz-AP-VOD">
|
||||
<objects>
|
||||
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="m2S-Jp-Qdl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<wkWebView wantsLayer="YES" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="eOr-cG-IQY">
|
||||
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<wkWebViewConfiguration key="configuration">
|
||||
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
|
||||
<wkPreferences key="preferences"/>
|
||||
</wkWebViewConfiguration>
|
||||
</wkWebView>
|
||||
</subviews>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="webView" destination="eOr-cG-IQY" id="GFe-mU-dBY"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="655"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SFSafariWebExtensionConverterVersion</key>
|
||||
<string>15.4</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
@@ -0,0 +1,22 @@
|
||||
function show(enabled, useSettingsInsteadOfPreferences) {
|
||||
if (useSettingsInsteadOfPreferences) {
|
||||
document.getElementsByClassName('state-on')[0].innerText = "AliasVault's Safari browser extension is succesfully enabled. If you wish to turn it off, go to the Safari Extensions preferences.";
|
||||
document.getElementsByClassName('state-off')[0].innerText = "AliasVault's Safari browser extension is currently disabled. If you wish to turn it on, go to the Safari Extensions preferences.";
|
||||
document.getElementsByClassName('state-unknown')[0].innerText = "To enable AliasVault's Safari browser extension, go to the Safari Extensions preferences.";
|
||||
document.getElementsByClassName('open-preferences')[0].innerText = "Open Safari Extensions Preferences…";
|
||||
}
|
||||
|
||||
if (typeof enabled === "boolean") {
|
||||
document.body.classList.toggle(`state-on`, enabled);
|
||||
document.body.classList.toggle(`state-off`, !enabled);
|
||||
} else {
|
||||
document.body.classList.remove(`state-on`);
|
||||
document.body.classList.remove(`state-off`);
|
||||
}
|
||||
}
|
||||
|
||||
function openPreferences() {
|
||||
webkit.messageHandlers.controller.postMessage("open-preferences");
|
||||
}
|
||||
|
||||
document.querySelector("button.open-preferences").addEventListener("click", openPreferences);
|
||||
@@ -0,0 +1,44 @@
|
||||
* {
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
|
||||
--spacing: 20px;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing);
|
||||
margin: 0 calc(var(--spacing) * 2);
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
font: -apple-system-short-body;
|
||||
font-family: -apple-system-short-body, system-ui;
|
||||
}
|
||||
|
||||
body:not(.state-on, .state-off) :is(.state-on, .state-off) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.state-on :is(.state-off, .state-unknown) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.state-off :is(.state-on, .state-unknown) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 1em;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// AliasVault
|
||||
//
|
||||
// Created by Leendert de Borst on 12/03/2025.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import SafariServices
|
||||
import WebKit
|
||||
|
||||
let extensionBundleIdentifier = "net.aliasvault.safari.extension"
|
||||
|
||||
class ViewController: NSViewController, WKNavigationDelegate, WKScriptMessageHandler {
|
||||
|
||||
@IBOutlet var webView: WKWebView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.webView.navigationDelegate = self
|
||||
|
||||
self.webView.configuration.userContentController.add(self, name: "controller")
|
||||
|
||||
self.webView.loadFileURL(Bundle.main.url(forResource: "Main", withExtension: "html")!, allowingReadAccessTo: Bundle.main.resourceURL!)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in
|
||||
guard let state = state, error == nil else {
|
||||
// Insert code to inform the user that something went wrong.
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if #available(macOS 13, *) {
|
||||
webView.evaluateJavaScript("show(\(state.isEnabled), true)")
|
||||
} else {
|
||||
webView.evaluateJavaScript("show(\(state.isEnabled), false)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
if (message.body as! String != "open-preferences") {
|
||||
return;
|
||||
}
|
||||
|
||||
SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
// Show manual instructions in case opening the preferences fails due to restricted permissions.
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Safari Extensions Settings"
|
||||
alert.informativeText = """
|
||||
Please follow these steps to enable the extension:
|
||||
1. Open Safari
|
||||
2. Click Safari > Settings in the menu bar
|
||||
3. Go to Extensions
|
||||
4. Find and enable "AliasVault"
|
||||
"""
|
||||
alert.addButton(withTitle: "OK")
|
||||
alert.runModal()
|
||||
}
|
||||
else {
|
||||
// Close app
|
||||
NSApplication.shared.terminate(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
29
browser-extension/safari-xcode/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
This folder contains the Xcode project used to publish the Safari version of the AliasVault browser extension to Apple.
|
||||
|
||||
This project was created using the `safari-web-extension-converter` tool. This XCode project is a simple wrapper around the
|
||||
WXT React browser extension, which is required by Apple in order to package and submit a Safari extension.
|
||||
|
||||
For more information see:
|
||||
- https://developer.apple.com/documentation/safariservices/converting-a-web-extension-for-safari
|
||||
- https://developer.apple.com/documentation/safariservices/running-your-safari-web-extension
|
||||
|
||||
To recreate this project, run the following command in the browser-extension root directory:
|
||||
|
||||
```bash
|
||||
# Build the Safari extension via the normal build process (outputs in dist/safari-mv2)
|
||||
npm run build:safari
|
||||
|
||||
# Convert the safari extension to an Xcode project (requires MacOS/XCode command line interface)
|
||||
xcrun safari-web-extension-converter --bundle-identifier net.aliasvault.safari --macos-only dist/safari-mv2 --project-location safari-xcode --force
|
||||
|
||||
# After the Xcode project is opened, you can run the extension by clicking the "Run" button in the top left corner of the Xcode window.
|
||||
# This will install the extension to your Safari browser and allow you to run it.
|
||||
```
|
||||
|
||||
> Note: This project does not need to be recreated when the extension is updated. It loads all extension files from the dist/safari-mv2 directory that is created by the `build:safari` command. To update the extension and/or publish a new version:
|
||||
> 1. Run `npm run build:safari` to rebuild the Safari extension
|
||||
> 2. Open this Xcode project and rebuild it to get the latest version
|
||||
> 3. Submit the extension to Apple for review via Xcode:
|
||||
> - Select the "Archive" option from the Product menu
|
||||
> - Select the newly created archive and click "Distribute App"
|
||||
> - Select "Distribute" and follow the instructions to submit to App Store Connect
|
||||
1
browser-extension/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
32
browser-extension/src/entrypoints/background.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { browser } from "wxt/browser";
|
||||
import { defineBackground } from 'wxt/sandbox';
|
||||
import { onMessage } from "webext-bridge/background";
|
||||
import { setupContextMenus, handleContextMenuClick } from './background/ContextMenu';
|
||||
import { handleCheckAuthStatus, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDerivedKey, handleGetVault, handleStoreVault, handleSyncVault } from './background/VaultMessageHandler';
|
||||
import { handleOpenPopup, handlePopupWithCredential } from './background/PopupMessageHandler';
|
||||
|
||||
export default defineBackground({
|
||||
/**
|
||||
* This is the main entry point for the background script.
|
||||
*/
|
||||
main() {
|
||||
// Set up context menus
|
||||
setupContextMenus();
|
||||
browser.contextMenus.onClicked.addListener((info: browser.menus.OnClickData, tab?: browser.tabs.Tab) =>
|
||||
handleContextMenuClick(info, tab)
|
||||
);
|
||||
|
||||
// Listen for messages using webext-bridge
|
||||
onMessage('CHECK_AUTH_STATUS', () => handleCheckAuthStatus());
|
||||
onMessage('STORE_VAULT', ({ data }) => handleStoreVault(data));
|
||||
onMessage('SYNC_VAULT', () => handleSyncVault());
|
||||
onMessage('GET_VAULT', () => handleGetVault());
|
||||
onMessage('CLEAR_VAULT', () => handleClearVault());
|
||||
onMessage('GET_CREDENTIALS', () => handleGetCredentials());
|
||||
onMessage('CREATE_IDENTITY', ({ data }) => handleCreateIdentity(data));
|
||||
onMessage('GET_DEFAULT_EMAIL_DOMAIN', () => handleGetDefaultEmailDomain());
|
||||
onMessage('GET_DERIVED_KEY', () => handleGetDerivedKey());
|
||||
onMessage('OPEN_POPUP', () => handleOpenPopup());
|
||||
onMessage('OPEN_POPUP_WITH_CREDENTIAL', ({ data }) => handlePopupWithCredential(data));
|
||||
}
|
||||
});
|
||||
@@ -1,18 +1,20 @@
|
||||
import { PasswordGenerator } from '../shared/generators/Password/PasswordGenerator';
|
||||
import { sendMessage } from 'webext-bridge/background';
|
||||
import { PasswordGenerator } from '../../utils/generators/Password/PasswordGenerator';
|
||||
import { browser } from 'wxt/browser';
|
||||
|
||||
/**
|
||||
* Setup the context menus.
|
||||
*/
|
||||
export function setupContextMenus() : void {
|
||||
// Create root menu
|
||||
chrome.contextMenus.create({
|
||||
browser.contextMenus.create({
|
||||
id: "aliasvault-root",
|
||||
title: "AliasVault",
|
||||
contexts: ["all"]
|
||||
});
|
||||
|
||||
// Add fill option first (only for editable fields)
|
||||
chrome.contextMenus.create({
|
||||
browser.contextMenus.create({
|
||||
id: "aliasvault-activate-form",
|
||||
parentId: "aliasvault-root",
|
||||
title: "Autofill with AliasVault",
|
||||
@@ -20,7 +22,7 @@ export function setupContextMenus() : void {
|
||||
});
|
||||
|
||||
// Add separator (only for editable fields)
|
||||
chrome.contextMenus.create({
|
||||
browser.contextMenus.create({
|
||||
id: "aliasvault-separator",
|
||||
parentId: "aliasvault-root",
|
||||
type: "separator",
|
||||
@@ -28,7 +30,7 @@ export function setupContextMenus() : void {
|
||||
});
|
||||
|
||||
// Add password generator option
|
||||
chrome.contextMenus.create({
|
||||
browser.contextMenus.create({
|
||||
id: "aliasvault-generate-password",
|
||||
parentId: "aliasvault-root",
|
||||
title: "Generate random password (copy to clipboard)",
|
||||
@@ -39,15 +41,15 @@ export function setupContextMenus() : void {
|
||||
/**
|
||||
* Handle context menu clicks.
|
||||
*/
|
||||
export function handleContextMenuClick(info: chrome.contextMenus.OnClickData, tab?: chrome.tabs.Tab) : void {
|
||||
export function handleContextMenuClick(info: browser.contextMenus.OnClickData, tab?: browser.tabs.Tab) : void {
|
||||
if (info.menuItemId === "aliasvault-generate-password") {
|
||||
// Initialize password generator
|
||||
const passwordGenerator = new PasswordGenerator();
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
|
||||
// Use chrome.scripting to write password to clipboard from active tab
|
||||
// Use browser.scripting to write password to clipboard from active tab
|
||||
if (tab?.id) {
|
||||
chrome.scripting.executeScript({
|
||||
browser.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: copyPasswordToClipboard,
|
||||
args: [password]
|
||||
@@ -57,22 +59,14 @@ export function handleContextMenuClick(info: chrome.contextMenus.OnClickData, ta
|
||||
|
||||
if (info.menuItemId === "aliasvault-activate-form" && tab?.id) {
|
||||
// First get the active element's identifier
|
||||
chrome.scripting.executeScript({
|
||||
browser.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: getActiveElementIdentifier,
|
||||
}, (results) => {
|
||||
const elementIdentifier = results[0]?.result;
|
||||
if (elementIdentifier) {
|
||||
// Then send message to content script with proper error handling
|
||||
chrome.tabs.sendMessage(
|
||||
tab.id,
|
||||
{
|
||||
type: 'OPEN_ALIASVAULT_POPUP',
|
||||
elementIdentifier
|
||||
}
|
||||
).catch(error => {
|
||||
console.error('Error sending message to content script:', error);
|
||||
});
|
||||
// Send message to content script with proper tab targeting
|
||||
sendMessage('OPEN_AUTOFILL_POPUP', { elementIdentifier }, `content-script@${tab.id}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { browser } from "wxt/browser";
|
||||
import { BoolResponse } from '../../utils/types/messaging/BoolResponse';
|
||||
/**
|
||||
* Handle opening the popup.
|
||||
*/
|
||||
export function handleOpenPopup() : Promise<BoolResponse> {
|
||||
return (async () : Promise<BoolResponse> => {
|
||||
browser.windows.create({
|
||||
url: browser.runtime.getURL('/popup.html?mode=inline_unlock&expanded=true'),
|
||||
type: 'popup',
|
||||
width: 400,
|
||||
height: 600,
|
||||
focused: true
|
||||
});
|
||||
return { success: true };
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle opening the popup with a credential.
|
||||
*/
|
||||
export function handlePopupWithCredential(message: any) : Promise<BoolResponse> {
|
||||
return (async () : Promise<BoolResponse> => {
|
||||
browser.windows.create({
|
||||
url: browser.runtime.getURL(`/popup.html?expanded=true#/credentials/${message.credentialId}`),
|
||||
type: 'popup',
|
||||
width: 400,
|
||||
height: 600,
|
||||
focused: true
|
||||
});
|
||||
return { success: true };
|
||||
})();
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import EncryptionUtility from '../../utils/EncryptionUtility';
|
||||
import SqliteClient from '../../utils/SqliteClient';
|
||||
import { WebApiService } from '../../utils/WebApiService';
|
||||
import { Vault } from '../../utils/types/webapi/Vault';
|
||||
import { VaultResponse } from '../../utils/types/webapi/VaultResponse';
|
||||
import { VaultPostResponse } from '../../utils/types/webapi/VaultPostResponse';
|
||||
import { storage } from 'wxt/storage';
|
||||
import { BoolResponse as messageBoolResponse } from '../../utils/types/messaging/BoolResponse';
|
||||
import { VaultResponse as messageVaultResponse } from '../../utils/types/messaging/VaultResponse';
|
||||
import { CredentialsResponse as messageCredentialsResponse } from '../../utils/types/messaging/CredentialsResponse';
|
||||
import { DefaultEmailDomainResponse as messageDefaultEmailDomainResponse } from '../../utils/types/messaging/DefaultEmailDomainResponse';
|
||||
|
||||
/**
|
||||
* Check if the user is logged in and if the vault is locked.
|
||||
*/
|
||||
export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, isVaultLocked: boolean }> {
|
||||
const username = await storage.getItem('local:username');
|
||||
const accessToken = await storage.getItem('local:accessToken');
|
||||
const vaultData = await storage.getItem('session:encryptedVault');
|
||||
|
||||
const isLoggedIn = username !== null && accessToken !== null;
|
||||
const isVaultLocked = isLoggedIn && vaultData !== null;
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
isVaultLocked
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the vault in browser storage.
|
||||
*/
|
||||
export async function handleStoreVault(
|
||||
message: any,
|
||||
) : Promise<messageBoolResponse> {
|
||||
try {
|
||||
const vaultResponse = message.vaultResponse as VaultResponse;
|
||||
const encryptedVaultBlob = vaultResponse.vault.blob;
|
||||
|
||||
// Store encrypted vault and derived key in session storage.
|
||||
await storage.setItems([
|
||||
{ key: 'session:encryptedVault', value: encryptedVaultBlob },
|
||||
{ key: 'session:derivedKey', value: message.derivedKey },
|
||||
{ key: 'session:publicEmailDomains', value: vaultResponse.vault.publicEmailDomainList },
|
||||
{ key: 'session:privateEmailDomains', value: vaultResponse.vault.privateEmailDomainList },
|
||||
{ key: 'session:vaultRevisionNumber', value: vaultResponse.vault.currentRevisionNumber }
|
||||
]);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to store vault:', error);
|
||||
return { success: false, error: 'Failed to store vault' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the vault with the server to check if a newer vault is available. If so, the vault will be updated.
|
||||
*/
|
||||
export async function handleSyncVault(
|
||||
) : Promise<messageBoolResponse> {
|
||||
const webApi = new WebApiService(() => {});
|
||||
const statusResponse = await webApi.getStatus();
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
return { success: false, error: statusError };
|
||||
}
|
||||
|
||||
const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number;
|
||||
|
||||
if (statusResponse.vaultRevision > vaultRevisionNumber) {
|
||||
// Retrieve the latest vault from the server.
|
||||
const vaultResponse = await webApi.get<VaultResponse>('Vault');
|
||||
|
||||
await storage.setItems([
|
||||
{ key: 'session:encryptedVault', value: vaultResponse.vault.blob },
|
||||
{ key: 'session:publicEmailDomains', value: vaultResponse.vault.publicEmailDomainList },
|
||||
{ key: 'session:privateEmailDomains', value: vaultResponse.vault.privateEmailDomainList },
|
||||
{ key: 'session:vaultRevisionNumber', value: vaultResponse.vault.currentRevisionNumber }
|
||||
]);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the vault from browser storage.
|
||||
*/
|
||||
export async function handleGetVault(
|
||||
) : Promise<messageVaultResponse> {
|
||||
try {
|
||||
const encryptedVault = await storage.getItem('session:encryptedVault') as string;
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[];
|
||||
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[];
|
||||
const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number;
|
||||
|
||||
if (!encryptedVault) {
|
||||
console.error('Vault not available');
|
||||
return { success: false, error: 'Vault not available' };
|
||||
}
|
||||
|
||||
const decryptedVault = await EncryptionUtility.symmetricDecrypt(
|
||||
encryptedVault,
|
||||
derivedKey
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
vault: decryptedVault,
|
||||
publicEmailDomains: publicEmailDomains ?? [],
|
||||
privateEmailDomains: privateEmailDomains ?? [],
|
||||
vaultRevisionNumber: vaultRevisionNumber ?? 0
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to get vault:', error);
|
||||
return { success: false, error: 'Failed to get vault' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the vault from browser storage.
|
||||
*/
|
||||
export function handleClearVault(
|
||||
) : messageBoolResponse {
|
||||
storage.removeItems([
|
||||
'session:encryptedVault',
|
||||
'session:derivedKey',
|
||||
'session:publicEmailDomains',
|
||||
'session:privateEmailDomains',
|
||||
'session:vaultRevisionNumber'
|
||||
]);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all credentials.
|
||||
*/
|
||||
export async function handleGetCredentials(
|
||||
) : Promise<messageCredentialsResponse> {
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
|
||||
if (!derivedKey) {
|
||||
return { success: false, error: 'Vault is locked' };
|
||||
}
|
||||
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const credentials = sqliteClient.getAllCredentials();
|
||||
return { success: true, credentials: credentials };
|
||||
} catch (error) {
|
||||
console.error('Error getting credentials:', error);
|
||||
return { success: false, error: 'Failed to get credentials' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an identity.
|
||||
*/
|
||||
export async function handleCreateIdentity(
|
||||
message: any,
|
||||
) : Promise<messageBoolResponse> {
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
|
||||
if (!derivedKey) {
|
||||
return { success: false, error: 'Vault is locked' };
|
||||
}
|
||||
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
|
||||
// Add the new credential to the vault/database.
|
||||
sqliteClient.createCredential(message.credential);
|
||||
|
||||
// Upload the new vault to the server.
|
||||
await uploadNewVaultToServer(sqliteClient);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to create identity:', error);
|
||||
return { success: false, error: 'Failed to create identity' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the email addresses for a vault.
|
||||
*/
|
||||
export async function getEmailAddressesForVault(
|
||||
sqliteClient: SqliteClient
|
||||
): Promise<string[]> {
|
||||
// TODO: create separate query to only get email addresses to avoid loading all credentials.
|
||||
const credentials = sqliteClient.getAllCredentials();
|
||||
|
||||
// Get metadata from storage
|
||||
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[];
|
||||
|
||||
const emailAddresses = credentials
|
||||
.filter(cred => cred.Email != null)
|
||||
.map(cred => cred.Email)
|
||||
.filter((email, index, self) => self.indexOf(email) === index);
|
||||
|
||||
return emailAddresses.filter(email => {
|
||||
const domain = email.split('@')[1];
|
||||
return privateEmailDomains.includes(domain);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default email domain for a vault.
|
||||
*/
|
||||
export function handleGetDefaultEmailDomain(
|
||||
) : Promise<messageDefaultEmailDomainResponse> {
|
||||
return (async () : Promise<messageDefaultEmailDomainResponse> => {
|
||||
try {
|
||||
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[];
|
||||
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[];
|
||||
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const defaultEmailDomain = sqliteClient.getDefaultEmailDomain();
|
||||
|
||||
/**
|
||||
* Check if a domain is valid.
|
||||
*/
|
||||
const isValidDomain = (domain: string) : boolean => {
|
||||
const isValid = (domain &&
|
||||
domain !== 'DISABLED.TLD' &&
|
||||
(privateEmailDomains.includes(domain) || publicEmailDomains.includes(domain))) as boolean;
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
// First check if the default domain that is configured in the vault is still valid.
|
||||
if (defaultEmailDomain && isValidDomain(defaultEmailDomain)) {
|
||||
return { success: true, domain: defaultEmailDomain };
|
||||
}
|
||||
|
||||
// If default domain is not valid, fall back to first available private domain.
|
||||
const firstPrivate = privateEmailDomains.find(isValidDomain);
|
||||
|
||||
if (firstPrivate) {
|
||||
return { success: true, domain: firstPrivate };
|
||||
}
|
||||
|
||||
// Return first valid public domain if no private domains are available.
|
||||
const firstPublic = publicEmailDomains.find(isValidDomain);
|
||||
|
||||
if (firstPublic) {
|
||||
return { success: true, domain: firstPublic };
|
||||
}
|
||||
|
||||
// Return null if no valid domains are found
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error getting default email domain:', error);
|
||||
return { success: false, error: 'Failed to get default email domain' };
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the derived key for the encrypted vault.
|
||||
*/
|
||||
export async function handleGetDerivedKey(
|
||||
) : Promise<string> {
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
return derivedKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a new version of the vault to the server using the provided sqlite client.
|
||||
*/
|
||||
async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<void> {
|
||||
const updatedVaultData = sqliteClient.exportToBase64();
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
|
||||
const encryptedVault = await EncryptionUtility.symmetricEncrypt(
|
||||
updatedVaultData,
|
||||
derivedKey
|
||||
);
|
||||
|
||||
await storage.setItems([
|
||||
{ key: 'session:encryptedVault', value: encryptedVault }
|
||||
]);
|
||||
|
||||
// Get metadata from storage
|
||||
const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number;
|
||||
|
||||
// Upload new encrypted vault to server.
|
||||
const username = await storage.getItem('local:username') as string;
|
||||
const emailAddresses = await getEmailAddressesForVault(sqliteClient);
|
||||
|
||||
const newVault: Vault = {
|
||||
blob: encryptedVault,
|
||||
createdAt: new Date().toISOString(),
|
||||
credentialsCount: sqliteClient.getAllCredentials().length,
|
||||
currentRevisionNumber: vaultRevisionNumber,
|
||||
emailAddressList: emailAddresses,
|
||||
privateEmailDomainList: [], // Empty on purpose, API will not use this for vault updates.
|
||||
publicEmailDomainList: [], // Empty on purpose, API will not use this for vault updates.
|
||||
encryptionPublicKey: '', // Empty on purpose, only required if new public/private key pair is generated.
|
||||
client: '', // Empty on purpose, API will not use this for vault updates.
|
||||
updatedAt: new Date().toISOString(),
|
||||
username: username,
|
||||
version: sqliteClient.getDatabaseVersion() ?? '0.0.0'
|
||||
};
|
||||
|
||||
const webApi = new WebApiService(() => {});
|
||||
const response = await webApi.post<Vault, VaultPostResponse>('Vault', newVault);
|
||||
|
||||
// Check if response is successful (.status === 0)
|
||||
if (response.status === 0) {
|
||||
await storage.setItem('session:vaultRevisionNumber', response.newRevisionNumber);
|
||||
} else {
|
||||
throw new Error('Failed to upload new vault to server');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new sqlite client for the stored vault.
|
||||
*/
|
||||
async function createVaultSqliteClient() : Promise<SqliteClient> {
|
||||
const encryptedVault = await storage.getItem('session:encryptedVault') as string;
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
|
||||
if (!encryptedVault || !derivedKey) {
|
||||
throw new Error('No vault or derived key found');
|
||||
}
|
||||
|
||||
// Decrypt the vault.
|
||||
const decryptedVault = await EncryptionUtility.symmetricDecrypt(
|
||||
encryptedVault,
|
||||
derivedKey
|
||||
);
|
||||
|
||||
// Initialize the SQLite client with the decrypted vault.
|
||||
const sqliteClient = new SqliteClient();
|
||||
await sqliteClient.initializeFromBase64(decryptedVault);
|
||||
|
||||
return sqliteClient;
|
||||
}
|
||||
103
browser-extension/src/entrypoints/content.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import './contentScript/style.css';
|
||||
import { FormDetector } from '../utils/formDetector/FormDetector';
|
||||
import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup } from './contentScript/Popup';
|
||||
import { injectIcon, popupDebounceTimeHasPassed } from './contentScript/Form';
|
||||
import { onMessage } from "webext-bridge/content-script";
|
||||
import { BoolResponse as messageBoolResponse } from '../utils/types/messaging/BoolResponse';
|
||||
import { defineContentScript } from 'wxt/sandbox';
|
||||
import { createShadowRootUi } from 'wxt/client';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
cssInjectionMode: 'ui',
|
||||
allFrames: true,
|
||||
matchAboutBlank: true,
|
||||
runAt: 'document_start',
|
||||
|
||||
/**
|
||||
* Main entry point for the content script.
|
||||
*/
|
||||
async main(ctx) {
|
||||
if (ctx.isInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a shadow root UI for isolation
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'aliasvault-ui',
|
||||
position: 'inline',
|
||||
anchor: 'body',
|
||||
/**
|
||||
* Handle mount.
|
||||
*/
|
||||
onMount(container) {
|
||||
/**
|
||||
* Handle input field focus.
|
||||
*/
|
||||
const handleFocusIn = async (e: FocusEvent) : Promise<void> => {
|
||||
if (ctx.isInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e.target as HTMLInputElement;
|
||||
const textInputTypes = ['text', 'email', 'tel', 'password', 'search', 'url'];
|
||||
|
||||
if (target.tagName === 'INPUT' && textInputTypes.includes(target.type) && !target.dataset.aliasvaultIgnore) {
|
||||
const formDetector = new FormDetector(document, target);
|
||||
if (!formDetector.containsLoginForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
injectIcon(target, container);
|
||||
|
||||
// Only show popup if its enabled and debounce time has passed.
|
||||
if (await isAutoShowPopupEnabled() && popupDebounceTimeHasPassed()) {
|
||||
openAutofillPopup(target, container);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for input field focus in the main document
|
||||
document.addEventListener('focusin', handleFocusIn);
|
||||
|
||||
// Listen for popstate events (back/forward navigation)
|
||||
window.addEventListener('popstate', () => {
|
||||
if (ctx.isInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeExistingPopup(container);
|
||||
});
|
||||
|
||||
// Listen for messages from the background script
|
||||
onMessage('OPEN_AUTOFILL_POPUP', async (message: { data: { elementIdentifier: string } }) : Promise<messageBoolResponse> => {
|
||||
const { data } = message;
|
||||
const { elementIdentifier } = data;
|
||||
|
||||
if (!elementIdentifier) {
|
||||
return { success: false, error: 'No element identifier provided' };
|
||||
}
|
||||
|
||||
const target = document.getElementById(elementIdentifier) ?? document.getElementsByName(elementIdentifier)[0];
|
||||
|
||||
if (!(target instanceof HTMLInputElement)) {
|
||||
return { success: false, error: 'Target element is not an input field' };
|
||||
}
|
||||
|
||||
const formDetector = new FormDetector(document, target);
|
||||
|
||||
if (!formDetector.containsLoginForm(true)) {
|
||||
return { success: false, error: 'No form found' };
|
||||
}
|
||||
|
||||
injectIcon(target, container);
|
||||
openAutofillPopup(target, container);
|
||||
return { success: true };
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Mount the UI to create the shadow root
|
||||
ui.autoMount();
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Credential } from "../shared/types/Credential";
|
||||
import { CombinedStopWords } from "@/utils/formDetector/FieldPatterns";
|
||||
import { Credential } from "../../utils/types/Credential";
|
||||
|
||||
/**
|
||||
* Filter credentials based on current URL and page context to determine which credentials to show
|
||||
@@ -40,22 +41,11 @@ export function filterCredentials(credentials: Credential[], currentUrl: string,
|
||||
|
||||
// 3. Page title word match if still no matches
|
||||
if (filtered.length === 0 && pageTitle.length > 0) {
|
||||
// TODO: make bad words list configurable per language.
|
||||
const badWords = new Set([
|
||||
'login', 'signin', 'sign', 'register', 'signup', 'account',
|
||||
'portal', 'dashboard', 'home', 'welcome', 'authentication',
|
||||
'page', 'site', 'secure', 'password', 'access', 'member',
|
||||
'user', 'profile', 'auth', 'session', 'inloggen',
|
||||
'registreren', 'registratie', 'free', 'gratis', 'create',
|
||||
'new', 'aanmelden', 'inschrijven', 'nieuwsbrief', 'schrijf',
|
||||
'your', 'jouw'
|
||||
]);
|
||||
|
||||
const titleWords = pageTitle.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(word =>
|
||||
word.length > 3 && // Filter out words shorter than 4 characters
|
||||
!badWords.has(word.toLowerCase()) // Filter out generic words
|
||||
!CombinedStopWords.has(word.toLowerCase()) // Filter out generic words
|
||||
);
|
||||
|
||||
filtered = credentials.filter(cred =>
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FormDetector } from "../shared/formDetector/FormDetector";
|
||||
import { Credential } from "../shared/types/Credential";
|
||||
import { FormDetector } from "../../utils/formDetector/FormDetector";
|
||||
import { FormFiller } from "../../utils/formDetector/FormFiller";
|
||||
import { Credential } from "../../utils/types/Credential";
|
||||
import { openAutofillPopup } from "./Popup";
|
||||
|
||||
/**
|
||||
@@ -13,7 +14,7 @@ let popupDebounceTime = 0;
|
||||
/**
|
||||
* Check if popup can be shown based on debounce time.
|
||||
*/
|
||||
export function canShowPopup() : boolean {
|
||||
export function popupDebounceTimeHasPassed() : boolean {
|
||||
if (Date.now() < popupDebounceTime) {
|
||||
return false;
|
||||
}
|
||||
@@ -46,194 +47,14 @@ export function fillCredential(credential: Credential, input: HTMLInputElement)
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.usernameField) {
|
||||
form.usernameField.value = credential.Username;
|
||||
triggerInputEvents(form.usernameField);
|
||||
}
|
||||
if (form.passwordField) {
|
||||
form.passwordField.value = credential.Password;
|
||||
triggerInputEvents(form.passwordField);
|
||||
}
|
||||
if (form.passwordConfirmField) {
|
||||
form.passwordConfirmField.value = credential.Password;
|
||||
triggerInputEvents(form.passwordConfirmField);
|
||||
}
|
||||
if (form.emailField) {
|
||||
form.emailField.value = credential.Email;
|
||||
triggerInputEvents(form.emailField);
|
||||
}
|
||||
if (form.emailConfirmField) {
|
||||
form.emailConfirmField.value = credential.Email;
|
||||
triggerInputEvents(form.emailConfirmField);
|
||||
}
|
||||
if (form.fullNameField) {
|
||||
form.fullNameField.value = `${credential.Alias.FirstName} ${credential.Alias.LastName}`;
|
||||
triggerInputEvents(form.fullNameField);
|
||||
}
|
||||
if (form.firstNameField) {
|
||||
form.firstNameField.value = credential.Alias.FirstName;
|
||||
triggerInputEvents(form.firstNameField);
|
||||
}
|
||||
if (form.lastNameField) {
|
||||
form.lastNameField.value = credential.Alias.LastName;
|
||||
triggerInputEvents(form.lastNameField);
|
||||
}
|
||||
|
||||
// Handle birthdate with input events
|
||||
if (form.birthdateField.single) {
|
||||
if (credential.Alias.BirthDate) {
|
||||
const birthDate = new Date(credential.Alias.BirthDate);
|
||||
const day = birthDate.getDate().toString().padStart(2, '0');
|
||||
const month = (birthDate.getMonth() + 1).toString().padStart(2, '0');
|
||||
const year = birthDate.getFullYear().toString();
|
||||
|
||||
let formattedDate = '';
|
||||
switch (form.birthdateField.format) {
|
||||
case 'dd/mm/yyyy':
|
||||
formattedDate = `${day}/${month}/${year}`;
|
||||
break;
|
||||
case 'mm/dd/yyyy':
|
||||
formattedDate = `${month}/${day}/${year}`;
|
||||
break;
|
||||
case 'dd-mm-yyyy':
|
||||
formattedDate = `${day}-${month}-${year}`;
|
||||
break;
|
||||
case 'mm-dd-yyyy':
|
||||
formattedDate = `${month}-${day}-${year}`;
|
||||
break;
|
||||
case 'yyyy-mm-dd':
|
||||
default:
|
||||
formattedDate = `${year}-${month}-${day}`;
|
||||
break;
|
||||
}
|
||||
|
||||
form.birthdateField.single.value = formattedDate;
|
||||
triggerInputEvents(form.birthdateField.single);
|
||||
}
|
||||
} else if (credential.Alias.BirthDate) {
|
||||
const birthDate = new Date(credential.Alias.BirthDate);
|
||||
if (form.birthdateField.day) {
|
||||
if (form.birthdateField.day instanceof HTMLSelectElement) {
|
||||
const dayValue = birthDate.getDate().toString().padStart(2, '0');
|
||||
const dayOption = Array.from(form.birthdateField.day.options).find(opt =>
|
||||
opt.value === dayValue ||
|
||||
opt.value === birthDate.getDate().toString() ||
|
||||
opt.text === dayValue ||
|
||||
opt.text === birthDate.getDate().toString()
|
||||
);
|
||||
if (dayOption) {
|
||||
form.birthdateField.day.value = dayOption.value;
|
||||
}
|
||||
} else {
|
||||
form.birthdateField.day.value = birthDate.getDate().toString().padStart(2, '0');
|
||||
}
|
||||
triggerInputEvents(form.birthdateField.day);
|
||||
}
|
||||
if (form.birthdateField.month) {
|
||||
if (form.birthdateField.month instanceof HTMLSelectElement) {
|
||||
const monthValue = (birthDate.getMonth() + 1).toString().padStart(2, '0');
|
||||
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
const monthOption = Array.from(form.birthdateField.month.options).find(opt =>
|
||||
opt.value === monthValue ||
|
||||
opt.value === (birthDate.getMonth() + 1).toString() ||
|
||||
opt.text === monthValue ||
|
||||
opt.text === (birthDate.getMonth() + 1).toString() ||
|
||||
opt.text.toLowerCase() === monthNames[birthDate.getMonth()].toLowerCase() ||
|
||||
opt.text.toLowerCase() === monthNames[birthDate.getMonth()].substring(0, 3).toLowerCase()
|
||||
);
|
||||
if (monthOption) {
|
||||
form.birthdateField.month.value = monthOption.value;
|
||||
}
|
||||
} else {
|
||||
form.birthdateField.month.value = (birthDate.getMonth() + 1).toString().padStart(2, '0');
|
||||
}
|
||||
triggerInputEvents(form.birthdateField.month);
|
||||
}
|
||||
if (form.birthdateField.year) {
|
||||
if (form.birthdateField.year instanceof HTMLSelectElement) {
|
||||
const yearValue = birthDate.getFullYear().toString();
|
||||
const yearOption = Array.from(form.birthdateField.year.options).find(opt =>
|
||||
opt.value === yearValue ||
|
||||
opt.text === yearValue
|
||||
);
|
||||
if (yearOption) {
|
||||
form.birthdateField.year.value = yearOption.value;
|
||||
}
|
||||
} else {
|
||||
form.birthdateField.year.value = birthDate.getFullYear().toString();
|
||||
}
|
||||
triggerInputEvents(form.birthdateField.year);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle gender with input events
|
||||
switch (form.genderField.type) {
|
||||
case 'select':
|
||||
if (form.genderField.field) {
|
||||
const maleValues = ['m', 'male', 'heer', 'mr', 'mr.', 'man'];
|
||||
const femaleValues = ['f', 'female', 'mevrouw', 'mrs', 'mrs.', 'ms', 'ms.', 'vrouw'];
|
||||
|
||||
const selectElement = form.genderField.field as HTMLSelectElement;
|
||||
const options = Array.from(selectElement.options);
|
||||
|
||||
if (credential.Alias.Gender === 'Male') {
|
||||
const maleOption = options.find(opt =>
|
||||
maleValues.includes(opt.value.toLowerCase()) ||
|
||||
maleValues.includes(opt.text.toLowerCase())
|
||||
);
|
||||
if (maleOption) {
|
||||
selectElement.value = maleOption.value;
|
||||
}
|
||||
} else if (credential.Alias.Gender === 'Female') {
|
||||
const femaleOption = options.find(opt =>
|
||||
femaleValues.includes(opt.value.toLowerCase()) ||
|
||||
femaleValues.includes(opt.text.toLowerCase())
|
||||
);
|
||||
if (femaleOption) {
|
||||
selectElement.value = femaleOption.value;
|
||||
}
|
||||
}
|
||||
|
||||
triggerInputEvents(selectElement);
|
||||
}
|
||||
break;
|
||||
case 'radio': {
|
||||
const radioButtons = form.genderField.radioButtons;
|
||||
if (!radioButtons) {
|
||||
break;
|
||||
}
|
||||
|
||||
let selectedRadio: HTMLInputElement | null = null;
|
||||
if (credential.Alias.Gender === 'Male' && radioButtons.male) {
|
||||
radioButtons.male.checked = true;
|
||||
selectedRadio = radioButtons.male;
|
||||
} else if (credential.Alias.Gender === 'Female' && radioButtons.female) {
|
||||
radioButtons.female.checked = true;
|
||||
selectedRadio = radioButtons.female;
|
||||
} else if (credential.Alias.Gender === 'Other' && radioButtons.other) {
|
||||
radioButtons.other.checked = true;
|
||||
selectedRadio = radioButtons.other;
|
||||
}
|
||||
|
||||
if (selectedRadio) {
|
||||
triggerInputEvents(selectedRadio);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'text':
|
||||
if (form.genderField.field && credential.Alias.Gender) {
|
||||
(form.genderField.field as HTMLInputElement).value = credential.Alias.Gender;
|
||||
triggerInputEvents(form.genderField.field as HTMLInputElement);
|
||||
}
|
||||
break;
|
||||
}
|
||||
const formFiller = new FormFiller(form, triggerInputEvents);
|
||||
formFiller.fillFields(credential);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject icon for a focused input element
|
||||
*/
|
||||
export function injectIcon(input: HTMLInputElement): void {
|
||||
export function injectIcon(input: HTMLInputElement, container: HTMLElement): void {
|
||||
const aliasvaultIconSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg enable-background="new 0 0 500 500" version="1.1" viewBox="0 0 500 500" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m459.87 294.95c0.016205 5.4005 0.03241 10.801-0.35022 16.873-1.111 6.3392-1.1941 12.173-2.6351 17.649-10.922 41.508-36.731 69.481-77.351 83.408-7.2157 2.4739-14.972 3.3702-22.479 4.995-23.629 0.042205-47.257 0.11453-70.886 0.12027-46.762 0.011322-93.523-0.01416-140.95-0.43411-8.59-2.0024-16.766-2.8352-24.398-5.3326-21.595-7.0666-39.523-19.656-53.708-37.552-10.227-12.903-17.579-27.17-21.28-43.221-1.475-6.3967-2.4711-12.904-3.6852-19.361-0.051849-5.747-0.1037-11.494 0.26915-17.886 4.159-42.973 27.68-71.638 63.562-92.153 0-0.70761-0.001961-1.6988 3.12e-4 -2.69 0.022484-9.8293-1.3071-19.894 0.35664-29.438 3.2391-18.579 11.08-35.272 23.763-49.773 12.098-13.832 26.457-23.989 43.609-30.029 7.813-2.7512 16.14-4.0417 24.234-5.9948 7.392-0.025734 14.784-0.05146 22.835 0.32253 4.1959 0.95392 7.7946 1.2538 11.258 2.1053 17.16 4.2192 32.287 12.176 45.469 24.104 2.2558 2.0411 4.372 6.6241 9.621 3.868 16.839-8.8419 34.718-11.597 53.603-8.594 16.791 2.6699 31.602 9.4308 44.236 20.636 11.531 10.227 19.84 22.841 25.393 37.236 6.3436 16.445 10.389 33.163 6.0798 49.389 7.9587 8.9321 15.807 16.704 22.421 25.414 9.162 12.065 15.33 25.746 18.144 40.776 0.97046 5.1848 1.9111 10.375 2.8654 15.563m-71.597 71.012c5.5615-5.2284 12.002-9.7986 16.508-15.817 10.474-13.992 14.333-29.916 11.288-47.446-2.2496-12.95-8.1973-24.076-17.243-33.063-12.746-12.663-28.865-18.614-46.786-18.569-69.912 0.17712-139.82 0.56831-209.74 0.96176-15.922 0.089599-29.168 7.4209-39.685 18.296-14.45 14.944-20.408 33.343-16.655 54.368 2.2763 12.754 8.2167 23.748 17.158 32.66 13.299 13.255 30.097 18.653 48.728 18.651 59.321-0.005188 118.64 0.042358 177.96-0.046601 9.5912-0.014374 19.181-0.86588 28.773-0.88855 10.649-0.025146 19.978-3.825 29.687-9.1074z" fill="#EEC170"/>
|
||||
@@ -244,18 +65,7 @@ export function injectIcon(input: HTMLInputElement): void {
|
||||
</svg>`;
|
||||
|
||||
const ICON_HTML = `
|
||||
<div class="aliasvault-input-icon" style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
pointer-events: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
">
|
||||
<div class="av-input-icon">
|
||||
<img src="data:image/svg+xml;base64,${btoa(aliasvaultIconSvg)}" style="width: 100%; height: 100%;" />
|
||||
</div>
|
||||
`;
|
||||
@@ -266,20 +76,12 @@ export function injectIcon(input: HTMLInputElement): void {
|
||||
}
|
||||
|
||||
// Create an overlay container at document level if it doesn't exist
|
||||
let overlayContainer = document.getElementById('aliasvault-overlay-container');
|
||||
let overlayContainer = container.querySelector('#aliasvault-overlay-container');
|
||||
if (!overlayContainer) {
|
||||
overlayContainer = document.createElement('div');
|
||||
overlayContainer = document.createElement('div') as HTMLElement;
|
||||
overlayContainer.id = 'aliasvault-overlay-container';
|
||||
overlayContainer.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 2147483640;
|
||||
`;
|
||||
document.body.appendChild(overlayContainer);
|
||||
overlayContainer.className = 'av-overlay-container';
|
||||
container.appendChild(overlayContainer);
|
||||
}
|
||||
|
||||
// Create the icon element from the HTML template
|
||||
@@ -311,7 +113,7 @@ export function injectIcon(input: HTMLInputElement): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setTimeout(() => input.focus(), 0);
|
||||
openAutofillPopup(input);
|
||||
openAutofillPopup(input, container);
|
||||
});
|
||||
|
||||
// Append the icon to the overlay container
|
||||
@@ -330,6 +132,7 @@ export function injectIcon(input: HTMLInputElement): void {
|
||||
setTimeout(() => {
|
||||
icon.remove();
|
||||
input.removeEventListener('blur', handleBlur);
|
||||
input.removeEventListener('keydown', handleKeyPress);
|
||||
window.removeEventListener('scroll', updateIconPosition, true);
|
||||
window.removeEventListener('resize', updateIconPosition);
|
||||
|
||||
@@ -340,7 +143,18 @@ export function injectIcon(input: HTMLInputElement): void {
|
||||
}, 200);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle key press to dismiss icon.
|
||||
*/
|
||||
const handleKeyPress = (e: KeyboardEvent): void => {
|
||||
// Dismiss on Enter, Escape, or Tab.
|
||||
if (e.key === 'Enter' || e.key === 'Escape' || e.key === 'Tab') {
|
||||
handleBlur();
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('blur', handleBlur);
|
||||
input.addEventListener('keydown', handleKeyPress);
|
||||
}
|
||||
|
||||
/**
|
||||
1032
browser-extension/src/entrypoints/contentScript/Popup.ts
Normal file
424
browser-extension/src/entrypoints/contentScript/style.css
Normal file
@@ -0,0 +1,424 @@
|
||||
/* AliasVault Content Script Styles */
|
||||
body {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* Base Popup Styles */
|
||||
.av-popup {
|
||||
position: absolute;
|
||||
z-index: 2147483646;
|
||||
background-color: rgb(31, 41, 55);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
width: 320px;
|
||||
border: 1px solid rgb(55, 65, 81);
|
||||
border-radius: 4px;
|
||||
max-width: 90vw;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Loading Popup Styles */
|
||||
.av-loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.av-loading-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.av-loading-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
/* Credential List Styles */
|
||||
.av-credential-list {
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #4b5563 #1f2937;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.av-credential-item {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.av-credential-item:hover {
|
||||
background-color: #2d3748;
|
||||
}
|
||||
|
||||
.av-credential-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-grow: 1;
|
||||
padding: 10px 16px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.av-credential-logo {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.av-credential-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.av-service-name {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
color: #f3f4f6;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-service-details {
|
||||
font-size: 0.85em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #9ca3af;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-popout-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
margin-right: 16px;
|
||||
opacity: 0.6;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
color: #ffffff;
|
||||
transition: opacity 0.2s ease, background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.av-popout-icon:hover {
|
||||
opacity: 1;
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.av-no-matches {
|
||||
padding-left: 10px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.av-divider {
|
||||
height: 1px;
|
||||
background: #374151;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Action Container */
|
||||
.av-action-container {
|
||||
display: flex;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
padding-bottom: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.av-button {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
background: #374151;
|
||||
color: #e5e7eb;
|
||||
font-size: 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.av-button:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.av-button-primary {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.av-button-primary:hover {
|
||||
background-color: #d68338;
|
||||
}
|
||||
|
||||
.av-button-close {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.av-button-close:hover {
|
||||
background-color: #dc2626;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Search Input */
|
||||
.av-search-input {
|
||||
flex: 2;
|
||||
border-radius: 4px;
|
||||
background: #374151;
|
||||
color: #e5e7eb;
|
||||
font-size: 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
border: 1px solid #4b5563;
|
||||
outline: none;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.av-search-input::placeholder {
|
||||
color: #bdbebe;
|
||||
}
|
||||
|
||||
.av-search-input:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
/* Vault Locked Popup */
|
||||
.av-vault-locked {
|
||||
padding: 12px 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.av-vault-locked:hover {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.av-vault-locked-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 32px;
|
||||
width: 100%;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.av-vault-locked-message {
|
||||
color: #d1d5db;
|
||||
font-size: 14px;
|
||||
flex-grow: 1;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-vault-locked-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
padding-right: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #d68338;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.av-vault-locked-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;
|
||||
}
|
||||
|
||||
/* Create Name Popup */
|
||||
.av-create-popup-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2147483647;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.av-create-popup {
|
||||
position: relative;
|
||||
z-index: 1000000000;
|
||||
background: #1f2937;
|
||||
border: 1px solid #374151;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06),
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
width: 400px;
|
||||
max-width: 90vw;
|
||||
transform: scale(0.95);
|
||||
opacity: 0;
|
||||
padding: 24px;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-create-popup.show {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.av-create-popup-title {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 18px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-weight: 600;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.av-create-popup-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid #374151;
|
||||
border-radius: 6px;
|
||||
background: #374151;
|
||||
color: #f8f9fa;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.av-create-popup-input:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.av-create-popup-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.av-create-popup-cancel {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #374151;
|
||||
background: transparent;
|
||||
color: #f8f9fa;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.av-create-popup-cancel:hover {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.av-create-popup-save {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
background: #d68338;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.av-create-popup-save:hover {
|
||||
background: #c97731;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* SVG Icons */
|
||||
.av-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.av-icon-lock {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* Form Icon Styles */
|
||||
.av-overlay-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 2147483640;
|
||||
}
|
||||
|
||||
.av-input-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
pointer-events: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
0% { opacity: 1; transform: scale(1.02); }
|
||||
100% { opacity: 0; transform: scale(1); }
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { useAuth } from './context/AuthContext';
|
||||
import { useMinDurationLoading } from './hooks/useMinDurationLoading';
|
||||
import { useMinDurationLoading } from '../../hooks/useMinDurationLoading';
|
||||
import Header from './components/Layout/Header';
|
||||
import BottomNav from './components/Layout/BottomNav';
|
||||
import AuthSettings from './pages/AuthSettings';
|
||||
@@ -83,7 +83,8 @@ const App: React.FC = () => {
|
||||
className="flex-1 overflow-y-auto bg-gray-100 dark:bg-gray-900"
|
||||
style={{
|
||||
paddingTop: '64px',
|
||||
height: 'calc(100vh - 120px)',
|
||||
height: 'calc(100% - 120px)',
|
||||
maxHeight: '600px',
|
||||
}}
|
||||
>
|
||||
<div className="p-4 mb-16">
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useWebApi } from '../context/WebApiContext';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import EncryptionUtility from '../../shared/EncryptionUtility';
|
||||
import { MailboxEmail } from '../../shared/types/webapi/MailboxEmail';
|
||||
import EncryptionUtility from '../../../utils/EncryptionUtility';
|
||||
import { MailboxEmail } from '../../../utils/types/webapi/MailboxEmail';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { AppInfo } from '../../shared/AppInfo';
|
||||
import { AppInfo } from '../../../utils/AppInfo';
|
||||
import { storage } from 'wxt/storage';
|
||||
|
||||
type EmailPreviewProps = {
|
||||
email: string;
|
||||
@@ -26,8 +27,8 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
*/
|
||||
const isPublicDomain = async (emailAddress: string): Promise<boolean> => {
|
||||
// Get metadata from storage
|
||||
const storageResult = await chrome.storage.session.get(['publicEmailDomains']);
|
||||
return storageResult.publicEmailDomains.some(domain => emailAddress.toLowerCase().endsWith(domain));
|
||||
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[] ?? [];
|
||||
return publicEmailDomains.some(domain => emailAddress.toLowerCase().endsWith(domain));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -134,7 +135,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
href={`https://spamok.com/${email.split('@')[0]}/${mail.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`flex justify-between items-center p-2 rounded cursor-pointer bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
||||
className={`flex justify-between items-center p-2 ps-3 pe-3 rounded cursor-pointer bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
||||
mail.id > lastEmailId ? 'bg-yellow-50 dark:bg-yellow-900/30' : ''
|
||||
}`}
|
||||
>
|
||||
@@ -151,7 +152,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
<Link
|
||||
key={mail.id}
|
||||
to={`/emails/${mail.id}`}
|
||||
className={`flex justify-between items-center p-2 rounded cursor-pointer bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
||||
className={`flex justify-between items-center p-2 ps-3 pe-3 rounded cursor-pointer bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
||||
mail.id > lastEmailId ? 'bg-yellow-50 dark:bg-yellow-900/30' : ''
|
||||
}`}
|
||||
>
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useDb } from '../../context/DbContext';
|
||||
|
||||
type TabName = 'credentials' | 'emails' | 'settings';
|
||||
|
||||
/**
|
||||
* Bottom nav component.
|
||||
*/
|
||||
@@ -10,12 +12,21 @@ const BottomNav: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const navigate = useNavigate();
|
||||
const [currentTab, setCurrentTab] = useState<'credentials' | 'emails' | 'settings'>('credentials');
|
||||
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);
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
/**
|
||||
* Handle tab change.
|
||||
*/
|
||||
const handleTabChange = (tab: 'credentials' | 'emails' | 'settings') : void => {
|
||||
const handleTabChange = (tab: TabName) : void => {
|
||||
setCurrentTab(tab);
|
||||
navigate(`/${tab}`);
|
||||
};
|
||||
@@ -2,7 +2,9 @@ import React from 'react';
|
||||
import { UserMenu } from './UserMenu';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { AppInfo } from '../../../shared/AppInfo';
|
||||
import { AppInfo } from '../../../../utils/AppInfo';
|
||||
import { storage } from 'wxt/storage';
|
||||
|
||||
/**
|
||||
* Header props.
|
||||
*/
|
||||
@@ -28,10 +30,10 @@ const Header: React.FC<HeaderProps> = ({
|
||||
* Open the client tab.
|
||||
*/
|
||||
const openClientTab = async () : Promise<void> => {
|
||||
const setting = await chrome.storage.local.get(['clientUrl']);
|
||||
const settingClientUrl = await storage.getItem('local:clientUrl') as string;
|
||||
let clientUrl = AppInfo.DEFAULT_CLIENT_URL;
|
||||
if (setting.clientUrl && setting.clientUrl.length > 0) {
|
||||
clientUrl = setting.clientUrl;
|
||||
if (settingClientUrl && settingClientUrl.length > 0) {
|
||||
clientUrl = settingClientUrl;
|
||||
}
|
||||
|
||||
window.open(clientUrl, '_blank');
|
||||
@@ -52,6 +54,19 @@ const Header: React.FC<HeaderProps> = ({
|
||||
navigate('/auth-settings');
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logo click.
|
||||
*/
|
||||
const logoClick = () : void => {
|
||||
// If logged in, navigate to credentials.
|
||||
if (authContext.isLoggedIn) {
|
||||
navigate('/credentials');
|
||||
} else {
|
||||
// If not logged in, navigate to index.
|
||||
navigate('/');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="fixed z-30 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div className="flex items-center h-16 px-4">
|
||||
@@ -74,9 +89,17 @@ const Header: React.FC<HeaderProps> = ({
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<img src="/assets/images/logo.svg" alt="AliasVault" className="h-8 w-8 mr-2" />
|
||||
<h1 className="text-gray-900 dark:text-white text-xl font-bold">AliasVault</h1>
|
||||
<span className="text-primary-500 text-[10px] ml-1 font-normal">BETA</span>
|
||||
<button
|
||||
onClick={() => logoClick()}
|
||||
className="flex items-center hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<img src="/assets/images/logo.svg" alt="AliasVault" className="h-8 w-8 mr-2" />
|
||||
<h1 className="text-gray-900 dark:text-white text-xl font-bold">AliasVault</h1>
|
||||
{/* Hide beta badge on Safari as it's not allowed to show non-production badges */}
|
||||
{!import.meta.env.SAFARI && (
|
||||
<span className="text-primary-500 text-[10px] ml-1 font-normal">BETA</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { AppInfo } from '../../shared/AppInfo';
|
||||
import { AppInfo } from '../../../utils/AppInfo';
|
||||
import { storage } from 'wxt/storage';
|
||||
|
||||
/**
|
||||
* Component for displaying the login server information.
|
||||
@@ -14,8 +15,8 @@ const LoginServerInfo: React.FC = () => {
|
||||
* Loads the base URL for the login server.
|
||||
*/
|
||||
const loadApiUrl = async () : Promise<void> => {
|
||||
const result = await chrome.storage.local.get(['apiUrl']);
|
||||
setBaseUrl(result.apiUrl ?? AppInfo.DEFAULT_API_URL);
|
||||
const apiUrl = await storage.getItem('local:apiUrl') as string;
|
||||
setBaseUrl(apiUrl ?? AppInfo.DEFAULT_API_URL);
|
||||
};
|
||||
loadApiUrl();
|
||||
}, []);
|
||||
@@ -0,0 +1,192 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import { TotpCode } from '../../../utils/types/TotpCode';
|
||||
import * as OTPAuth from 'otpauth';
|
||||
|
||||
type TotpViewerProps = {
|
||||
credentialId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows TOTP codes for a credential.
|
||||
*/
|
||||
export const TotpViewer: React.FC<TotpViewerProps> = ({ credentialId }) => {
|
||||
const [totpCodes, setTotpCodes] = useState<TotpCode[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentCodes, setCurrentCodes] = useState<Record<string, string>>({});
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
const dbContext = useDb();
|
||||
|
||||
/**
|
||||
* Gets the remaining seconds for the TOTP code.
|
||||
*/
|
||||
const getRemainingSeconds = (step = 30): number => {
|
||||
const totp = new OTPAuth.TOTP({
|
||||
secret: 'dummy', // We only need this for timing calculations
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: step
|
||||
});
|
||||
return totp.period - (Math.floor(Date.now() / 1000) % totp.period);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the remaining percentage for the TOTP code.
|
||||
*/
|
||||
const getRemainingPercentage = (): number => {
|
||||
const remaining = getRemainingSeconds();
|
||||
// Invert the percentage so it counts down instead of up
|
||||
return Math.floor(((30.0 - remaining) / 30.0) * 100);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a TOTP code for a given secret key.
|
||||
*/
|
||||
const generateTotpCode = (secretKey: string): string => {
|
||||
try {
|
||||
const totp = new OTPAuth.TOTP({
|
||||
secret: secretKey,
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30
|
||||
});
|
||||
return totp.generate();
|
||||
} catch (error) {
|
||||
console.error('Error generating TOTP code:', error);
|
||||
return 'Error';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Copies a TOTP code to the clipboard.
|
||||
*/
|
||||
const copyToClipboard = async (code: string, id: string): Promise<void> => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopiedId(id);
|
||||
|
||||
// Reset copied state after 2 seconds
|
||||
setTimeout(() => {
|
||||
setCopiedId(null);
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Loads the TOTP codes for the credential.
|
||||
*/
|
||||
const loadTotpCodes = async (): Promise<void> => {
|
||||
if (!dbContext?.sqliteClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const codes = dbContext.sqliteClient.getTotpCodesForCredential(credentialId);
|
||||
setTotpCodes(codes);
|
||||
} catch (error) {
|
||||
console.error('Error loading TOTP codes:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadTotpCodes();
|
||||
}, [credentialId, dbContext?.sqliteClient]);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Updates the current TOTP codes.
|
||||
*/
|
||||
const updateTotpCodes = (prevCodes: Record<string, string>): Record<string, string> => {
|
||||
const newCodes: Record<string, string> = {};
|
||||
totpCodes.forEach(code => {
|
||||
const generatedCode = generateTotpCode(code.SecretKey);
|
||||
// Only update if we have a valid code
|
||||
if (generatedCode !== 'Error') {
|
||||
newCodes[code.Id] = generatedCode;
|
||||
} else {
|
||||
// Keep the previous code if there's an error
|
||||
newCodes[code.Id] = prevCodes[code.Id] ?? 'Error';
|
||||
}
|
||||
});
|
||||
return newCodes;
|
||||
};
|
||||
|
||||
// Generate initial codes
|
||||
const initialCodes: Record<string, string> = {};
|
||||
totpCodes.forEach(code => {
|
||||
initialCodes[code.Id] = generateTotpCode(code.SecretKey);
|
||||
});
|
||||
setCurrentCodes(initialCodes);
|
||||
|
||||
// Set up interval to refresh codes
|
||||
const intervalId = setInterval(() => {
|
||||
setCurrentCodes(updateTotpCodes);
|
||||
}, 1000);
|
||||
|
||||
// Clean up interval on unmount or when totpCodes change
|
||||
return () : void => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [totpCodes]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Two-factor authentication</h2>
|
||||
Loading TOTP codes...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (totpCodes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-base font-semibold text-gray-900 dark:text-white">Two-factor authentication</h2>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{totpCodes.map(totpCode => (
|
||||
<button
|
||||
key={totpCode.Id}
|
||||
className={`w-full text-left p-2 ps-3 pe-3 rounded bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700`}
|
||||
onClick={() => copyToClipboard(currentCodes[totpCode.Id], totpCode.Id)}
|
||||
aria-label={`Copy ${totpCode.Name} code`}
|
||||
>
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<div className="flex items-center flex-1">
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{totpCode.Name}</h4>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-lg font-bold text-gray-900 dark:text-white">
|
||||
{currentCodes[totpCode.Id]}
|
||||
</span>
|
||||
<div className="text-xs">
|
||||
{copiedId === totpCode.Id ? (
|
||||
<span className="text-green-600 dark:text-green-400">Copied!</span>
|
||||
) : (
|
||||
<span className="text-gray-500 dark:text-gray-400">{getRemainingSeconds()}s</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1 h-6 bg-gray-200 rounded-full dark:bg-gray-600">
|
||||
<div
|
||||
className="bg-blue-600 rounded-full transition-all"
|
||||
style={{ height: `${getRemainingPercentage()}%`, width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,8 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useDb } from './DbContext';
|
||||
import { storage } from 'wxt/storage';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/entrypoints/contentScript/Popup';
|
||||
|
||||
type AuthContextType = {
|
||||
isLoggedIn: boolean;
|
||||
@@ -35,9 +38,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
* Initialize the authentication state.
|
||||
*/
|
||||
const initializeAuth = async () : Promise<void> => {
|
||||
const stored = await chrome.storage.local.get(['accessToken', 'refreshToken', 'username']);
|
||||
if (stored.accessToken && stored.refreshToken && stored.username) {
|
||||
setUsername(stored.username);
|
||||
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);
|
||||
}
|
||||
setIsInitialized(true);
|
||||
@@ -50,11 +55,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
* Set auth tokens in chrome storage as part of the login process. After db is initialized, the login method should be called as well.
|
||||
*/
|
||||
const setAuthTokens = useCallback(async (username: string, accessToken: string, refreshToken: string) : Promise<void> => {
|
||||
await chrome.storage.local.set({
|
||||
username,
|
||||
accessToken,
|
||||
refreshToken
|
||||
});
|
||||
await storage.setItem('local:username', username);
|
||||
await storage.setItem('local:accessToken', accessToken);
|
||||
await storage.setItem('local:refreshToken', refreshToken);
|
||||
|
||||
setUsername(username);
|
||||
}, []);
|
||||
@@ -64,14 +67,17 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
*/
|
||||
const login = useCallback(async () : Promise<void> => {
|
||||
setIsLoggedIn(true);
|
||||
|
||||
// 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);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Logout the user and clear the auth tokens from chrome storage.
|
||||
*/
|
||||
const logout = useCallback(async (errorMessage?: string) : Promise<void> => {
|
||||
await chrome.runtime.sendMessage({ type: 'CLEAR_VAULT' });
|
||||
await chrome.storage.local.remove(['username', 'accessToken', 'refreshToken']);
|
||||
await sendMessage('CLEAR_VAULT', {}, 'background');
|
||||
await storage.removeItems(['local:username', 'local:accessToken', 'local:refreshToken']);
|
||||
dbContext?.clearDatabase();
|
||||
|
||||
// Set local storage global message that will be shown on the login page.
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import SqliteClient from '../../shared/SqliteClient';
|
||||
import { VaultResponse } from '../../shared/types/webapi/VaultResponse';
|
||||
import EncryptionUtility from '../../shared/EncryptionUtility';
|
||||
import SqliteClient from '../../../utils/SqliteClient';
|
||||
import { VaultResponse } from '../../../utils/types/webapi/VaultResponse';
|
||||
import EncryptionUtility from '../../../utils/EncryptionUtility';
|
||||
import { VaultResponse as messageVaultResponse } from '../../../utils/types/messaging/VaultResponse';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
type DbContextType = {
|
||||
sqliteClient: SqliteClient | null;
|
||||
@@ -71,16 +73,15 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
/*
|
||||
* Store encrypted vault in background worker.
|
||||
*/
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'STORE_VAULT',
|
||||
sendMessage('STORE_VAULT', {
|
||||
derivedKey: derivedKey,
|
||||
vaultResponse: vaultResponse,
|
||||
});
|
||||
}, 'background');
|
||||
}, []);
|
||||
|
||||
const checkStoredVault = useCallback(async () => {
|
||||
try {
|
||||
const response = await chrome.runtime.sendMessage({ type: 'GET_VAULT' });
|
||||
const response = await sendMessage('GET_VAULT', {}, 'background') as messageVaultResponse;
|
||||
if (response?.vault) {
|
||||
const client = new SqliteClient();
|
||||
await client.initializeFromBase64(response.vault);
|
||||
@@ -88,9 +89,9 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
setSqliteClient(client);
|
||||
setDbInitialized(true);
|
||||
setDbAvailable(true);
|
||||
setPublicEmailDomains(response.publicEmailDomains);
|
||||
setPrivateEmailDomains(response.privateEmailDomains);
|
||||
setVaultRevision(response.vaultRevisionNumber);
|
||||
setPublicEmailDomains(response.publicEmailDomains ?? []);
|
||||
setPrivateEmailDomains(response.privateEmailDomains ?? []);
|
||||
setVaultRevision(response.vaultRevisionNumber ?? 0);
|
||||
} else {
|
||||
setDbInitialized(true);
|
||||
setDbAvailable(false);
|
||||
@@ -117,7 +118,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
const clearDatabase = useCallback(() : void => {
|
||||
setSqliteClient(null);
|
||||
setDbInitialized(false);
|
||||
chrome.runtime.sendMessage({ type: 'CLEAR_VAULT' });
|
||||
sendMessage('CLEAR_VAULT', {}, 'background');
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(() => ({
|
||||
134
browser-extension/src/entrypoints/popup/context/ThemeContext.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { createContext, useContext, useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import { storage } from 'wxt/storage';
|
||||
|
||||
/**
|
||||
* Theme type.
|
||||
*/
|
||||
type Theme = 'light' | 'dark' | 'system';
|
||||
|
||||
/**
|
||||
* Theme preference key in storage.
|
||||
*/
|
||||
const THEME_PREFERENCE_KEY = 'local:theme';
|
||||
|
||||
/**
|
||||
* Theme context type.
|
||||
*/
|
||||
type ThemeContextType = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme context.
|
||||
*/
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Theme provider
|
||||
*/
|
||||
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
/**
|
||||
* Theme state that can be 'light', 'dark', or 'system'.
|
||||
*/
|
||||
const [theme, setTheme] = useState<Theme>('system');
|
||||
|
||||
/**
|
||||
* Tracks whether dark mode is active (based on theme or system preference).
|
||||
*/
|
||||
const [isDarkMode, setIsDarkMode] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Load theme setting from storage.
|
||||
*/
|
||||
const loadTheme = async () : Promise<void> => {
|
||||
const savedTheme = await getTheme();
|
||||
setTheme(savedTheme);
|
||||
};
|
||||
loadTheme();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Set the theme and save to storage.
|
||||
*/
|
||||
const updateTheme = useCallback((newTheme: Theme): void => {
|
||||
setTheme(newTheme);
|
||||
setStoredTheme(newTheme);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get the theme from storage.
|
||||
*/
|
||||
const getTheme = async (): Promise<Theme> => {
|
||||
return (await storage.getItem(THEME_PREFERENCE_KEY) as Theme) || 'system';
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the theme in storage.
|
||||
*/
|
||||
const setStoredTheme = async (theme: Theme): Promise<void> => {
|
||||
await storage.setItem(THEME_PREFERENCE_KEY, theme);
|
||||
};
|
||||
|
||||
/**
|
||||
* Effect to apply theme to document and handle system preference changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Update the dark mode status.
|
||||
*/
|
||||
const updateDarkMode = (): void => {
|
||||
if (theme === 'system') {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
setIsDarkMode(prefersDark);
|
||||
document.documentElement.classList.toggle('dark', prefersDark);
|
||||
} else {
|
||||
const isDark = theme === 'dark';
|
||||
setIsDarkMode(isDark);
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial update
|
||||
updateDarkMode();
|
||||
|
||||
// Listen for system preference changes if using 'system' theme
|
||||
if (theme === 'system') {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
/**
|
||||
* Update the dark mode status when the system preference changes.
|
||||
*/
|
||||
const handler = () : void => updateDarkMode();
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
return () : void => mediaQuery.removeEventListener('change', handler);
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
theme,
|
||||
setTheme: updateTheme,
|
||||
isDarkMode,
|
||||
}),
|
||||
[theme, isDarkMode, updateTheme]
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={value}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to use theme state
|
||||
*/
|
||||
export const useTheme = (): ThemeContextType => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { WebApiService } from '../../shared/WebApiService';
|
||||
import { WebApiService } from '../../../utils/WebApiService';
|
||||
import { useAuth } from './AuthContext';
|
||||
|
||||
const WebApiContext = createContext<WebApiService | null>(null);
|
||||
@@ -4,9 +4,11 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AliasVault</title>
|
||||
<link href="~/assets/tailwind.css" rel="stylesheet" />
|
||||
<meta name="manifest.type" content="browser_action" />
|
||||
</head>
|
||||
<body class="bg-white dark:bg-gray-900">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="src/app/Index.tsx"></script>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,6 +4,11 @@ import { AuthProvider } from './context/AuthContext';
|
||||
import { WebApiProvider } from './context/WebApiContext';
|
||||
import { DbProvider } from './context/DbContext';
|
||||
import { LoadingProvider } from './context/LoadingContext';
|
||||
import { ThemeProvider } from './context/ThemeContext';
|
||||
import { setupExpandedMode } from '../../utils/ExpandedMode';
|
||||
|
||||
// Run before React initializes to ensure the popup is always a fixed width except for when explicitly expanded.
|
||||
setupExpandedMode();
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
root.render(
|
||||
@@ -11,7 +16,9 @@ root.render(
|
||||
<AuthProvider>
|
||||
<WebApiProvider>
|
||||
<LoadingProvider>
|
||||
<App />
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</LoadingProvider>
|
||||
</WebApiProvider>
|
||||
</AuthProvider>
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AppInfo } from '../../shared/AppInfo';
|
||||
import { AppInfo } from '../../../utils/AppInfo';
|
||||
import { storage } from 'wxt/storage';
|
||||
import { GLOBAL_POPUP_ENABLED_KEY, DISABLED_SITES_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY } from '../../contentScript/Popup';
|
||||
|
||||
type ApiOption = {
|
||||
label: string;
|
||||
@@ -18,56 +20,86 @@ const AuthSettings: React.FC = () => {
|
||||
const [selectedOption, setSelectedOption] = useState<string>('');
|
||||
const [customUrl, setCustomUrl] = useState<string>('');
|
||||
const [customClientUrl, setCustomClientUrl] = useState<string>('');
|
||||
const [isGloballyEnabled, setIsGloballyEnabled] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Load saved URLs from storage
|
||||
chrome.storage.local.get(['apiUrl', 'clientUrl'], (result) => {
|
||||
const savedUrl = result.apiUrl;
|
||||
const savedClientUrl = result.clientUrl;
|
||||
const matchingOption = DEFAULT_OPTIONS.find(opt => opt.value === savedUrl);
|
||||
/**
|
||||
* Load the stored settings from the storage.
|
||||
*/
|
||||
const loadStoredSettings = async () : Promise<void> => {
|
||||
const apiUrl = await storage.getItem('local:apiUrl') as string;
|
||||
const clientUrl = await storage.getItem('local:clientUrl') as string;
|
||||
const globallyEnabled = await storage.getItem(GLOBAL_POPUP_ENABLED_KEY) !== false; // Default to true if not set
|
||||
const dismissUntil = await storage.getItem(VAULT_LOCKED_DISMISS_UNTIL_KEY) as number;
|
||||
|
||||
if (dismissUntil) {
|
||||
setIsGloballyEnabled(false);
|
||||
} else {
|
||||
setIsGloballyEnabled(globallyEnabled);
|
||||
}
|
||||
|
||||
const matchingOption = DEFAULT_OPTIONS.find(opt => opt.value === apiUrl);
|
||||
|
||||
if (matchingOption) {
|
||||
setSelectedOption(matchingOption.value);
|
||||
} else if (savedUrl) {
|
||||
} else if (apiUrl) {
|
||||
setSelectedOption('custom');
|
||||
setCustomUrl(savedUrl);
|
||||
setCustomClientUrl(savedClientUrl ?? '');
|
||||
setCustomUrl(apiUrl);
|
||||
setCustomClientUrl(clientUrl ?? '');
|
||||
} else {
|
||||
setSelectedOption(DEFAULT_OPTIONS[0].value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
loadStoredSettings();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle option change
|
||||
*/
|
||||
const handleOptionChange = (e: React.ChangeEvent<HTMLSelectElement>) : void => {
|
||||
const handleOptionChange = async (e: React.ChangeEvent<HTMLSelectElement>) : Promise<void> => {
|
||||
const value = e.target.value;
|
||||
setSelectedOption(value);
|
||||
if (value !== 'custom') {
|
||||
chrome.storage.local.set({
|
||||
apiUrl: '',
|
||||
clientUrl: '',
|
||||
});
|
||||
await storage.setItem('local:apiUrl', '');
|
||||
await storage.setItem('local:clientUrl', '');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle custom API URL change
|
||||
*/
|
||||
const handleCustomUrlChange = (e: React.ChangeEvent<HTMLInputElement>) : void => {
|
||||
const handleCustomUrlChange = async (e: React.ChangeEvent<HTMLInputElement>) : Promise<void> => {
|
||||
const value = e.target.value;
|
||||
setCustomUrl(value);
|
||||
chrome.storage.local.set({ apiUrl: value });
|
||||
await storage.setItem('local:apiUrl', value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle custom client URL change
|
||||
* @param e
|
||||
*/
|
||||
const handleCustomClientUrlChange = (e: React.ChangeEvent<HTMLInputElement>) : void => {
|
||||
const handleCustomClientUrlChange = async (e: React.ChangeEvent<HTMLInputElement>) : Promise<void> => {
|
||||
const value = e.target.value;
|
||||
setCustomClientUrl(value);
|
||||
chrome.storage.local.set({ clientUrl: value });
|
||||
await storage.setItem('local:clientUrl', value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle global popup.
|
||||
*/
|
||||
const toggleGlobalPopup = async () : Promise<void> => {
|
||||
const newGloballyEnabled = !isGloballyEnabled;
|
||||
|
||||
await storage.setItem(GLOBAL_POPUP_ENABLED_KEY, newGloballyEnabled);
|
||||
|
||||
if (newGloballyEnabled) {
|
||||
// Reset all disabled sites when enabling globally
|
||||
await storage.setItem(DISABLED_SITES_KEY, []);
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
}
|
||||
|
||||
setIsGloballyEnabled(newGloballyEnabled);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -119,6 +151,27 @@ const AuthSettings: React.FC = () => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Autofill Popup Settings Section */}
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">Autofill popup</p>
|
||||
<button
|
||||
onClick={toggleGlobalPopup}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
isGloballyEnabled
|
||||
? 'bg-green-200 text-green-800 hover:bg-green-300 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50'
|
||||
: 'bg-red-200 text-red-800 hover:bg-red-300 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50'
|
||||
}`}
|
||||
>
|
||||
{isGloballyEnabled ? 'Enabled' : 'Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-gray-400 dark:text-gray-600">
|
||||
Version: {AppInfo.VERSION}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import { Credential } from '../../shared/types/Credential';
|
||||
import { Credential } from '../../../utils/types/Credential';
|
||||
import { Buffer } from 'buffer';
|
||||
import { FormInputCopyToClipboard } from '../components/FormInputCopyToClipboard';
|
||||
import { EmailPreview } from '../components/EmailPreview';
|
||||
import { TotpViewer } from '../components/TotpViewer';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
|
||||
/**
|
||||
@@ -18,15 +19,15 @@ const CredentialDetails: React.FC = () => {
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
|
||||
/**
|
||||
* Check if the current page is a popup.
|
||||
* Check if the current page is an expanded popup.
|
||||
*/
|
||||
const isPopup = () : boolean => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('popup') === 'true';
|
||||
return urlParams.get('expanded') === 'true';
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the credential details in a new popup.
|
||||
* Open the credential details in a new expanded popup.
|
||||
*/
|
||||
const openInNewPopup = () : void => {
|
||||
const width = 380;
|
||||
@@ -35,7 +36,7 @@ const CredentialDetails: React.FC = () => {
|
||||
const top = window.screen.height / 2 - height / 2;
|
||||
|
||||
window.open(
|
||||
`index.html?popup=true#/credentials/${id}`,
|
||||
`popup.html?expanded=true#/credentials/${id}`,
|
||||
'CredentialDetails',
|
||||
`width=${width},height=${height},left=${left},top=${top},popup=true`
|
||||
);
|
||||
@@ -72,8 +73,8 @@ const CredentialDetails: React.FC = () => {
|
||||
// For popup windows, ensure we have proper history state for navigation
|
||||
if (isPopup()) {
|
||||
// Clear existing history and create fresh entries
|
||||
window.history.replaceState({}, '', `index.html#/credentials`);
|
||||
window.history.pushState({}, '', `index.html#/credentials/${id}`);
|
||||
window.history.replaceState({}, '', `popup.html#/credentials`);
|
||||
window.history.pushState({}, '', `popup.html#/credentials/${id}`);
|
||||
}
|
||||
|
||||
if (!dbContext?.sqliteClient || !id) {
|
||||
@@ -100,7 +101,7 @@ const CredentialDetails: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
@@ -147,14 +148,14 @@ const CredentialDetails: React.FC = () => {
|
||||
{credential.Email && (
|
||||
<>
|
||||
{isEmailDomainSupported(credential.Email) && (
|
||||
<div className="mt-6">
|
||||
<EmailPreview
|
||||
email={credential.Email}
|
||||
/>
|
||||
</div>
|
||||
<EmailPreview
|
||||
email={credential.Email}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<TotpViewer credentialId={credential.Id} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6">
|
||||
@@ -1,14 +1,15 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import { Credential } from '../../shared/types/Credential';
|
||||
import { Credential } from '../../../utils/types/Credential';
|
||||
import { Buffer } from 'buffer';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
import { useWebApi } from '../context/WebApiContext';
|
||||
import { VaultResponse } from '../../shared/types/webapi/VaultResponse';
|
||||
import { VaultResponse } from '../../../utils/types/webapi/VaultResponse';
|
||||
import ReloadButton from '../components/ReloadButton';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import { useMinDurationLoading } from '../hooks/useMinDurationLoading';
|
||||
import { useMinDurationLoading } from '../../../hooks/useMinDurationLoading';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
/**
|
||||
* Credentials list page.
|
||||
@@ -64,7 +65,7 @@ const CredentialsList: React.FC = () => {
|
||||
}
|
||||
|
||||
// Get derived key from background worker
|
||||
const passwordHashBase64 = await chrome.runtime.sendMessage({ type: 'GET_DERIVED_KEY' });
|
||||
const passwordHashBase64 = await sendMessage('GET_DERIVED_KEY', {}, 'background') as string;
|
||||
|
||||
// Initialize the SQLite context again with the newly retrieved decrypted blob
|
||||
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Email } from '../../shared/types/webapi/Email';
|
||||
import { Email } from '../../../utils/types/webapi/Email';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import { useWebApi } from '../context/WebApiContext';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import { useMinDurationLoading } from '../hooks/useMinDurationLoading';
|
||||
import EncryptionUtility from '../../shared/EncryptionUtility';
|
||||
import { Attachment } from '../../shared/types/webapi/Attachment';
|
||||
import { useMinDurationLoading } from '../../../hooks/useMinDurationLoading';
|
||||
import EncryptionUtility from '../../../utils/EncryptionUtility';
|
||||
import { Attachment } from '../../../utils/types/webapi/Attachment';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
import ConversionUtility from '../utils/ConversionUtility';
|
||||
|
||||
@@ -36,8 +36,8 @@ const EmailDetails: React.FC = () => {
|
||||
// For popup windows, ensure we have proper history state for navigation
|
||||
if (isPopup()) {
|
||||
// Clear existing history and create fresh entries
|
||||
window.history.replaceState({}, '', `index.html#/emails`);
|
||||
window.history.pushState({}, '', `index.html#/emails/${id}`);
|
||||
window.history.replaceState({}, '', `popup.html#/emails`);
|
||||
window.history.pushState({}, '', `popup.html#/emails/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,15 +81,15 @@ const EmailDetails: React.FC = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the current page is a popup.
|
||||
* Check if the current page is an expanded popup.
|
||||
*/
|
||||
const isPopup = () : boolean => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('popup') === 'true';
|
||||
return urlParams.get('expanded') === 'true';
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the credential details in a new popup.
|
||||
* Open the credential details in a new expanded popup.
|
||||
*/
|
||||
const openInNewPopup = () : void => {
|
||||
const width = 800;
|
||||
@@ -98,7 +98,7 @@ const EmailDetails: React.FC = () => {
|
||||
const top = window.screen.height / 2 - height / 2;
|
||||
|
||||
window.open(
|
||||
`index.html?popup=true#/emails/${id}`,
|
||||
`popup.html?expanded=true#/emails/${id}`,
|
||||
'EmailDetails',
|
||||
`width=${width},height=${height},left=${left},top=${top},popup=true`
|
||||
);
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { MailboxBulkRequest, MailboxBulkResponse } from '../../shared/types/webapi/MailboxBulk';
|
||||
import { MailboxEmail } from '../../shared/types/webapi/MailboxEmail';
|
||||
import { MailboxBulkRequest, MailboxBulkResponse } from '../../../utils/types/webapi/MailboxBulk';
|
||||
import { MailboxEmail } from '../../../utils/types/webapi/MailboxEmail';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import { useWebApi } from '../context/WebApiContext';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import { useMinDurationLoading } from '../hooks/useMinDurationLoading';
|
||||
import EncryptionUtility from '../../shared/EncryptionUtility';
|
||||
import { useMinDurationLoading } from '../../../hooks/useMinDurationLoading';
|
||||
import EncryptionUtility from '../../../utils/EncryptionUtility';
|
||||
import ReloadButton from '../components/ReloadButton';
|
||||
import { Link } from 'react-router-dom';
|
||||
/**
|
||||
@@ -4,14 +4,14 @@ import { useDb } from '../context/DbContext';
|
||||
import { useWebApi } from '../context/WebApiContext';
|
||||
import { Buffer } from 'buffer';
|
||||
import Button from '../components/Button';
|
||||
import EncryptionUtility from '../../shared/EncryptionUtility';
|
||||
import EncryptionUtility from '../../../utils/EncryptionUtility';
|
||||
import SrpUtility from '../utils/SrpUtility';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
import { VaultResponse } from '../../shared/types/webapi/VaultResponse';
|
||||
import { LoginResponse } from '../../shared/types/webapi/Login';
|
||||
import { VaultResponse } from '../../../utils/types/webapi/VaultResponse';
|
||||
import { LoginResponse } from '../../../utils/types/webapi/Login';
|
||||
import LoginServerInfo from '../components/LoginServerInfo';
|
||||
import { AppInfo } from '../../shared/AppInfo';
|
||||
|
||||
import { AppInfo } from '../../../utils/AppInfo';
|
||||
import { storage } from 'wxt/storage';
|
||||
/**
|
||||
* Login page
|
||||
*/
|
||||
@@ -39,10 +39,10 @@ const Login: React.FC = () => {
|
||||
* Load the client URL from the storage.
|
||||
*/
|
||||
const loadClientUrl = async () : Promise<void> => {
|
||||
const setting = await chrome.storage.local.get(['clientUrl']);
|
||||
const settingClientUrl = await storage.getItem('local:clientUrl') as string;
|
||||
let clientUrl = AppInfo.DEFAULT_CLIENT_URL;
|
||||
if (setting.clientUrl && setting.clientUrl.length > 0) {
|
||||
clientUrl = setting.clientUrl;
|
||||
if (settingClientUrl && settingClientUrl.length > 0) {
|
||||
clientUrl = settingClientUrl;
|
||||
}
|
||||
|
||||
setClientUrl(clientUrl);
|
||||
@@ -1,5 +1,9 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { DISABLED_SITES_KEY, GLOBAL_POPUP_ENABLED_KEY } from '../../contentScript/Popup';
|
||||
import { AppInfo } from '../../../utils/AppInfo';
|
||||
import { storage } from "wxt/storage";
|
||||
import { browser } from 'wxt/browser';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
|
||||
/**
|
||||
* Popup settings type.
|
||||
@@ -15,6 +19,7 @@ type PopupSettings = {
|
||||
* Settings page component.
|
||||
*/
|
||||
const Settings: React.FC = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [settings, setSettings] = useState<PopupSettings>({
|
||||
disabledUrls: [],
|
||||
currentUrl: '',
|
||||
@@ -25,9 +30,9 @@ const Settings: React.FC = () => {
|
||||
/**
|
||||
* Get current tab in browser.
|
||||
*/
|
||||
const getCurrentTab = async () : Promise<chrome.tabs.Tab> => {
|
||||
const getCurrentTab = async (): Promise<browser.tabs.Tab> => {
|
||||
const queryOptions = { active: true, currentWindow: true };
|
||||
const [tab] = await chrome.tabs.query(queryOptions);
|
||||
const [tab] = await browser.tabs.query(queryOptions);
|
||||
return tab;
|
||||
};
|
||||
|
||||
@@ -38,17 +43,15 @@ const Settings: React.FC = () => {
|
||||
const tab = await getCurrentTab();
|
||||
const currentUrl = new URL(tab.url ?? '').hostname;
|
||||
|
||||
// Load settings from chrome.storage.local
|
||||
chrome.storage.local.get([DISABLED_SITES_KEY, GLOBAL_POPUP_ENABLED_KEY], (result) => {
|
||||
const disabledUrls = result[DISABLED_SITES_KEY] ?? [];
|
||||
const isGloballyEnabled = result[GLOBAL_POPUP_ENABLED_KEY] !== false; // Default to true if not set
|
||||
// Load settings local storage.
|
||||
const disabledUrls = await storage.getItem(DISABLED_SITES_KEY) as string[] ?? [];
|
||||
const isGloballyEnabled = await storage.getItem(GLOBAL_POPUP_ENABLED_KEY) !== false; // Default to true if not set
|
||||
|
||||
setSettings({
|
||||
disabledUrls,
|
||||
currentUrl,
|
||||
isEnabled: !disabledUrls.includes(currentUrl),
|
||||
isGloballyEnabled
|
||||
});
|
||||
setSettings({
|
||||
disabledUrls,
|
||||
currentUrl,
|
||||
isEnabled: !disabledUrls.includes(currentUrl),
|
||||
isGloballyEnabled,
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -69,8 +72,7 @@ const Settings: React.FC = () => {
|
||||
newDisabledUrls = newDisabledUrls.filter(url => url !== currentUrl);
|
||||
}
|
||||
|
||||
const storageData = { [DISABLED_SITES_KEY]: newDisabledUrls };
|
||||
await chrome.storage.local.set(storageData);
|
||||
await storage.setItem(DISABLED_SITES_KEY, newDisabledUrls);
|
||||
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
@@ -83,8 +85,7 @@ const Settings: React.FC = () => {
|
||||
* Reset settings.
|
||||
*/
|
||||
const resetSettings = async () : Promise<void> => {
|
||||
const storageData = { [DISABLED_SITES_KEY]: [] };
|
||||
await chrome.storage.local.set(storageData);
|
||||
await storage.setItem(DISABLED_SITES_KEY, []);
|
||||
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
@@ -99,9 +100,7 @@ const Settings: React.FC = () => {
|
||||
const toggleGlobalPopup = async () : Promise<void> => {
|
||||
const newGloballyEnabled = !settings.isGloballyEnabled;
|
||||
|
||||
await chrome.storage.local.set({
|
||||
[GLOBAL_POPUP_ENABLED_KEY]: newGloballyEnabled
|
||||
});
|
||||
await storage.setItem(GLOBAL_POPUP_ENABLED_KEY, newGloballyEnabled);
|
||||
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
@@ -109,6 +108,20 @@ const Settings: React.FC = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Set theme preference.
|
||||
*/
|
||||
const setThemePreference = async (newTheme: 'system' | 'light' | 'dark') : Promise<void> => {
|
||||
// Use the ThemeContext to apply the theme
|
||||
setTheme(newTheme);
|
||||
|
||||
// Update local state
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
theme: newTheme
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
@@ -131,11 +144,11 @@ const Settings: React.FC = () => {
|
||||
onClick={toggleGlobalPopup}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
settings.isGloballyEnabled
|
||||
? 'bg-red-500 hover:bg-red-600 text-white'
|
||||
: 'bg-green-500 hover:bg-green-600 text-white'
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isGloballyEnabled ? 'Disable' : 'Enable'}
|
||||
{settings.isGloballyEnabled ? 'Enabled' : 'Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,18 +164,18 @@ const Settings: React.FC = () => {
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">Open popup on: {settings.currentUrl}</p>
|
||||
<p className={`text-sm mt-1 ${settings.isEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{settings.isEnabled ? 'Popup is active' : 'Popup is disabled'}
|
||||
{settings.isEnabled ? 'Enabled for this site' : 'Disabled for this site'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleCurrentSite}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
settings.isEnabled
|
||||
? 'bg-red-500 hover:bg-red-600 text-white'
|
||||
: 'bg-green-500 hover:bg-green-600 text-white'
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isEnabled ? 'Disable' : 'Enable'}
|
||||
{settings.isEnabled ? 'Enabled' : 'Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -177,6 +190,57 @@ const Settings: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Appearance Settings Section */}
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">Appearance</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">Theme</p>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="system"
|
||||
checked={theme === 'system'}
|
||||
onChange={() => setThemePreference('system')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Use default</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="light"
|
||||
checked={theme === 'light'}
|
||||
onChange={() => setThemePreference('light')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Light</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="dark"
|
||||
checked={theme === 'dark'}
|
||||
onChange={() => setThemePreference('dark')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Dark</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="text-center text-gray-400 dark:text-gray-600">
|
||||
Version: {AppInfo.VERSION}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,10 +4,13 @@ import { useAuth } from '../context/AuthContext';
|
||||
import { useWebApi } from '../context/WebApiContext';
|
||||
import { Buffer } from 'buffer';
|
||||
import Button from '../components/Button';
|
||||
import EncryptionUtility from '../../shared/EncryptionUtility';
|
||||
import EncryptionUtility from '../../../utils/EncryptionUtility';
|
||||
import SrpUtility from '../utils/SrpUtility';
|
||||
import { VaultResponse } from '../../shared/types/webapi/VaultResponse';
|
||||
import { VaultResponse } from '../../../utils/types/webapi/VaultResponse';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/entrypoints/contentScript/Popup';
|
||||
import { storage } from 'wxt/storage';
|
||||
|
||||
/**
|
||||
* Unlock page
|
||||
@@ -15,6 +18,7 @@ import { useLoading } from '../context/LoadingContext';
|
||||
const Unlock: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const webApi = useWebApi();
|
||||
const srpUtil = new SrpUtility(webApi);
|
||||
@@ -73,6 +77,9 @@ const Unlock: React.FC = () => {
|
||||
|
||||
// Initialize the SQLite context with the new vault data.
|
||||
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
|
||||
// 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);
|
||||
} catch (err) {
|
||||
setError('Failed to unlock vault. Please check your password and try again.');
|
||||
console.error('Unlock error:', err);
|
||||
@@ -81,6 +88,13 @@ const Unlock: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logout
|
||||
*/
|
||||
const handleLogout = () : void => {
|
||||
navigate('/logout', { replace: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md">
|
||||
<form onSubmit={handleSubmit} className="bg-white dark:bg-gray-700 w-full shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
@@ -116,7 +130,7 @@ const Unlock: React.FC = () => {
|
||||
</Button>
|
||||
|
||||
<div className="text-sm font-medium text-gray-500 dark:text-gray-200 mt-6">
|
||||
Switch accounts? <a href="/logout" className="text-primary-700 hover:underline dark:text-primary-500">Log out</a>
|
||||
Switch accounts? <button onClick={handleLogout} className="text-primary-700 hover:underline dark:text-primary-500">Log out</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
3
browser-extension/src/entrypoints/popup/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
font-size: 75%;
|
||||
}
|
||||
@@ -21,9 +21,7 @@ class ConversionUtility {
|
||||
if (anchors.length > 0) {
|
||||
anchors.forEach((anchor: Element) => {
|
||||
// Handle target attribute
|
||||
if (!anchor.hasAttribute('target')) {
|
||||
anchor.setAttribute('target', '_blank');
|
||||
} else if (anchor.getAttribute('target') !== '_blank') {
|
||||
if (!anchor.hasAttribute('target') || anchor.getAttribute('target') !== '_blank') {
|
||||
anchor.setAttribute('target', '_blank');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import srp from 'secure-remote-password/client'
|
||||
import { WebApiService } from '../../shared/WebApiService';
|
||||
import { LoginRequest, LoginResponse } from '../../shared/types/webapi/Login';
|
||||
import { ValidateLoginRequest, ValidateLoginRequest2Fa, ValidateLoginResponse } from '../../shared/types/webapi/ValidateLogin';
|
||||
import { WebApiService } from '../../../utils/WebApiService';
|
||||
import { LoginRequest, LoginResponse } from '../../../utils/types/webapi/Login';
|
||||
import { ValidateLoginRequest, ValidateLoginRequest2Fa, ValidateLoginResponse } from '../../../utils/types/webapi/ValidateLogin';
|
||||
|
||||
/**
|
||||
* Utility class for SRP authentication operations.
|
||||
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
BIN
browser-extension/src/public/icon/512.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
@@ -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.12.3';
|
||||
public static readonly VERSION = '0.14.0';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault server (API) version. If the server version is below this, the
|
||||
@@ -19,11 +19,32 @@ export class AppInfo {
|
||||
*/
|
||||
public static readonly MIN_VAULT_VERSION = '1.4.1';
|
||||
|
||||
/*
|
||||
/**
|
||||
* The client name to use in the X-AliasVault-Client header.
|
||||
* TODO: make this configurable when adding other browser support (e.g. Firefox).
|
||||
* Detects the specific browser being used.
|
||||
*/
|
||||
public static readonly CLIENT_NAME = 'chrome';
|
||||
public static readonly CLIENT_NAME = (() : 'chrome' | 'firefox' | 'edge' | 'safari' | 'browser' => {
|
||||
// This uses the WXT environment variables to detect the specific browser being used.
|
||||
const env = import.meta.env;
|
||||
|
||||
if (env.FIREFOX) {
|
||||
return 'firefox';
|
||||
}
|
||||
|
||||
if (env.CHROME) {
|
||||
return 'chrome';
|
||||
}
|
||||
|
||||
if (env.EDGE) {
|
||||
return 'edge';
|
||||
}
|
||||
|
||||
if (env.SAFARI) {
|
||||
return 'safari';
|
||||
}
|
||||
|
||||
return 'browser';
|
||||
})();
|
||||
|
||||
/**
|
||||
* The default AliasVault client URL.
|
||||