Compare commits
41 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 |
24
.github/workflows/browser-extension-build.yml
vendored
@@ -174,7 +174,12 @@ jobs:
|
||||
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
|
||||
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
|
||||
@@ -191,13 +196,19 @@ jobs:
|
||||
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
|
||||
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/aliasvault-browser-extension-*-sources.zip
|
||||
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
|
||||
@@ -214,7 +225,12 @@ jobs:
|
||||
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
|
||||
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
|
||||
|
||||
16
README.md
@@ -10,7 +10,7 @@
|
||||
> 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.
|
||||
|
||||
## 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://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.
|
||||
@@ -67,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.13.0/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
|
||||
@@ -105,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)
|
||||
@@ -117,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.
|
||||
|
||||
@@ -18,4 +18,8 @@ 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.
|
||||
```
|
||||
|
||||
34
browser-extension/package-lock.json
generated
@@ -9,9 +9,11 @@
|
||||
"version": "0.0.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^22.13.10",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"buffer": "^6.0.3",
|
||||
"globals": "^16.0.0",
|
||||
"otpauth": "^9.3.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.4",
|
||||
@@ -1473,6 +1475,18 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz",
|
||||
"integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -2056,10 +2070,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
|
||||
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
|
||||
"devOptional": true,
|
||||
"version": "22.13.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
|
||||
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
@@ -9114,6 +9127,18 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/otpauth": {
|
||||
"version": "9.3.6",
|
||||
"resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.3.6.tgz",
|
||||
"integrity": "sha512-eIcCvuEvcAAPHxUKC9Q4uCe0Fh/yRc5jv9z+f/kvyIF2LPrhgAOuLB7J9CssGYhND/BL8M9hlHBTFmffpoQlMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.6.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/hectorm/otpauth?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/own-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||
@@ -12223,7 +12248,6 @@
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unimport": {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"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",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"lint": "eslint src",
|
||||
@@ -24,9 +25,11 @@
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^22.13.10",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"buffer": "^6.0.3",
|
||||
"globals": "^16.0.0",
|
||||
"otpauth": "^9.3.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.4",
|
||||
|
||||
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>
|
||||
|
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
|
||||
@@ -2,7 +2,7 @@ import { browser } from "wxt/browser";
|
||||
import { defineBackground } from 'wxt/sandbox';
|
||||
import { onMessage } from "webext-bridge/background";
|
||||
import { setupContextMenus, handleContextMenuClick } from './background/ContextMenu';
|
||||
import { handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDerivedKey, handleGetVault, handleStoreVault, handleSyncVault } from './background/VaultMessageHandler';
|
||||
import { handleCheckAuthStatus, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDerivedKey, handleGetVault, handleStoreVault, handleSyncVault } from './background/VaultMessageHandler';
|
||||
import { handleOpenPopup, handlePopupWithCredential } from './background/PopupMessageHandler';
|
||||
|
||||
export default defineBackground({
|
||||
@@ -17,6 +17,7 @@ export default defineBackground({
|
||||
);
|
||||
|
||||
// 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());
|
||||
|
||||
@@ -11,6 +11,23 @@ import { VaultResponse as messageVaultResponse } from '../../utils/types/messagi
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -1,83 +1,103 @@
|
||||
import './contentScript/style.css';
|
||||
import { FormDetector } from '../utils/formDetector/FormDetector';
|
||||
import { isAutoShowPopupDisabled, openAutofillPopup, removeExistingPopup } from './contentScript/Popup';
|
||||
import { canShowPopup, injectIcon } from './contentScript/Form';
|
||||
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.
|
||||
*/
|
||||
main(ctx) {
|
||||
async main(ctx) {
|
||||
if (ctx.isInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for input field focus
|
||||
document.addEventListener('focusin', async (e) => {
|
||||
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'];
|
||||
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 (target.tagName === 'INPUT' && textInputTypes.includes(target.type) && !target.dataset.aliasvaultIgnore) {
|
||||
const formDetector = new FormDetector(document, target);
|
||||
if (!formDetector.containsLoginForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formDetector.containsLoginForm()) {
|
||||
return;
|
||||
}
|
||||
injectIcon(target, container);
|
||||
|
||||
injectIcon(target);
|
||||
// Only show popup if its enabled and debounce time has passed.
|
||||
if (await isAutoShowPopupEnabled() && popupDebounceTimeHasPassed()) {
|
||||
openAutofillPopup(target, container);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isDisabled = await isAutoShowPopupDisabled();
|
||||
const canShow = canShowPopup();
|
||||
// Listen for input field focus in the main document
|
||||
document.addEventListener('focusin', handleFocusIn);
|
||||
|
||||
// Only show popup if it's not disabled and the popup can be shown
|
||||
if (!isDisabled && canShow) {
|
||||
openAutofillPopup(target);
|
||||
}
|
||||
}
|
||||
// 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 };
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Listen for popstate events (back/forward navigation)
|
||||
window.addEventListener('popstate', () => {
|
||||
if (ctx.isInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeExistingPopup();
|
||||
});
|
||||
|
||||
// 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);
|
||||
openAutofillPopup(target);
|
||||
return { success: true };
|
||||
});
|
||||
// Mount the UI to create the shadow root
|
||||
ui.autoMount();
|
||||
},
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { FormDetector } from "../../utils/formDetector/FormDetector";
|
||||
import { FormFiller } from "../../utils/formDetector/FormFiller";
|
||||
import { Credential } from "../../utils/types/Credential";
|
||||
import { openAutofillPopup } from "./Popup";
|
||||
|
||||
/**
|
||||
* Global timestamp to track popup debounce time.
|
||||
* This is used to not show the popup again for a specific amount of time.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -53,7 +54,7 @@ export function fillCredential(credential: Credential, input: HTMLInputElement)
|
||||
/**
|
||||
* 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"/>
|
||||
@@ -64,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>
|
||||
`;
|
||||
@@ -86,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
|
||||
@@ -131,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
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Check if the current theme is dark.
|
||||
*/
|
||||
export function isDarkMode(): boolean {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
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); }
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -135,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' : ''
|
||||
}`}
|
||||
>
|
||||
@@ -152,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' : ''
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -95,7 +95,10 @@ const Header: React.FC<HeaderProps> = ({
|
||||
>
|
||||
<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>
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import React, { createContext, useContext, useState, useEffect, useMemo, useCall
|
||||
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;
|
||||
@@ -66,6 +67,9 @@ 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);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -6,14 +6,6 @@
|
||||
<title>AliasVault</title>
|
||||
<link href="~/assets/tailwind.css" rel="stylesheet" />
|
||||
<meta name="manifest.type" content="browser_action" />
|
||||
<script>
|
||||
// Check if expanded=true is in the URL, which means the popup was opened in expanded mode with unlimited width.
|
||||
// If not, set the width to 350px to force the default popup to a fixed width.
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (!urlParams.get('expanded')) {
|
||||
document.documentElement.classList.add('max-w-[350px]');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-white dark:bg-gray-900">
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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;
|
||||
@@ -19,6 +20,7 @@ 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(() => {
|
||||
/**
|
||||
@@ -27,6 +29,15 @@ const AuthSettings: React.FC = () => {
|
||||
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) {
|
||||
@@ -74,6 +85,23 @@ const AuthSettings: React.FC = () => {
|
||||
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 (
|
||||
<div className="p-4">
|
||||
<div className="mb-6">
|
||||
@@ -124,6 +152,23 @@ const AuthSettings: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
|
||||
@@ -5,6 +5,7 @@ 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';
|
||||
|
||||
/**
|
||||
@@ -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">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DISABLED_SITES_KEY, GLOBAL_POPUP_ENABLED_KEY } from '../../contentScrip
|
||||
import { AppInfo } from '../../../utils/AppInfo';
|
||||
import { storage } from "wxt/storage";
|
||||
import { browser } from 'wxt/browser';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
|
||||
/**
|
||||
* Popup settings type.
|
||||
@@ -18,6 +19,7 @@ type PopupSettings = {
|
||||
* Settings page component.
|
||||
*/
|
||||
const Settings: React.FC = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [settings, setSettings] = useState<PopupSettings>({
|
||||
disabledUrls: [],
|
||||
currentUrl: '',
|
||||
@@ -49,7 +51,7 @@ const Settings: React.FC = () => {
|
||||
disabledUrls,
|
||||
currentUrl,
|
||||
isEnabled: !disabledUrls.includes(currentUrl),
|
||||
isGloballyEnabled
|
||||
isGloballyEnabled,
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -106,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">
|
||||
@@ -128,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>
|
||||
@@ -148,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>
|
||||
|
||||
@@ -175,6 +191,53 @@ const Settings: React.FC = () => {
|
||||
</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>
|
||||
|
||||
@@ -8,6 +8,9 @@ import EncryptionUtility from '../../../utils/EncryptionUtility';
|
||||
import SrpUtility from '../utils/SrpUtility';
|
||||
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>
|
||||
|
||||
@@ -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.13.0';
|
||||
public static readonly VERSION = '0.14.0';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault server (API) version. If the server version is below this, the
|
||||
|
||||
20
browser-extension/src/utils/ExpandedMode.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Setup the expanded mode.
|
||||
*/
|
||||
export function setupExpandedMode() : void {
|
||||
/**
|
||||
* This runs once when imported and checks if the popup was opened in expanded mode with unlimited width.
|
||||
* If not, it sets the width to 350px to force the default popup to a fixed width.
|
||||
* This is used to ensure the popup is always a fixed width, even if some content like email preview
|
||||
* is too wide to fit in the default width. Some browsers like Firefox and Safari will then try to
|
||||
* expand the popup to the width of the content, which can cause the popup to become too wide and bad UX.
|
||||
*
|
||||
* You can test this by opening the popup and then clicking on the email preview. If the popup width does
|
||||
* not change, it works. Then if you expand/popout the extension, the content of the page should adjust
|
||||
* to the new width of the resizable popup.
|
||||
*/
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (!urlParams.get('expanded')) {
|
||||
document.documentElement.classList.add('max-w-[350px]');
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import initSqlJs, { Database } from 'sql.js';
|
||||
import { Credential } from './types/Credential';
|
||||
import { EncryptionKey } from './types/EncryptionKey';
|
||||
import { TotpCode } from './types/TotpCode';
|
||||
|
||||
/**
|
||||
* Client for interacting with the SQLite database.
|
||||
@@ -421,6 +422,66 @@ class SqliteClient {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TOTP codes for a credential
|
||||
* @param credentialId - The ID of the credential to get TOTP codes for
|
||||
* @returns Array of TotpCode objects
|
||||
*/
|
||||
public getTotpCodesForCredential(credentialId: string): TotpCode[] {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
/*
|
||||
* Check if TotpCodes table exists (for backward compatibility).
|
||||
* TODO: whenever the browser extension has a minimum client DB version of 1.5.0+,
|
||||
* we can remove this check as the TotpCodes table then is guaranteed to exist.
|
||||
*/
|
||||
if (!this.tableExists('TotpCodes')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
Id,
|
||||
Name,
|
||||
SecretKey,
|
||||
CredentialId
|
||||
FROM TotpCodes
|
||||
WHERE CredentialId = ? AND IsDeleted = 0`;
|
||||
|
||||
return this.executeQuery<TotpCode>(query, [credentialId]);
|
||||
} catch (error) {
|
||||
console.error('Error getting TOTP codes:', error);
|
||||
// Return empty array instead of throwing to be robust
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a table exists in the database
|
||||
* @param tableName - The name of the table to check
|
||||
* @returns True if the table exists, false otherwise
|
||||
*/
|
||||
private tableExists(tableName: string): boolean {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name=?`;
|
||||
|
||||
const results = this.executeQuery(query, [tableName]);
|
||||
return results.length > 0;
|
||||
} catch (error) {
|
||||
console.error(`Error checking if table ${tableName} exists:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SqliteClient;
|
||||
@@ -35,12 +35,12 @@ export class FormFiller {
|
||||
}
|
||||
|
||||
if (this.form.passwordField) {
|
||||
this.form.passwordField.value = credential.Password;
|
||||
this.fillPasswordField(this.form.passwordField, credential.Password);
|
||||
this.triggerInputEvents(this.form.passwordField);
|
||||
}
|
||||
|
||||
if (this.form.passwordConfirmField) {
|
||||
this.form.passwordConfirmField.value = credential.Password;
|
||||
this.fillPasswordField(this.form.passwordConfirmField, credential.Password);
|
||||
this.triggerInputEvents(this.form.passwordConfirmField);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,26 @@ export class FormFiller {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the password field with the given password. This uses a small delay between each character to simulate human typing.
|
||||
* In the past there have been issues where Microsoft 365 login forms would clear the password field when just setting the value directly.
|
||||
*
|
||||
* @param field The password field to fill.
|
||||
* @param password The password to fill the field with.
|
||||
*/
|
||||
private async fillPasswordField(field: HTMLInputElement, password: string): Promise<void> {
|
||||
// Clear the field first
|
||||
field.value = '';
|
||||
this.triggerInputEvents(field);
|
||||
|
||||
// Type each character with a small delay
|
||||
for (let i = 0; i < password.length; i++) {
|
||||
field.value = password.substring(0, i + 1);
|
||||
// Small random delay between 5-15ms to simulate human typing
|
||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 10 + 5));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the birthdate fields of the form.
|
||||
* @param credential The credential to fill the form with.
|
||||
|
||||
@@ -44,11 +44,14 @@ describe('FormFiller', () => {
|
||||
expect(wasTriggerCalledFor(mockTriggerInputEvents, formFields.emailConfirmField)).toBe(true);
|
||||
});
|
||||
|
||||
it('should fill password and confirmation fields', () => {
|
||||
it('should fill password and confirmation fields', async () => {
|
||||
formFields.passwordConfirmField = document.createElement('input');
|
||||
|
||||
formFiller.fillFields(mockCredential);
|
||||
|
||||
// Delay for 150ms to ensure the password field is filled as it uses a small delay between each character.
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
|
||||
expect(formFields.passwordField?.value).toBe('testpass');
|
||||
expect(formFields.passwordConfirmField?.value).toBe('testpass');
|
||||
expect(wasTriggerCalledFor(mockTriggerInputEvents, formFields.passwordField)).toBe(true);
|
||||
|
||||
16
browser-extension/src/utils/types/TotpCode.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* TotpCode SQLite database type.
|
||||
*/
|
||||
export type TotpCode = {
|
||||
/** The ID of the TOTP code */
|
||||
Id: string;
|
||||
|
||||
/** The name of the TOTP code */
|
||||
Name: string;
|
||||
|
||||
/** The secret key for the TOTP code */
|
||||
SecretKey: string;
|
||||
|
||||
/** The credential ID this TOTP code belongs to */
|
||||
CredentialId: string;
|
||||
};
|
||||
@@ -1,10 +1,9 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./src/entrypoints/*.{js,jsx,ts,tsx}",
|
||||
"./src/entrypoints/**/*.{js,jsx,ts,tsx}",
|
||||
"./src/entrypoints/**/*/*.{html,js}"
|
||||
"./src/**/*.{js,jsx,ts,tsx,html}"
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
|
||||
@@ -7,7 +7,7 @@ export default defineConfig({
|
||||
manifest: {
|
||||
name: "AliasVault",
|
||||
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
|
||||
version: "0.13.0",
|
||||
version: "0.14.0",
|
||||
content_security_policy: {
|
||||
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ In order to install the Firefox Extension, see the options below.
|
||||
## Firefox Add-ons
|
||||
Installing the extension from the Firefox Add-ons is the easiest way to get started. This ensures that you are always using the latest version of the extension.
|
||||
|
||||
1. Go to the (TODO: add Firefox Add-ons link)
|
||||
1. Go to the [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/aliasvault/) page
|
||||
2. Click on the "Add to Firefox" button
|
||||
3. The extension will be installed and added to your browser
|
||||
|
||||
|
||||
53
docs/browser-extensions/safari/build-from-source.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
layout: default
|
||||
title: Build from Source
|
||||
parent: Safari
|
||||
grand_parent: Browser Extensions
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
# Building Safari Extension from Source
|
||||
|
||||
This guide explains how to build and install the AliasVault Safari extension from source code.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js installed on your computer
|
||||
- Git to clone the repository (optional)
|
||||
- MacOS machine with Xcode installed
|
||||
|
||||
## Building the Safari Extension
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/lanedirt/AliasVault.git
|
||||
```
|
||||
|
||||
2. Navigate to the Browser Extension directory:
|
||||
```bash
|
||||
cd AliasVault/browser-extension
|
||||
```
|
||||
|
||||
3. Install the required dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
4. Build the extension:
|
||||
```bash
|
||||
npm run build:safari
|
||||
```
|
||||
|
||||
5. Open Xcode and open the `browser-extension/safari-xcode/AliasVault/AliasVault.xcodeproj` file
|
||||
|
||||
6. Run the project. This will open up the AliasVault MacOS wrapper app and automatically install the extension to your Safari Extensions list.
|
||||
|
||||
## Installing and enabling the extension in Safari
|
||||
|
||||
1. Open Safari and go to menu > Safari > Settings
|
||||
2. Click on the "Extensions" tab
|
||||
3. Enable the AliasVault extension. If the extension is not visible, then you may need to enable developer mode in Safari settings first to allow unsigned extensions to run.
|
||||
|
||||
## Development Mode (Optional)
|
||||
|
||||
If you plan to modify the extension code, see the [browser-extensions](../../misc/dev/browser-extensions.md) developer documentation for more information.
|
||||
19
docs/browser-extensions/safari/index.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
layout: default
|
||||
title: Safari
|
||||
parent: Browser Extensions
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
# Safari Extension
|
||||
In order to install the Safari Extension, see the options below.
|
||||
|
||||
## MacOS App Store
|
||||
Installing the extension from the MacOS App Store is the easiest way to get started. This ensures that you are always using the latest version of the extension.
|
||||
|
||||
1. Go to the [MacOS App Store](https://apps.apple.com/app/id6743163173) page
|
||||
2. Click on the "Get" button
|
||||
3. The extension will be installed and added to Safari. Follow the instructions in the AliasVault MacOS app that is shown on screen after installation.
|
||||
|
||||
## Build from Source
|
||||
If you wish to install the extension from source instead, see the [build-from-source](build-from-source.md) documentation. This will allow you to make changes to the extension and/or to use a specific version of the extension.
|
||||
@@ -72,6 +72,9 @@ and then in the prompt choose option 2.
|
||||
|
||||
AliasVault includes a built-in email server that can handle multiple custom domains for your aliases.
|
||||
|
||||
{: .note }
|
||||
Please be aware that if you skip this step, AliasVault will default to use public email domains offered by SpamOK. While this will still work for creating email aliases, it has privacy limitations. For complete privacy and control, we recommend following the setup steps below to use your own private domain. [Learn more about the differences between private and public email domains](../misc/private-vs-public-email.md).
|
||||
|
||||
To set up the email server, you need the following:
|
||||
- Public IPv4 address
|
||||
- Open ports (25 and 587) in server firewall for SMTP traffic
|
||||
|
||||
@@ -76,6 +76,17 @@ npm run build:edge
|
||||
- Enable "Developer mode" in the top right corner
|
||||
- Click "Load unpacked" and the folder `./browser-extension/dist/edge-mv3`
|
||||
|
||||
### Safari
|
||||
|
||||
1. Build the extension:
|
||||
```bash
|
||||
npm run build:safari
|
||||
```
|
||||
|
||||
2. Open the Xcode project in the `safari-xcode/AliasVault/AliasVault.xcodeproj` folder and build / run the app.
|
||||
|
||||
3. The extension will be installed automatically in Safari. Follow the on-screen MacOS app instructions to complete the installation.
|
||||
|
||||
## Automatic tests
|
||||
The extension has a suite of automatic tests that are run on every pull request. These tests are located in the `__tests__` directories scattered throughout the browser extension codebase.
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
layout: default
|
||||
title: Development
|
||||
parent: Miscellaneous
|
||||
nav_order: 5
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
56
docs/misc/private-vs-public-email.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
layout: default
|
||||
title: Private vs Public Email Domains
|
||||
parent: Miscellaneous
|
||||
nav_order: 3
|
||||
---
|
||||
|
||||
# Private vs Public Email Domains
|
||||
AliasVault offers two types of email domains: private and public.
|
||||
|
||||
## Private Email Domains
|
||||
Private email domains come in two forms:
|
||||
|
||||
1. For the official cloud-hosted AliasVault service, users get access to the aliasvault.net domain, which is a private domain managed by AliasVault.
|
||||
|
||||
2. For self-hosted installations, private domains are domains that you control and configure yourself to connect to your AliasVault server instance.
|
||||
|
||||
In both cases, private domains are directly connected to the AliasVault server infrastructure. Any email aliases created using these domains benefit from full end-to-end encryption - emails are encrypted with the receiver's public/private key pair before they are stored on the AliasVault server. These emails can only be decrypted by the receiver's private key that is stored securely in the user's vault. This ensures that no one can read your emails except for you.
|
||||
|
||||
---
|
||||
|
||||
## Public Email Domains
|
||||
For convenience, AliasVault also offers public email domains which are provided through an integration with [SpamOK.com](https://spamok.com), a free service operated by Lanedirt (the author of AliasVault). These domains are suitable for testing and non-critical email aliases and offer convenience for self-hosted users who cannot set up their own private domains.
|
||||
|
||||
## Available Domains
|
||||
The following public email domains are currently available through SpamOK:
|
||||
- spamok.com
|
||||
- solarflarecorp.com
|
||||
- spamok.nl
|
||||
- 3060.nl
|
||||
- landmail.nl
|
||||
- asdasd.nl
|
||||
- spamok.de
|
||||
- spamok.com.ua
|
||||
- spamok.es
|
||||
- spamok.fr
|
||||
|
||||
## Important Disclaimers
|
||||
Public email domains do have limitations, please be aware of them:
|
||||
|
||||
1. **Public Nature**: These are fully public domains - anyone can access any email address as long as they know the name of the alias. The benefit is that this makes these domains fully anonymous because usage cannot be tied back to a specific user. But this also means that there is no privacy guarantee, as your emails can be read by anyone who knows the email address.
|
||||
|
||||
2. **No Service Level Agreement**: SpamOK is provided as a free service without any SLA or warranty. Email delivery and service availability are not guaranteed and can be interrupted at any time without notice.
|
||||
|
||||
### When to Use SpamOK Domains
|
||||
SpamOK domains are suitable for:
|
||||
- Testing AliasVault functionality
|
||||
- Non-critical email aliases
|
||||
- Temporary or disposable email needs
|
||||
|
||||
### When to Set Up Your Own Email Server
|
||||
Consider setting up your own email server if you need:
|
||||
- Complete control over your email domains
|
||||
- Private email addresses where all incoming emails are encrypted before being stored on the AliasVault server. No one can read your emails except for you.
|
||||
- Guaranteed service availability
|
||||
- Professional or business use
|
||||
@@ -18,6 +18,7 @@ Follow the steps in the checklist below to prepare a new release.
|
||||
|
||||
## Versioning browser extension
|
||||
- [ ] Update ./browser-extension/wxt.config.ts with the new version for the extension. This will be shown in the browser extension web stores. This version should be equal to the git release tag.
|
||||
- [ ] Update the version `MARKETING_VERSION` and increase the build number `CURRENT_PROJECT_VERSION` in the ./browser-extension/safari-xcode/AliasVault/AliasVault.xcodeproj project file in MacOS Xcode. This is the version that will be shown in the Safari Browser Extension App Store.
|
||||
- [ ] Update ./browser-extension/src/utils/AppInfo.ts with the new version for the extension. This version should be equal to the git release tag.
|
||||
- [ ] Update ./browser-extension/src/utils/AppInfo.ts with the minimum supported server version (in case of required API breaking changes).
|
||||
- [ ] Update ./browser-extension/src/shared/AppInfo.ts with the minimum supported client vault version (in case of required client vault model changes).
|
||||
@@ -56,3 +57,4 @@ The GitHub Actions workflow `Browser Extension Build` will build the browser ext
|
||||
2. Upload the Chrome archive to the Chrome Web Store.
|
||||
3. Upload the Firefox archive (normal + sources) to the Firefox Add-ons page.
|
||||
4. Upload the Edge archive to the Microsoft Edge Add-ons page.
|
||||
5. Submit the Safari extension to Apple for review by opening the `browser-extension/safari-xcode/AliasVault/AliasVault.xcodeproj` project in Xcode and submitting the extension via the "Distribute App" option.
|
||||
@@ -23,8 +23,6 @@
|
||||
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.6.0" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.6.0" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
<ProjectReference Include="..\Utilities\Cryptography\AliasVault.Cryptography.Client\AliasVault.Cryptography.Client.csproj" />
|
||||
<ProjectReference Include="..\Utilities\AliasVault.CsvImportExport\AliasVault.CsvImportExport.csproj" />
|
||||
<ProjectReference Include="..\Utilities\AliasVault.FaviconExtractor\AliasVault.FaviconExtractor.csproj" />
|
||||
<ProjectReference Include="..\Utilities\AliasVault.TotpGenerator\AliasVault.TotpGenerator.csproj" />
|
||||
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
|
||||
<ServiceWorker Include="wwwroot\service-worker.published.js">
|
||||
<PublishedContent>wwwroot/service-worker.published.js</PublishedContent>
|
||||
|
||||
@@ -100,8 +100,12 @@
|
||||
if (OriginalAttachmentsIds.Contains(attachment.Id))
|
||||
{
|
||||
// If it was part of the original set, we soft delete it.
|
||||
attachment.IsDeleted = true;
|
||||
attachment.UpdatedAt = DateTime.UtcNow;
|
||||
var attachmentToDelete = Attachments.FirstOrDefault(a => a.Id == attachment.Id);
|
||||
if (attachmentToDelete is not null)
|
||||
{
|
||||
attachmentToDelete.IsDeleted = true;
|
||||
attachmentToDelete.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
}
|
||||
else if (MailboxEmails.Count == 0)
|
||||
{
|
||||
<div class="text-gray-900 dark:text-white">No emails received (yet).</div>
|
||||
<div class="text-gray-500 dark:text-gray-400">No emails received (yet).</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
244
src/AliasVault.Client/Main/Components/TotpCodes/TotpCodes.razor
Normal file
@@ -0,0 +1,244 @@
|
||||
@inherits ComponentBase
|
||||
@inject GlobalNotificationService GlobalNotificationService
|
||||
@inject ConfirmModalService ConfirmModalService
|
||||
@using AliasVault.RazorComponents.Services
|
||||
@using TotpGenerator
|
||||
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Two-factor authentication</h3>
|
||||
</div>
|
||||
@if (TotpCodeList.Any(t => !t.IsDeleted) && !IsAddFormVisible)
|
||||
{
|
||||
<div>
|
||||
<button id="add-totp-code" @onclick="ShowAddForm" type="button" class="text-blue-700 hover:text-white border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-3 py-2 text-center dark:border-blue-500 dark:text-blue-500 dark:hover:text-white dark:hover:bg-blue-600 dark:focus:ring-blue-800">
|
||||
Add TOTP Code
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if ((TotpCodeList.Count == 0 || TotpCodeList.All(t => t.IsDeleted)) && !IsAddFormVisible)
|
||||
{
|
||||
<div class="flex flex-col justify-center">
|
||||
<p class="text-gray-500 dark:text-gray-400"><a @onclick="ShowAddForm" id="add-totp-code" href="javascript:void(0)" class="text-blue-600 hover:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400">Add a two-factor authenticator code</a></p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (IsAddFormVisible)
|
||||
{
|
||||
<div class="p-4 mb-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<EditForm Model="@NewTotpCode" OnValidSubmit="AddTotpCode">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h4 class="text-lg font-medium text-gray-900 dark:text-white">Add 2FA TOTP Code</h4>
|
||||
<button @onclick="HideAddForm" type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white">
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
||||
</svg>
|
||||
<span class="sr-only">Close form</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">If the website offers or requires 2FA for your account (such as Google Authenticator), you can use AliasVault instead to generate the codes for you.</p>
|
||||
<div class="mb-4">
|
||||
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Name (optional)</label>
|
||||
<InputText id="name" @bind-Value="NewTotpCode.Name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" />
|
||||
<ValidationMessage For="@(() => NewTotpCode.Name)" class="text-red-600 dark:text-red-400 text-sm mt-1" />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="secretKey" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key</label>
|
||||
<InputText id="secretKey" @bind-Value="NewTotpCode.SecretKey" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" placeholder="Enter secret key (manual entry)" />
|
||||
<ValidationMessage For="@(() => NewTotpCode.SecretKey)" class="text-red-600 dark:text-red-400 text-sm mt-1" />
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button id="save-totp-code" type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 mt-4">
|
||||
@foreach (var totpCode in TotpCodeList.Where(t => !t.IsDeleted))
|
||||
{
|
||||
<div class="p-2 ps-3 pe-3 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center flex-1">
|
||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white">@totpCode.Name</h4>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-col items-end">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Save to view code</div>
|
||||
</div>
|
||||
<button type="button" @onclick="() => DeleteTotpCode(totpCode)" class="delete-totp-code text-red-600 hover:text-red-800 dark:text-red-500 dark:hover:text-red-400">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The list of TOTP codes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public List<TotpCode> TotpCodeList { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Event callback for when the TOTP codes list changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<List<TotpCode>> TotpCodesChanged { get; set; }
|
||||
|
||||
private bool IsAddFormVisible { get; set; } = false;
|
||||
private TotpCodeEdit NewTotpCode { get; set; } = new();
|
||||
private List<Guid> OriginalTotpCodeIds { get; set; } = [];
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
|
||||
// Keep track of the original TOTP codes.
|
||||
OriginalTotpCodeIds = TotpCodeList.Where(t => !t.IsDeleted).Select(t => t.Id).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the add form.
|
||||
/// </summary>
|
||||
private void ShowAddForm()
|
||||
{
|
||||
NewTotpCode = new TotpCodeEdit();
|
||||
IsAddFormVisible = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides the add form.
|
||||
/// </summary>
|
||||
private void HideAddForm()
|
||||
{
|
||||
IsAddFormVisible = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new TOTP code called from the add form.
|
||||
/// </summary>
|
||||
private async Task AddTotpCode()
|
||||
{
|
||||
string secretKey = NewTotpCode.SecretKey;
|
||||
|
||||
// Sanitize the secret key (remove whitespace and hyphens)
|
||||
secretKey = secretKey.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
string? name = NewTotpCode.Name;
|
||||
|
||||
// Check if the input is a TOTP URI
|
||||
if (secretKey.StartsWith("otpauth://totp/"))
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = new Uri(secretKey);
|
||||
var queryParams = System.Web.HttpUtility.ParseQueryString(uri.Query);
|
||||
|
||||
// Extract the secret from query parameters
|
||||
secretKey = queryParams["secret"] ?? throw new ArgumentException("Secret not found in URI");
|
||||
|
||||
// If no name was provided, try to get it from the URI
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
// The label is everything after 'totp/' and before '?'
|
||||
var label = uri.AbsolutePath.TrimStart('/');
|
||||
// If the label contains ':', take the part after it
|
||||
name = label.Contains(':') ? label.Split(':')[1] : label;
|
||||
|
||||
// If there's an issuer in the query params, use it as a prefix
|
||||
var issuer = queryParams["issuer"];
|
||||
if (!string.IsNullOrWhiteSpace(issuer))
|
||||
{
|
||||
name = $"{issuer}: {name}";
|
||||
}
|
||||
NewTotpCode.Name = name;
|
||||
}
|
||||
NewTotpCode.SecretKey = secretKey;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("Invalid TOTP URI format. Please check and try again.", true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Validate the secret key by trying to generate a code
|
||||
TotpGenerator.GenerateTotpCode(secretKey);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("Invalid secret key. Please check and try again.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new TOTP code in memory
|
||||
var newTotpCode = NewTotpCode.ToEntity();
|
||||
newTotpCode.Name = name ?? "Authenticator";
|
||||
|
||||
// Add to the list
|
||||
TotpCodeList.Add(newTotpCode);
|
||||
|
||||
// Notify parent component
|
||||
await TotpCodesChanged.InvokeAsync(TotpCodeList);
|
||||
|
||||
HideAddForm();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a TOTP code.
|
||||
/// </summary>
|
||||
/// <param name="totpCode">The TOTP code to delete.</param>
|
||||
private async Task DeleteTotpCode(TotpCode totpCode)
|
||||
{
|
||||
// Show confirmation modal.
|
||||
var result = await ConfirmModalService.ShowConfirmation("Delete TOTP code", "Are you sure you want to delete this TOTP code?");
|
||||
if (!result)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the TOTP code was part of the original set
|
||||
if (OriginalTotpCodeIds.Contains(totpCode.Id))
|
||||
{
|
||||
// If it was part of the original set, we soft delete it
|
||||
var totpCodeToDelete = TotpCodeList.FirstOrDefault(t => t.Id == totpCode.Id);
|
||||
if (totpCodeToDelete is not null)
|
||||
{
|
||||
totpCodeToDelete.IsDeleted = true;
|
||||
totpCodeToDelete.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If it was not part of the original set, we hard delete it
|
||||
TotpCodeList.Remove(totpCode);
|
||||
}
|
||||
|
||||
// Notify parent component
|
||||
await TotpCodesChanged.InvokeAsync(TotpCodeList);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
181
src/AliasVault.Client/Main/Components/TotpCodes/TotpViewer.razor
Normal file
@@ -0,0 +1,181 @@
|
||||
@inherits ComponentBase
|
||||
@inject ClipboardCopyService ClipboardCopyService
|
||||
@inject JsInteropService JsInteropService
|
||||
@implements IDisposable
|
||||
@using TotpGenerator
|
||||
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Two-factor authentication</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else if (TotpCodeList.Count == 0)
|
||||
{
|
||||
<div class="flex flex-col justify-center">
|
||||
<p class="text-gray-500 dark:text-gray-400">No two-factor authenticator codes available</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid grid-cols-1 gap-4 mt-4">
|
||||
@foreach (var totpCode in TotpCodeList)
|
||||
{
|
||||
<div class="p-2 ps-3 pe-3 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center flex-1">
|
||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white">@totpCode.Name</h4>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-col items-end">
|
||||
<div class="totp-code text-lg font-bold cursor-pointer text-gray-900 dark:text-white hover:text-primary-600 dark:hover:text-primary-400 transition-colors" @onclick="() => CopyToClipboard(totpCode)">
|
||||
@GetTotpCode(totpCode.SecretKey)
|
||||
</div>
|
||||
<div class="text-xs">
|
||||
@if (IsCopied(totpCode.Id.ToString()))
|
||||
{
|
||||
<span class="text-green-600 dark:text-green-400">Copied!</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-gray-500 dark:text-gray-400">@GetRemainingSeconds()s</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1.5 h-8 bg-gray-200 rounded-full dark:bg-gray-600">
|
||||
<div class="bg-blue-600 rounded-full transition-all" style="height: @(GetRemainingPercentage())%; width: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The list of TOTP codes to display.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public required ICollection<TotpCode> TotpCodeList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The dictionary of current cached TOTP codes.
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, string> _currentCodes = new();
|
||||
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private Timer? _refreshTimer;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_refreshTimer?.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
// Generate initial codes
|
||||
foreach (var code in TotpCodeList.Select(t => t.SecretKey))
|
||||
{
|
||||
_currentCodes[code] = TotpGenerator.GenerateTotpCode(code);
|
||||
}
|
||||
|
||||
// Start a timer to refresh the TOTP codes every second
|
||||
_refreshTimer = new Timer(async _ => await RefreshCodesAsync(), null, 0, 1000);
|
||||
|
||||
IsLoading = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remaining seconds for the TOTP code.
|
||||
/// </summary>
|
||||
/// <returns>The remaining seconds.</returns>
|
||||
private static int GetRemainingSeconds(int step = 30)
|
||||
{
|
||||
var unixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
return step - (int)(unixTimestamp % step);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remaining percentage for the TOTP code.
|
||||
/// </summary>
|
||||
/// <returns>The remaining percentage.</returns>
|
||||
private static int GetRemainingPercentage()
|
||||
{
|
||||
var remaining = GetRemainingSeconds();
|
||||
// Invert the percentage so it counts down instead of up
|
||||
return (int)(((30.0 - remaining) / 30.0) * 100);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the TOTP codes by generating new codes based on the secret keys.
|
||||
/// </summary>
|
||||
private async Task RefreshCodesAsync()
|
||||
{
|
||||
foreach (var code in TotpCodeList.Select(t => t.SecretKey))
|
||||
{
|
||||
var newCode = TotpGenerator.GenerateTotpCode(code);
|
||||
if (!_currentCodes.ContainsKey(code) || _currentCodes[code] != newCode)
|
||||
{
|
||||
_currentCodes[code] = newCode;
|
||||
}
|
||||
}
|
||||
|
||||
// Always update the UI to refresh the progress bar
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TOTP code for a given secret key from the cached current codes dictionary.
|
||||
/// </summary>
|
||||
/// <param name="secretKey">The secret key to get the code for.</param>
|
||||
/// <returns>The TOTP code.</returns>
|
||||
private string GetTotpCode(string secretKey)
|
||||
{
|
||||
if (_currentCodes.TryGetValue(secretKey, out var code))
|
||||
{
|
||||
return code;
|
||||
}
|
||||
|
||||
var newCode = TotpGenerator.GenerateTotpCode(secretKey);
|
||||
_currentCodes[secretKey] = newCode;
|
||||
return newCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the TOTP code to the clipboard.
|
||||
/// </summary>
|
||||
/// <param name="totpCode">The TOTP code to copy.</param>
|
||||
private async Task CopyToClipboard(TotpCode totpCode)
|
||||
{
|
||||
var code = GetTotpCode(totpCode.SecretKey);
|
||||
await JsInteropService.CopyToClipboard(code);
|
||||
ClipboardCopyService.SetCopied(totpCode.Id.ToString());
|
||||
StateHasChanged();
|
||||
|
||||
// After 2 seconds, reset the copied state
|
||||
await Task.Delay(2000);
|
||||
if (ClipboardCopyService.GetCopiedId() == totpCode.Id.ToString())
|
||||
{
|
||||
ClipboardCopyService.SetCopied(string.Empty);
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the TOTP code was last copied.
|
||||
/// </summary>
|
||||
/// <param name="code">The TOTP code to check.</param>
|
||||
/// <returns>True if the TOTP code was last copied, false otherwise.</returns>
|
||||
private bool IsCopied(string code) => ClipboardCopyService.GetCopiedId() == code;
|
||||
}
|
||||
@@ -10,6 +10,9 @@ namespace AliasVault.Client.Main.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using AliasClientDb;
|
||||
using AliasVault.Client.Main.Models.FormValidation;
|
||||
|
||||
@@ -80,4 +83,88 @@ public sealed class CredentialEdit
|
||||
/// Gets or sets the Attachment list.
|
||||
/// </summary>
|
||||
public List<Attachment> Attachments { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TOTP codes list.
|
||||
/// </summary>
|
||||
public List<TotpCode> TotpCodes { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Creates a CredentialEdit instance from a Credential entity.
|
||||
/// </summary>
|
||||
/// <param name="credential">The credential entity to convert.</param>
|
||||
/// <returns>A new CredentialEdit instance.</returns>
|
||||
public static CredentialEdit FromEntity(Credential credential)
|
||||
{
|
||||
// Create a deep copy of the credential object to prevent changes to the original object
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
ReferenceHandler = ReferenceHandler.Preserve,
|
||||
MaxDepth = 128,
|
||||
};
|
||||
|
||||
// Create a deep copy of the credential object
|
||||
var credentialJson = JsonSerializer.Serialize(credential, options);
|
||||
var credentialCopy = JsonSerializer.Deserialize<Credential>(credentialJson, options)!;
|
||||
|
||||
return new CredentialEdit
|
||||
{
|
||||
Id = credentialCopy.Id,
|
||||
Notes = credentialCopy.Notes ?? string.Empty,
|
||||
Username = credentialCopy.Username ?? string.Empty,
|
||||
ServiceName = credentialCopy.Service.Name ?? string.Empty,
|
||||
ServiceUrl = credentialCopy.Service.Url,
|
||||
ServiceLogo = credentialCopy.Service.Logo,
|
||||
Password = credentialCopy.Passwords.FirstOrDefault() ?? new Password
|
||||
{
|
||||
Value = string.Empty,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
},
|
||||
Alias = credentialCopy.Alias,
|
||||
AliasBirthDate = credentialCopy.Alias.BirthDate.ToString("yyyy-MM-dd"),
|
||||
Attachments = credentialCopy.Attachments.ToList(),
|
||||
TotpCodes = credentialCopy.TotpCodes.ToList(),
|
||||
CreateDate = credentialCopy.CreatedAt,
|
||||
LastUpdate = credentialCopy.UpdatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this CredentialEdit instance to a Credential entity.
|
||||
/// </summary>
|
||||
/// <returns>A new Credential entity.</returns>
|
||||
public Credential ToEntity()
|
||||
{
|
||||
var credential = new Credential
|
||||
{
|
||||
Id = Id,
|
||||
Notes = Notes,
|
||||
Username = Username,
|
||||
Service = new Service
|
||||
{
|
||||
Name = ServiceName,
|
||||
Url = ServiceUrl,
|
||||
Logo = ServiceLogo,
|
||||
},
|
||||
Passwords = new List<Password>
|
||||
{
|
||||
Password,
|
||||
},
|
||||
Alias = Alias,
|
||||
Attachments = Attachments,
|
||||
TotpCodes = TotpCodes,
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(AliasBirthDate))
|
||||
{
|
||||
credential.Alias.BirthDate = DateTime.MinValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
credential.Alias.BirthDate = DateTime.Parse(AliasBirthDate, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return credential;
|
||||
}
|
||||
}
|
||||
|
||||
66
src/AliasVault.Client/Main/Models/TotpCodeEdit.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="TotpCodeEdit.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Client.Main.Models;
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using AliasClientDb;
|
||||
|
||||
/// <summary>
|
||||
/// Credential edit model.
|
||||
/// </summary>
|
||||
public sealed class TotpCodeEdit
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Id of the TOTP code.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the TOTP code.
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the secret key of the TOTP code.
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "Secret key is required")]
|
||||
public string SecretKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the created at date of the TOTP code.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the updated at date of the TOTP code.
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the TOTP code is deleted.
|
||||
/// </summary>
|
||||
public bool IsDeleted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts the edit model to a TotpCode entity.
|
||||
/// </summary>
|
||||
/// <returns>The TotpCode entity.</returns>
|
||||
public TotpCode ToEntity()
|
||||
{
|
||||
return new TotpCode
|
||||
{
|
||||
Id = Id,
|
||||
Name = Name ?? string.Empty,
|
||||
SecretKey = SecretKey,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
IsDeleted = IsDeleted,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,19 @@ else
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (EditMode && Id.HasValue)
|
||||
{
|
||||
<div class="col-span-1 md:col-span-1 lg:col-span-1">
|
||||
<TotpCodes TotpCodeList="@Obj.TotpCodes" TotpCodesChanged="HandleTotpCodesChanged" />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="col-span-1 md:col-span-1 lg:col-span-1">
|
||||
<TotpCodes TotpCodeList="@Obj.TotpCodes" TotpCodesChanged="HandleTotpCodesChanged" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="col-span-1 md:col-span-1 lg:col-span-1">
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Notes</h3>
|
||||
@@ -204,7 +217,7 @@ else
|
||||
return;
|
||||
}
|
||||
|
||||
Obj = CredentialToCredentialEdit(alias);
|
||||
Obj = CredentialEdit.FromEntity(alias);
|
||||
if (Obj.ServiceUrl is null)
|
||||
{
|
||||
Obj.ServiceUrl = CredentialService.DefaultServiceUrl;
|
||||
@@ -218,8 +231,9 @@ else
|
||||
alias.Alias.Email = "@" + CredentialService.GetDefaultEmailDomain();
|
||||
alias.Service = new Service();
|
||||
alias.Passwords = new List<Password> { new Password() };
|
||||
alias.TotpCodes = new List<TotpCode>();
|
||||
|
||||
Obj = CredentialToCredentialEdit(alias);
|
||||
Obj = CredentialEdit.FromEntity(alias);
|
||||
Obj.ServiceUrl = CredentialService.DefaultServiceUrl;
|
||||
}
|
||||
|
||||
@@ -257,12 +271,18 @@ else
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void HandleTotpCodesChanged(List<TotpCode> updatedTotpCodes)
|
||||
{
|
||||
Obj.TotpCodes = updatedTotpCodes;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task GenerateRandomAlias()
|
||||
{
|
||||
GlobalLoadingSpinner.Show();
|
||||
StateHasChanged();
|
||||
|
||||
Obj = CredentialToCredentialEdit(await CredentialService.GenerateRandomIdentity(CredentialEditToCredential(Obj)));
|
||||
Obj = CredentialEdit.FromEntity(await CredentialService.GenerateRandomIdentity(Obj.ToEntity()));
|
||||
|
||||
GlobalLoadingSpinner.Hide();
|
||||
StateHasChanged();
|
||||
@@ -305,82 +325,6 @@ else
|
||||
Obj.Password.Value = CredentialService.GenerateRandomPassword();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to convert a Credential object to a CredentialEdit object.
|
||||
/// </summary>
|
||||
private CredentialEdit CredentialToCredentialEdit(Credential alias)
|
||||
{
|
||||
// Create a deep copy of the alias object to prevent changes to the original object
|
||||
// when editing the alias in the form. We only want to save the changes when the user
|
||||
// clicks the save button.
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
ReferenceHandler = ReferenceHandler.Preserve,
|
||||
MaxDepth = 128 // Adjust this value as needed
|
||||
};
|
||||
|
||||
// Create a deep copy of the credential object
|
||||
var aliasJson = JsonSerializer.Serialize(alias, options);
|
||||
var aliasCopy = JsonSerializer.Deserialize<Credential>(aliasJson, options)!;
|
||||
|
||||
return new CredentialEdit
|
||||
{
|
||||
Id = aliasCopy.Id,
|
||||
Notes = aliasCopy.Notes ?? string.Empty,
|
||||
Username = aliasCopy.Username ?? string.Empty,
|
||||
ServiceName = aliasCopy.Service.Name ?? string.Empty,
|
||||
ServiceUrl = aliasCopy.Service.Url,
|
||||
ServiceLogo = aliasCopy.Service.Logo,
|
||||
Password = aliasCopy.Passwords.FirstOrDefault() ?? new Password
|
||||
{
|
||||
Value = string.Empty,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
},
|
||||
Alias = aliasCopy.Alias,
|
||||
AliasBirthDate = aliasCopy.Alias.BirthDate.ToString("yyyy-MM-dd"),
|
||||
Attachments = aliasCopy.Attachments.ToList(),
|
||||
CreateDate = aliasCopy.CreatedAt,
|
||||
LastUpdate = aliasCopy.UpdatedAt
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to convert a CredentialEdit object to a Credential object.
|
||||
/// </summary>
|
||||
private Credential CredentialEditToCredential(CredentialEdit alias)
|
||||
{
|
||||
var credential = new Credential
|
||||
{
|
||||
Id = alias.Id,
|
||||
Notes = alias.Notes,
|
||||
Username = alias.Username,
|
||||
Service = new Service
|
||||
{
|
||||
Name = alias.ServiceName,
|
||||
Url = alias.ServiceUrl,
|
||||
Logo = alias.ServiceLogo,
|
||||
},
|
||||
Passwords = new List<Password>
|
||||
{
|
||||
alias.Password,
|
||||
},
|
||||
Alias = alias.Alias,
|
||||
Attachments = alias.Attachments,
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(alias.AliasBirthDate))
|
||||
{
|
||||
credential.Alias.BirthDate = DateTime.MinValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
credential.Alias.BirthDate = DateTime.Parse(alias.AliasBirthDate, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return credential;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancel the edit operation and navigate back to the credentials view.
|
||||
/// </summary>
|
||||
@@ -414,12 +358,12 @@ else
|
||||
{
|
||||
if (Id is not null)
|
||||
{
|
||||
Id = await CredentialService.UpdateEntryAsync(CredentialEditToCredential(Obj));
|
||||
Id = await CredentialService.UpdateEntryAsync(Obj.ToEntity());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Id = await CredentialService.InsertEntryAsync(CredentialEditToCredential(Obj));
|
||||
Id = await CredentialService.InsertEntryAsync(Obj.ToEntity());
|
||||
}
|
||||
|
||||
GlobalLoadingSpinner.Hide();
|
||||
|
||||
@@ -44,6 +44,12 @@ else
|
||||
</div>
|
||||
</div>
|
||||
<RecentEmails EmailAddress="@Alias.Alias.Email" />
|
||||
|
||||
@if (Alias.TotpCodes.Count > 0)
|
||||
{
|
||||
<TotpViewer TotpCodeList="@Alias.TotpCodes" />
|
||||
}
|
||||
|
||||
@if (Alias.Notes != null && Alias.Notes.Length > 0)
|
||||
{
|
||||
<FormattedNote Notes="@Alias.Notes" />
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "http://0.0.0.0:5067",
|
||||
"applicationUrl": "http://localhost:5067",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Release"
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "https://0.0.0.0:7103",
|
||||
"applicationUrl": "https://localhost:7103",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
||||
@@ -182,6 +182,12 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
|
||||
login.Attachments.Add(attachment);
|
||||
}
|
||||
|
||||
// Add TOTP codes
|
||||
foreach (var totpCode in loginObject.TotpCodes)
|
||||
{
|
||||
login.TotpCodes.Add(totpCode);
|
||||
}
|
||||
|
||||
context.Credentials.Add(login);
|
||||
|
||||
// Add password.
|
||||
@@ -216,8 +222,6 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
|
||||
throw new InvalidOperationException("Login object not found.");
|
||||
}
|
||||
|
||||
// If the email starts with an @ it is most likely still the placeholder which hasn't been filled.
|
||||
// So we remove it.
|
||||
if (loginObject.Alias.Email is not null && loginObject.Alias.Email.StartsWith('@'))
|
||||
{
|
||||
loginObject.Alias.Email = null;
|
||||
@@ -229,61 +233,13 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
|
||||
loginObject.Service.Url = null;
|
||||
}
|
||||
|
||||
login.UpdatedAt = DateTime.UtcNow;
|
||||
login.Notes = loginObject.Notes;
|
||||
login.Username = loginObject.Username;
|
||||
// Update all fields and collections.
|
||||
UpdateBasicCredentialInfo(login, loginObject);
|
||||
UpdateAttachments(context, login, loginObject);
|
||||
UpdateTotpCodes(context, login, loginObject);
|
||||
|
||||
login.Alias.NickName = loginObject.Alias.NickName;
|
||||
login.Alias.FirstName = loginObject.Alias.FirstName;
|
||||
login.Alias.LastName = loginObject.Alias.LastName;
|
||||
login.Alias.BirthDate = loginObject.Alias.BirthDate;
|
||||
login.Alias.Gender = loginObject.Alias.Gender;
|
||||
login.Alias.Email = loginObject.Alias.Email;
|
||||
login.Alias.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
login.Passwords = loginObject.Passwords;
|
||||
if (login.Passwords.Count > 0)
|
||||
{
|
||||
login.Passwords.First().UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
login.Service.Name = loginObject.Service.Name;
|
||||
login.Service.Url = loginObject.Service.Url;
|
||||
login.Service.Logo = loginObject.Service.Logo;
|
||||
login.Service.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Remove attachments that are no longer in the list
|
||||
var attachmentsToRemove = login.Attachments.Where(existingAttachment =>
|
||||
!loginObject.Attachments.Any(a => a.Id == existingAttachment.Id)).ToList();
|
||||
foreach (var attachmentToRemove in attachmentsToRemove)
|
||||
{
|
||||
login.Attachments.Remove(attachmentToRemove);
|
||||
context.Entry(attachmentToRemove).State = EntityState.Deleted;
|
||||
}
|
||||
|
||||
// Update existing attachments and add new ones
|
||||
foreach (var attachment in loginObject.Attachments)
|
||||
{
|
||||
if (attachment.Id != Guid.Empty)
|
||||
{
|
||||
var existingAttachment = login.Attachments.FirstOrDefault(a => a.Id == attachment.Id);
|
||||
if (existingAttachment != null)
|
||||
{
|
||||
// Update existing attachment
|
||||
context.Entry(existingAttachment).CurrentValues.SetValues(attachment);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new attachment
|
||||
login.Attachments.Add(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
// Save the database to the server.
|
||||
if (!await dbService.SaveDatabaseAsync())
|
||||
{
|
||||
// If saving database failed, return empty guid to indicate error.
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
@@ -300,14 +256,23 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
|
||||
var context = await dbService.GetDbContextAsync();
|
||||
|
||||
var loginObject = await context.Credentials
|
||||
.Include(x => x.Passwords)
|
||||
.Include(x => x.Alias)
|
||||
.Include(x => x.Service)
|
||||
.Include(x => x.Attachments)
|
||||
.AsSplitQuery()
|
||||
.Where(x => x.Id == loginId)
|
||||
.Where(x => !x.IsDeleted)
|
||||
.FirstOrDefaultAsync();
|
||||
.Include(x => x.Passwords)
|
||||
.Include(x => x.Alias)
|
||||
.Include(x => x.Service)
|
||||
.Include(x => x.Attachments)
|
||||
.Include(x => x.TotpCodes)
|
||||
.AsSplitQuery()
|
||||
.Where(x => x.Id == loginId)
|
||||
.Where(x => !x.IsDeleted)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (loginObject != null)
|
||||
{
|
||||
// Filter out deleted items from collections after loading
|
||||
loginObject.Passwords = loginObject.Passwords.Where(p => !p.IsDeleted).ToList();
|
||||
loginObject.Attachments = loginObject.Attachments.Where(a => !a.IsDeleted).ToList();
|
||||
loginObject.TotpCodes = loginObject.TotpCodes.Where(t => !t.IsDeleted).ToList();
|
||||
}
|
||||
|
||||
return loginObject;
|
||||
}
|
||||
@@ -321,13 +286,14 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
|
||||
var context = await dbService.GetDbContextAsync();
|
||||
|
||||
var loginObject = await context.Credentials
|
||||
.Include(x => x.Passwords)
|
||||
.Include(x => x.Alias)
|
||||
.Include(x => x.Service)
|
||||
.Include(x => x.Attachments)
|
||||
.AsSplitQuery()
|
||||
.Where(x => !x.IsDeleted)
|
||||
.ToListAsync();
|
||||
.Include(x => x.Passwords.Where(p => !p.IsDeleted))
|
||||
.Include(x => x.Alias)
|
||||
.Include(x => x.Service)
|
||||
.Include(x => x.Attachments.Where(a => !a.IsDeleted))
|
||||
.Include(x => x.TotpCodes.Where(t => !t.IsDeleted))
|
||||
.AsSplitQuery()
|
||||
.Where(x => !x.IsDeleted)
|
||||
.ToListAsync();
|
||||
|
||||
return loginObject;
|
||||
}
|
||||
@@ -392,6 +358,107 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
|
||||
return await dbService.SaveDatabaseAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the basic credential information.
|
||||
/// </summary>
|
||||
/// <param name="login">The login object to update.</param>
|
||||
/// <param name="loginObject">The login object to update from.</param>
|
||||
private static void UpdateBasicCredentialInfo(Credential login, Credential loginObject)
|
||||
{
|
||||
login.UpdatedAt = DateTime.UtcNow;
|
||||
login.Notes = loginObject.Notes;
|
||||
login.Username = loginObject.Username;
|
||||
|
||||
login.Alias.NickName = loginObject.Alias.NickName;
|
||||
login.Alias.FirstName = loginObject.Alias.FirstName;
|
||||
login.Alias.LastName = loginObject.Alias.LastName;
|
||||
login.Alias.BirthDate = loginObject.Alias.BirthDate;
|
||||
login.Alias.Gender = loginObject.Alias.Gender;
|
||||
login.Alias.Email = loginObject.Alias.Email;
|
||||
login.Alias.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
login.Passwords = loginObject.Passwords;
|
||||
if (login.Passwords.Count > 0)
|
||||
{
|
||||
login.Passwords.First().UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
login.Service.Name = loginObject.Service.Name;
|
||||
login.Service.Url = loginObject.Service.Url;
|
||||
login.Service.Logo = loginObject.Service.Logo;
|
||||
login.Service.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the attachments.
|
||||
/// </summary>
|
||||
/// <param name="context">The database context.</param>
|
||||
/// <param name="login">The login object to update.</param>
|
||||
/// <param name="loginObject">The login object to update from.</param>
|
||||
private static void UpdateAttachments(DbContext context, Credential login, Credential loginObject)
|
||||
{
|
||||
var attachmentsToRemove = login.Attachments
|
||||
.Where(existingAttachment => !loginObject.Attachments.Any(a => a.Id == existingAttachment.Id))
|
||||
.ToList();
|
||||
|
||||
foreach (var attachmentToRemove in attachmentsToRemove)
|
||||
{
|
||||
login.Attachments.Remove(attachmentToRemove);
|
||||
context.Entry(attachmentToRemove).State = EntityState.Deleted;
|
||||
}
|
||||
|
||||
foreach (var attachment in loginObject.Attachments)
|
||||
{
|
||||
if (attachment.Id != Guid.Empty)
|
||||
{
|
||||
var existingAttachment = login.Attachments.FirstOrDefault(a => a.Id == attachment.Id);
|
||||
if (existingAttachment != null)
|
||||
{
|
||||
context.Entry(existingAttachment).CurrentValues.SetValues(attachment);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
login.Attachments.Add(attachment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the TOTP codes.
|
||||
/// </summary>
|
||||
/// <param name="context">The database context.</param>
|
||||
/// <param name="login">The login object to update.</param>
|
||||
/// <param name="loginObject">The login object to update from.</param>
|
||||
private static void UpdateTotpCodes(DbContext context, Credential login, Credential loginObject)
|
||||
{
|
||||
var totpCodesToRemove = login.TotpCodes
|
||||
.Where(existingTotp => !loginObject.TotpCodes.Any(t => t.Id == existingTotp.Id))
|
||||
.ToList();
|
||||
|
||||
foreach (var totpToRemove in totpCodesToRemove)
|
||||
{
|
||||
login.TotpCodes.Remove(totpToRemove);
|
||||
context.Entry(totpToRemove).State = EntityState.Deleted;
|
||||
}
|
||||
|
||||
foreach (var totpCode in loginObject.TotpCodes)
|
||||
{
|
||||
if (totpCode.Id != Guid.Empty)
|
||||
{
|
||||
var existingTotpCode = login.TotpCodes.FirstOrDefault(t => t.Id == totpCode.Id);
|
||||
if (existingTotpCode != null)
|
||||
{
|
||||
context.Entry(existingTotpCode).CurrentValues.SetValues(totpCode);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
login.TotpCodes.Add(totpCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract favicon from service URL if available in object. If successful the passed object itself will be updated with the bytes.
|
||||
/// </summary>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
@using AliasVault.Client.Main.Components.Forms
|
||||
@using AliasVault.Client.Main.Components.Layout
|
||||
@using AliasVault.Client.Main.Components.Loading
|
||||
@using AliasVault.Client.Main.Components.TotpCodes
|
||||
@using AliasVault.Client.Main.Components.Widgets
|
||||
@using AliasVault.Client.Main.Models
|
||||
@using AliasVault.Client.Services
|
||||
|
||||
@@ -805,6 +805,10 @@ video {
|
||||
margin-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
.ms-auto {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
@@ -944,6 +948,14 @@ video {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.w-1 {
|
||||
width: 0.25rem;
|
||||
}
|
||||
|
||||
.w-1\.5 {
|
||||
width: 0.375rem;
|
||||
}
|
||||
|
||||
.w-1\/2 {
|
||||
width: 50%;
|
||||
}
|
||||
@@ -1136,6 +1148,10 @@ video {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.items-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
@@ -1168,6 +1184,10 @@ video {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.space-x-1 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.25rem * var(--tw-space-x-reverse));
|
||||
@@ -1424,6 +1444,16 @@ video {
|
||||
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||
@@ -1539,6 +1569,10 @@ video {
|
||||
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
@@ -1721,6 +1755,14 @@ video {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.pe-3 {
|
||||
padding-inline-end: 0.75rem;
|
||||
}
|
||||
|
||||
.ps-3 {
|
||||
padding-inline-start: 0.75rem;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -2025,6 +2067,12 @@ video {
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-all {
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-colors {
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
@@ -2069,6 +2117,11 @@ video {
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-blue-800:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 64 175 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-gray-100:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||
@@ -2204,6 +2257,11 @@ video {
|
||||
color: rgb(185 28 28 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-red-800:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(153 27 27 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-white:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
@@ -2339,6 +2397,11 @@ video {
|
||||
border-color: rgb(156 163 175 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-gray-500:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(107 114 128 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-gray-600:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(75 85 99 / var(--tw-border-opacity));
|
||||
@@ -2379,6 +2442,11 @@ video {
|
||||
border-color: rgb(234 179 8 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-600:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-800:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 64 175 / var(--tw-bg-opacity));
|
||||
@@ -2556,6 +2624,11 @@ video {
|
||||
color: rgb(248 113 113 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-red-500:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(239 68 68 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-white:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
@@ -2590,6 +2663,16 @@ video {
|
||||
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-blue-600:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-blue-700:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-gray-500:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
@@ -2645,6 +2728,11 @@ video {
|
||||
color: rgb(191 219 254 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-blue-400:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(96 165 250 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-gray-200:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(229 231 235 / var(--tw-text-opacity));
|
||||
@@ -2655,11 +2743,21 @@ video {
|
||||
color: rgb(248 185 99 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-primary-400:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(246 167 82 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-primary-500:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(244 149 65 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-red-400:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(248 113 113 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-white:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace AliasClientDb;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// The AliasClientDbContext class.
|
||||
@@ -78,6 +77,11 @@ public class AliasClientDbContext : DbContext
|
||||
/// </summary>
|
||||
public DbSet<Setting> Settings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TotpCodes DbSet.
|
||||
/// </summary>
|
||||
public DbSet<TotpCode> TotpCodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The OnModelCreating method.
|
||||
/// </summary>
|
||||
@@ -125,6 +129,13 @@ public class AliasClientDbContext : DbContext
|
||||
.WithMany(c => c.Passwords)
|
||||
.HasForeignKey(l => l.CredentialId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Configure TotpCode - Credential relationship
|
||||
modelBuilder.Entity<TotpCode>()
|
||||
.HasOne(l => l.Credential)
|
||||
.WithMany(c => c.TotpCodes)
|
||||
.HasForeignKey(l => l.CredentialId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -53,6 +53,11 @@ public class Credential : SyncableEntity
|
||||
/// </summary>
|
||||
public virtual ICollection<Attachment> Attachments { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TOTP code objects.
|
||||
/// </summary>
|
||||
public virtual ICollection<TotpCode> TotpCodes { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the service ID foreign key.
|
||||
/// </summary>
|
||||
|
||||
364
src/Databases/AliasClientDb/Migrations/20250310131554_1.5.0-AddTotpCodes.Designer.cs
generated
Normal file
@@ -0,0 +1,364 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using AliasClientDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasClientDb.Migrations
|
||||
{
|
||||
[DbContext(typeof(AliasClientDbContext))]
|
||||
[Migration("20250310131554_1.5.0-AddTotpCodes")]
|
||||
partial class _150AddTotpCodes
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.2")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true);
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Alias", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("BirthDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("Gender")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("NickName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Aliases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Attachment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("CredentialId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CredentialId");
|
||||
|
||||
b.ToTable("Attachments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AliasId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ServiceId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AliasId");
|
||||
|
||||
b.HasIndex("ServiceId");
|
||||
|
||||
b.ToTable("Credentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.EncryptionKey", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PrivateKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PublicKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("EncryptionKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Password", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("CredentialId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CredentialId");
|
||||
|
||||
b.ToTable("Passwords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Service", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("Logo")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Services");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.TotpCode", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("CredentialId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SecretKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CredentialId");
|
||||
|
||||
b.ToTable("TotpCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Attachment", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Credential", "Credential")
|
||||
.WithMany("Attachments")
|
||||
.HasForeignKey("CredentialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Credential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Alias", "Alias")
|
||||
.WithMany("Credentials")
|
||||
.HasForeignKey("AliasId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("AliasClientDb.Service", "Service")
|
||||
.WithMany("Credentials")
|
||||
.HasForeignKey("ServiceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Alias");
|
||||
|
||||
b.Navigation("Service");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Password", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Credential", "Credential")
|
||||
.WithMany("Passwords")
|
||||
.HasForeignKey("CredentialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Credential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.TotpCode", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Credential", "Credential")
|
||||
.WithMany("TotpCodes")
|
||||
.HasForeignKey("CredentialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Credential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Alias", b =>
|
||||
{
|
||||
b.Navigation("Credentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.Navigation("Attachments");
|
||||
|
||||
b.Navigation("Passwords");
|
||||
|
||||
b.Navigation("TotpCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Service", b =>
|
||||
{
|
||||
b.Navigation("Credentials");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasClientDb.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class _150AddTotpCodes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TotpCodes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 255, nullable: false),
|
||||
SecretKey = table.Column<string>(type: "TEXT", maxLength: 255, nullable: false),
|
||||
CredentialId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TotpCodes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_TotpCodes_Credentials_CredentialId",
|
||||
column: x => x.CredentialId,
|
||||
principalTable: "Credentials",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TotpCodes_CredentialId",
|
||||
table: "TotpCodes",
|
||||
column: "CredentialId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "TotpCodes");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace AliasClientDb.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.8")
|
||||
.HasAnnotation("ProductVersion", "9.0.2")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true);
|
||||
@@ -250,6 +250,41 @@ namespace AliasClientDb.Migrations
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.TotpCode", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("CredentialId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SecretKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CredentialId");
|
||||
|
||||
b.ToTable("TotpCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Attachment", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Credential", "Credential")
|
||||
@@ -291,6 +326,17 @@ namespace AliasClientDb.Migrations
|
||||
b.Navigation("Credential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.TotpCode", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Credential", "Credential")
|
||||
.WithMany("TotpCodes")
|
||||
.HasForeignKey("CredentialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Credential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Alias", b =>
|
||||
{
|
||||
b.Navigation("Credentials");
|
||||
@@ -301,6 +347,8 @@ namespace AliasClientDb.Migrations
|
||||
b.Navigation("Attachments");
|
||||
|
||||
b.Navigation("Passwords");
|
||||
|
||||
b.Navigation("TotpCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Service", b =>
|
||||
|
||||
48
src/Databases/AliasClientDb/TotpCode.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="TotpCode.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasClientDb;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using AliasClientDb.Abstracts;
|
||||
|
||||
/// <summary>
|
||||
/// The TotpCode class that stores 2FA information associated with a credential.
|
||||
/// </summary>
|
||||
public class TotpCode : SyncableEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the ID.
|
||||
/// </summary>
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the TOTP code.
|
||||
/// </summary>
|
||||
[MaxLength(255)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the secret key for the TOTP code.
|
||||
/// </summary>
|
||||
[MaxLength(255)]
|
||||
public string SecretKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the credential ID.
|
||||
/// </summary>
|
||||
public Guid CredentialId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the credential.
|
||||
/// </summary>
|
||||
[ForeignKey("CredentialId")]
|
||||
public virtual Credential? Credential { get; set; }
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.2" />
|
||||
<PackageReference Include="MimeKit" Version="4.10.0" />
|
||||
<PackageReference Include="MimeKit" Version="4.11.0" />
|
||||
<PackageReference Include="NUglify" Version="1.21.13" />
|
||||
<PackageReference Include="SmtpServer" Version="10.0.1" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
|
||||
@@ -25,7 +25,7 @@ public static class AppInfo
|
||||
/// <summary>
|
||||
/// Gets the minor version number.
|
||||
/// </summary>
|
||||
public const int VersionMinor = 13;
|
||||
public const int VersionMinor = 14;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the patch version number.
|
||||
@@ -54,6 +54,8 @@ public static class AppInfo
|
||||
{ "firefox", MinimumClientVersion },
|
||||
{ "edge", MinimumClientVersion },
|
||||
{ "safari", MinimumClientVersion },
|
||||
|
||||
// Note: Brave currently identifies as "chrome" in the user agent string, so this version definition might be obsolete.
|
||||
{ "brave", MinimumClientVersion },
|
||||
|
||||
// Fallback for unknown browsers.
|
||||
|
||||
@@ -29,25 +29,29 @@ public static class Constants
|
||||
{
|
||||
Name = "Firefox",
|
||||
IconPath = "/img/browser-icons/firefox.svg",
|
||||
IsAvailable = false,
|
||||
DownloadUrl = "https://addons.mozilla.org/en-US/firefox/addon/aliasvault/",
|
||||
IsAvailable = true,
|
||||
},
|
||||
[BrowserType.Safari] = new BrowserExtensionInfo
|
||||
{
|
||||
Name = "Safari",
|
||||
IconPath = "/img/browser-icons/safari.svg",
|
||||
IsAvailable = false,
|
||||
DownloadUrl = "https://apps.apple.com/app/6743163173",
|
||||
IsAvailable = true,
|
||||
},
|
||||
[BrowserType.Edge] = new BrowserExtensionInfo
|
||||
{
|
||||
Name = "Microsoft Edge",
|
||||
IconPath = "/img/browser-icons/edge.svg",
|
||||
IsAvailable = false,
|
||||
DownloadUrl = "https://microsoftedge.microsoft.com/addons/detail/aliasvault/kabaanafahnjkfkplbnllebdmppdemfo",
|
||||
IsAvailable = true,
|
||||
},
|
||||
[BrowserType.Brave] = new BrowserExtensionInfo
|
||||
{
|
||||
Name = "Brave",
|
||||
IconPath = "/img/browser-icons/brave.svg",
|
||||
IsAvailable = false,
|
||||
DownloadUrl = "https://chromewebstore.google.com/detail/aliasvault/bmoggiinmnodjphdjnmpcnlleamkfedj",
|
||||
IsAvailable = true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
124
src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/TotpTests.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="TotpTests.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.E2ETests.Tests.Client.Shard5;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end tests for uploading and downloading attachments.
|
||||
/// </summary>
|
||||
[Parallelizable(ParallelScope.Self)]
|
||||
[Category("ClientTests")]
|
||||
[TestFixture]
|
||||
public class TotpTests : ClientPlaywrightTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Test that adding and verifying a TOTP code works correctly.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
[Order(1)]
|
||||
public async Task AddAndVerifyTotpCode()
|
||||
{
|
||||
// Create a new credential with service name = "Test Service TOTP"
|
||||
var serviceName = "Test Service TOTP";
|
||||
await CreateCredentialEntry(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "service-name", serviceName },
|
||||
});
|
||||
|
||||
// Open the edit page again by clicking the button that contains the text "Edit"
|
||||
await Page.ClickAsync("text=Edit");
|
||||
await WaitForUrlAsync("credentials/**/edit", "Edit the existing credentials");
|
||||
|
||||
// Click the "Add TOTP Code" button
|
||||
var addButton = Page.Locator("button[id='add-totp-code'], a[id='add-totp-code']");
|
||||
await addButton.ClickAsync();
|
||||
|
||||
// Fill in the TOTP code details
|
||||
var secretKeyInput = Page.Locator("input[id='secretKey']");
|
||||
var nameInput = Page.Locator("input[id='name']");
|
||||
|
||||
await nameInput.FillAsync("Test TOTP");
|
||||
await secretKeyInput.FillAsync("JBSWY3DPEHPK3PXP"); // Example secret key
|
||||
|
||||
// Submit the form
|
||||
var saveButton = Page.Locator("button[id='save-totp-code']");
|
||||
await saveButton.ClickAsync();
|
||||
|
||||
// Save the credential
|
||||
var submitButton = Page.Locator("text=Save Credentials").First;
|
||||
await submitButton.ClickAsync();
|
||||
|
||||
await WaitForUrlAsync("credentials/**", "Credential updated successfully");
|
||||
|
||||
// Verify that the TOTP code appears in the list
|
||||
var pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain("Test TOTP"), "TOTP code name does not appear on the page");
|
||||
|
||||
// Verify that a 6-digit code is generated and displayed
|
||||
var codeElement = Page.Locator(".totp-code");
|
||||
var code = await codeElement.TextContentAsync();
|
||||
Assert.That(code, Does.Match(@"^\d{6}$"), "Generated TOTP code is not a 6-digit number");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that deleting a TOTP code works correctly.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
[Order(2)]
|
||||
public async Task DeleteTotpCode()
|
||||
{
|
||||
// Create a new credential with service name = "Test Service TOTP Delete"
|
||||
var serviceName = "Test Service TOTP Delete";
|
||||
await CreateCredentialEntry(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "service-name", serviceName },
|
||||
});
|
||||
|
||||
// Open the edit page again by clicking the button that contains the text "Edit"
|
||||
await Page.ClickAsync("text=Edit");
|
||||
await WaitForUrlAsync("credentials/**/edit", "Edit the existing credentials");
|
||||
|
||||
// Add a TOTP code
|
||||
var addButton = Page.Locator("button[id='add-totp-code'], a[id='add-totp-code']");
|
||||
await addButton.ClickAsync();
|
||||
|
||||
await Page.Locator("input[id='name']").FillAsync("TOTP to Delete");
|
||||
await Page.Locator("input[id='secretKey']").FillAsync("JBSWY3DPEHPK3PXP");
|
||||
|
||||
await Page.Locator("button[id='save-totp-code']").ClickAsync();
|
||||
|
||||
// Save the credential
|
||||
var submitButton = Page.Locator("text=Save Credentials").First;
|
||||
await submitButton.ClickAsync();
|
||||
|
||||
await WaitForUrlAsync("credentials/**", "Credential updated successfully");
|
||||
|
||||
// Verify the TOTP code was added
|
||||
var pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain("TOTP to Delete"), "TOTP code was not added successfully");
|
||||
|
||||
// Open the edit page again by clicking the button that contains the text "Edit"
|
||||
await Page.ClickAsync("text=Edit");
|
||||
await WaitForUrlAsync("credentials/**/edit", "Edit the existing credentials");
|
||||
|
||||
// Click the delete button for the TOTP code
|
||||
var deleteButton = Page.Locator("button.delete-totp-code").First;
|
||||
await deleteButton.ClickAsync();
|
||||
|
||||
// Confirm deletion in the modal
|
||||
var confirmButton = Page.Locator("button:has-text('Confirm')");
|
||||
await confirmButton.ClickAsync();
|
||||
|
||||
// Verify the TOTP code was deleted
|
||||
pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Not.Contain("TOTP to Delete"), "TOTP code was not deleted successfully");
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4"/>
|
||||
<PackageReference Include="MailKit" Version="4.10.0" />
|
||||
<PackageReference Include="MailKit" Version="4.11.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="4.3.2"/>
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.6.0"/>
|
||||
|
||||