chrome-0.0.1
This commit is contained in:
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
npm run lint
|
||||
77
README.md
77
README.md
@@ -1,59 +1,48 @@
|
||||
# GootiExtension
|
||||
# Gooti
|
||||
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.0.7.
|
||||
## Nostr Identity Manager & Signer
|
||||
|
||||
## Development server
|
||||
Gooti is a browser extension for managing multiple [Nostr](https://github.com/nostr-protocol/nostr) identities and for signing events on web apps without having to give them your keys.
|
||||
|
||||
To start a local development server, run:
|
||||
It implements these mandatory [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md) methods:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```typescript
|
||||
async window.nostr.getPublicKey(): string
|
||||
async window.nostr.signEvent(event: { created_at: number, kind: number, tags: string[][], content: string }): Event
|
||||
```
|
||||
|
||||
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||
It also implements these optional methods:
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||
|
||||
```bash
|
||||
ng generate component component-name
|
||||
```typescript
|
||||
async window.nostr.getRelays(): { [url: string]: {read: boolean, write: boolean} }
|
||||
async window.nostr.nip04.encrypt(pubkey, plaintext): string
|
||||
async window.nostr.nip04.decrypt(pubkey, ciphertext): string
|
||||
```
|
||||
|
||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||
The repository is configured to hold the extensions for Chrome and Firefox. While the Chrome extension is yet already available, the Firefox extension will follow later.
|
||||
|
||||
```bash
|
||||
ng generate --help
|
||||
[Chrome Extension](https://chromewebstore.google.com/detail/cgikhnoggbhdblnckhcahgkipmiiohbk)
|
||||
|
||||
Firefox Extension (yet to come)
|
||||
|
||||
## Develop Chrome Extension
|
||||
|
||||
To run the Chrome extension from this code:
|
||||
|
||||
```
|
||||
git clone https://github.com/sam-hayes-org/gooti-extension
|
||||
cd gooti-extension
|
||||
npm i
|
||||
npm run watch:chrome
|
||||
```
|
||||
|
||||
## Building
|
||||
then
|
||||
|
||||
To build the project run:
|
||||
1. within Chrome go to `chrome://extensions`
|
||||
2. ensure "developer mode" is enabled on the top right
|
||||
3. click on "Load unpackaged"
|
||||
4. select the `dist/chrome` folder of this repository
|
||||
|
||||
```bash
|
||||
ng build
|
||||
```
|
||||
---
|
||||
|
||||
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
For end-to-end (e2e) testing, run:
|
||||
|
||||
```bash
|
||||
ng e2e
|
||||
```
|
||||
|
||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||
LICENSE: Public Domain
|
||||
232
angular.json
232
angular.json
@@ -2,6 +2,238 @@
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"cli": {
|
||||
"schematicCollections": ["angular-eslint"]
|
||||
},
|
||||
"projects": {
|
||||
"chrome": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "projects/chrome",
|
||||
"sourceRoot": "projects/chrome/src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-builders/custom-webpack:browser",
|
||||
"options": {
|
||||
"customWebpackConfig": {
|
||||
"path": "projects/chrome/custom-webpack.config.ts"
|
||||
},
|
||||
"outputPath": "dist/chrome",
|
||||
"index": "projects/chrome/src/index.html",
|
||||
"main": "projects/chrome/src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "projects/chrome/tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "projects/chrome/public"
|
||||
}
|
||||
],
|
||||
"styles": ["projects/chrome/src/styles.scss"],
|
||||
"scripts": ["node_modules/bootstrap/dist/js/bootstrap.bundle.js"]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "5MB",
|
||||
"maximumError": "10MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"optimization": {
|
||||
"scripts": true,
|
||||
"styles": false,
|
||||
"fonts": true
|
||||
}
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "chrome:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "chrome:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
"tsConfig": "projects/chrome/tsconfig.spec.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "projects/chrome/public"
|
||||
}
|
||||
],
|
||||
"styles": ["projects/chrome/src/styles.scss"],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"projects/chrome/**/*.ts",
|
||||
"projects/chrome/**/*.html"
|
||||
],
|
||||
"eslintConfig": "projects/chrome/eslint.config.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"firefox": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "projects/firefox",
|
||||
"sourceRoot": "projects/firefox/src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/firefox",
|
||||
"index": "projects/firefox/src/index.html",
|
||||
"browser": "projects/firefox/src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "projects/firefox/tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "projects/firefox/public"
|
||||
}
|
||||
],
|
||||
"styles": ["projects/firefox/src/styles.scss"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "firefox:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "firefox:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
"tsConfig": "projects/firefox/tsconfig.spec.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "projects/firefox/public"
|
||||
}
|
||||
],
|
||||
"styles": ["projects/firefox/src/styles.scss"],
|
||||
"scripts": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"projectType": "library",
|
||||
"root": "projects/common",
|
||||
"sourceRoot": "projects/common/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "projects/common/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "projects/common/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "projects/common/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "projects/common/tsconfig.spec.json",
|
||||
"polyfills": ["zone.js", "zone.js/testing"]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"projects/common/**/*.ts",
|
||||
"projects/common/**/*.html"
|
||||
],
|
||||
"eslintConfig": "projects/common/eslint.config.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
chrome_prepare_manifest.sh
Executable file
7
chrome_prepare_manifest.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
version=$( cat package.json | jq '.custom.chrome.version' | tr -d '"')
|
||||
|
||||
jq '.version = $newVersion' --arg newVersion $version ./projects/chrome/public/manifest.json > ./projects/chrome/public/tmp.manifest.json && mv ./projects/chrome/public/tmp.manifest.json ./projects/chrome/public/manifest.json
|
||||
|
||||
echo $version
|
||||
26
eslint.config.js
Normal file
26
eslint.config.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// @ts-check
|
||||
const eslint = require("@eslint/js");
|
||||
const tseslint = require("typescript-eslint");
|
||||
const angular = require("angular-eslint");
|
||||
|
||||
module.exports = tseslint.config(
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
extends: [
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...tseslint.configs.stylistic,
|
||||
...angular.configs.tsRecommended,
|
||||
],
|
||||
processor: angular.processInlineTemplates,
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ["**/*.html"],
|
||||
extends: [
|
||||
...angular.configs.templateRecommended,
|
||||
...angular.configs.templateAccessibility,
|
||||
],
|
||||
rules: {},
|
||||
}
|
||||
);
|
||||
11610
package-lock.json
generated
11610
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
@@ -1,12 +1,28 @@
|
||||
{
|
||||
"name": "gooti-extension",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"custom": {
|
||||
"chrome": {
|
||||
"version": "0.0.1"
|
||||
},
|
||||
"firefox": {
|
||||
"version": "0.0.0"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
"clean:chrome": "rimraf dist/chrome",
|
||||
"clean:firefox": "rimraf dist/firefox",
|
||||
"start:chrome": "ng serve chrome",
|
||||
"start:firefox": "ng serve firefox",
|
||||
"prepare:chrome": "./chrome_prepare_manifest.sh",
|
||||
"build:chrome": "npm run prepare:chrome && ng build chrome",
|
||||
"build:firefox": "ng build firefox",
|
||||
"watch:chrome": "npm run prepare:chrome && ng build chrome --watch --configuration development",
|
||||
"watch:firefox": "ng build firefox --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@@ -18,20 +34,38 @@
|
||||
"@angular/platform-browser": "^19.0.0",
|
||||
"@angular/platform-browser-dynamic": "^19.0.0",
|
||||
"@angular/router": "^19.0.0",
|
||||
"@nostr-dev-kit/ndk": "^2.11.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"buffer": "^6.0.3",
|
||||
"nostr-tools": "^2.10.4",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"webextension-polyfill": "^0.12.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "^19.0.0",
|
||||
"@angular-devkit/build-angular": "^19.0.7",
|
||||
"@angular/cli": "^19.0.7",
|
||||
"@angular/compiler-cli": "^19.0.0",
|
||||
"@types/bootstrap": "^5.2.10",
|
||||
"@types/chrome": "^0.0.293",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/webextension-polyfill": "^0.12.1",
|
||||
"angular-eslint": "19.0.2",
|
||||
"eslint": "^9.16.0",
|
||||
"husky": "^9.1.7",
|
||||
"jasmine-core": "~5.4.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.6.2"
|
||||
"ng-packagr": "^19.0.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "8.18.0"
|
||||
}
|
||||
}
|
||||
|
||||
22
projects/chrome/custom-webpack.config.ts
Normal file
22
projects/chrome/custom-webpack.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Configuration } from 'webpack';
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
background: {
|
||||
import: 'src/background.ts',
|
||||
runtime: false,
|
||||
},
|
||||
'gooti-extension': {
|
||||
import: 'src/gooti-extension.ts',
|
||||
runtime: false,
|
||||
},
|
||||
'gooti-content-script': {
|
||||
import: 'src/gooti-content-script.ts',
|
||||
runtime: false,
|
||||
},
|
||||
prompt: {
|
||||
import: 'src/prompt.ts',
|
||||
runtime: false,
|
||||
},
|
||||
},
|
||||
} as Configuration;
|
||||
32
projects/chrome/eslint.config.js
Normal file
32
projects/chrome/eslint.config.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// @ts-check
|
||||
const tseslint = require("typescript-eslint");
|
||||
const rootConfig = require("../../eslint.config.js");
|
||||
|
||||
module.exports = tseslint.config(
|
||||
...rootConfig,
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
rules: {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
type: "attribute",
|
||||
prefix: "app",
|
||||
style: "camelCase",
|
||||
},
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
type: "element",
|
||||
prefix: "app",
|
||||
style: "kebab-case",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/*.html"],
|
||||
rules: {},
|
||||
}
|
||||
);
|
||||
1
projects/chrome/public/bird.svg
Normal file
1
projects/chrome/public/bird.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Origami-Paper-Bird--Streamline-Cyber.svg" height="24" width="24"><desc>Origami Paper Bird Streamline Icon: https://streamlinehq.com</desc><path fill="#ffffff" d="M4.66378 6.62012 0.751282 10.9774H6.11588l0.5042 7.3802 11.73752 4.8911L9.55488 9.01202l-4.8911 -2.3919Z" stroke-width="1"></path><path fill="#ffffff" d="M18.8423 0.751343 9.55499 9.01194l5.32571 8.61246L18.8423 0.751343Z" stroke-width="1"></path><path fill="#bbd8ff" d="m9.555 9.01187 -1.4675 -0.7178 -1.7681 5.66933 0.3007 4.3942L9.555 9.01187Z" stroke-width="1"></path><path fill="#bbd8ff" d="m4.66378 6.62012 1.4521 4.35728H0.751282L4.66378 6.62012Z" stroke-width="1"></path><path fill="#bbd8ff" d="m15.3767 18.4282 7.872 -15.23167 -5.5814 2.5565 -2.7866 11.87137 0.496 0.8038Z" stroke-width="1"></path><path stroke="#092f63" stroke-linejoin="round" stroke-miterlimit="10" d="m9.55488 9.01202 -4.8911 -2.3919L0.751282 10.9774H6.11588l0.5042 7.3802 11.73752 4.8911L9.55488 9.01202Z" stroke-width="1"></path><path stroke="#092f63" stroke-linejoin="round" stroke-miterlimit="10" d="M9.55499 9.01194 18.8423 0.751343 14.8807 17.6244" stroke-width="1"></path><path stroke="#092f63" stroke-linejoin="round" stroke-miterlimit="10" d="m17.6673 5.75303 5.5814 -2.5565 -7.872 15.23167" stroke-width="1"></path><path stroke="#092f63" stroke-linejoin="round" stroke-miterlimit="10" d="m4.66382 6.62012 1.4521 4.35728" stroke-width="1"></path><path stroke="#092f63" stroke-linejoin="round" stroke-miterlimit="10" d="m6.62109 18.3564 2.9338 -9.34456" stroke-width="1"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
BIN
projects/chrome/public/gooti-with-bg.png
Normal file
BIN
projects/chrome/public/gooti-with-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 983 B |
1
projects/chrome/public/gooti.svg
Normal file
1
projects/chrome/public/gooti.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Origami-Paper-Bird--Streamline-Cyber.svg" height="24" width="24"><desc>Origami Paper Bird Streamline Icon: https://streamlinehq.com</desc><path fill="#ffffff" d="M4.66378 6.62012 0.751282 10.9774H6.11588l0.5042 7.3802 11.73752 4.8911L9.55488 9.01202l-4.8911 -2.3919Z" stroke-width="1"></path><path fill="#ffffff" d="M18.8423 0.751343 9.55499 9.01194l5.32571 8.61246L18.8423 0.751343Z" stroke-width="1"></path><path fill="#bbd8ff" d="m9.555 9.01187 -1.4675 -0.7178 -1.7681 5.66933 0.3007 4.3942L9.555 9.01187Z" stroke-width="1"></path><path fill="#bbd8ff" d="m4.66378 6.62012 1.4521 4.35728H0.751282L4.66378 6.62012Z" stroke-width="1"></path><path fill="#bbd8ff" d="m15.3767 18.4282 7.872 -15.23167 -5.5814 2.5565 -2.7866 11.87137 0.496 0.8038Z" stroke-width="1"></path><path stroke="#0d6efd" stroke-linejoin="round" stroke-miterlimit="10" d="m9.55488 9.01202 -4.8911 -2.3919L0.751282 10.9774H6.11588l0.5042 7.3802 11.73752 4.8911L9.55488 9.01202Z" stroke-width="1"></path><path stroke="#0d6efd" stroke-linejoin="round" stroke-miterlimit="10" d="M9.55499 9.01194 18.8423 0.751343 14.8807 17.6244" stroke-width="1"></path><path stroke="#0d6efd" stroke-linejoin="round" stroke-miterlimit="10" d="m17.6673 5.75303 5.5814 -2.5565 -7.872 15.23167" stroke-width="1"></path><path stroke="#0d6efd" stroke-linejoin="round" stroke-miterlimit="10" d="m4.66382 6.62012 1.4521 4.35728" stroke-width="1"></path><path stroke="#0d6efd" stroke-linejoin="round" stroke-miterlimit="10" d="m6.62109 18.3564 2.9338 -9.34456" stroke-width="1"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
45
projects/chrome/public/manifest.json
Normal file
45
projects/chrome/public/manifest.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Gooti",
|
||||
"description": "Nostr Identity Manager & Signer",
|
||||
"version": "0.0.1",
|
||||
"homepage_url": "https://getgooti.com",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"windows",
|
||||
"storage"
|
||||
],
|
||||
"action": {
|
||||
"default_popup": "index.html",
|
||||
"default_icon": "gooti-with-bg.png"
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"run_at": "document_start",
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"gooti-content-script.js"
|
||||
],
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": [
|
||||
"gooti-extension.js"
|
||||
],
|
||||
"matches": [
|
||||
"https://*/*",
|
||||
"http://localhost:*/*",
|
||||
"http://0.0.0.0:*/*",
|
||||
"http://127.0.0.1:*/*",
|
||||
"http://*.localhost/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
3
projects/chrome/public/person-fill.svg
Normal file
3
projects/chrome/public/person-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="#ffffff" class="bi bi-person-fill" viewBox="0 0 16 16">
|
||||
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 219 B |
221
projects/chrome/public/prompt.html
Normal file
221
projects/chrome/public/prompt.html
Normal file
@@ -0,0 +1,221 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html data-bs-theme="dark">
|
||||
<head>
|
||||
<title>Gooti</title>
|
||||
<link rel="stylesheet" type="text/css" href="styles.css" />
|
||||
<script src="scripts.js"></script>
|
||||
<style>
|
||||
body {
|
||||
background: var(--background);
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.color-primary {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.page {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 60px;
|
||||
grid-template-columns: 1fr;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: var(--size);
|
||||
background: var(--background-light);
|
||||
border-radius: 8px;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.json {
|
||||
white-space: pre;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: normal;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
color: gray;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="sam-flex-column" style="overflow-y: auto">
|
||||
<div class="sam-text-header">
|
||||
<span id="titleSpan" style="font-weight: 400 !important"></span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="host-INSERT sam-align-self-center sam-text-muted"
|
||||
style="font-weight: 500"
|
||||
></span>
|
||||
|
||||
<!-- Card for getPublicKey -->
|
||||
<div id="cardGetPublicKey" class="card sam-mt sam-ml sam-mr">
|
||||
<span style="text-align: center">
|
||||
<b><span class="host-INSERT color-primary"></span></b>
|
||||
is requesting permission to<br />
|
||||
<br />
|
||||
<b class="color-primary">read your public key</b> <br />
|
||||
<br />
|
||||
<span>
|
||||
for the selected identity
|
||||
<span
|
||||
style="font-weight: 500"
|
||||
class="nick-INSERT color-primary"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Card for getRelays -->
|
||||
<div id="cardGetRelays" class="card sam-mt sam-ml sam-mr">
|
||||
<span style="text-align: center">
|
||||
<b><span class="host-INSERT color-primary"></span></b>
|
||||
is requesting permission to<br />
|
||||
<br />
|
||||
<b class="color-primary">read your relays</b> <br />
|
||||
<br />
|
||||
<span>
|
||||
for the selected identity
|
||||
<span
|
||||
style="font-weight: 500"
|
||||
class="nick-INSERT color-primary"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Card for signEvent -->
|
||||
<div id="cardSignEvent" class="card sam-mt sam-ml sam-mr">
|
||||
<span style="text-align: center">
|
||||
<b><span class="host-INSERT color-primary"></span></b>
|
||||
is requesting permission to<br />
|
||||
<br />
|
||||
<b class="color-primary">sign an event</b> (kind
|
||||
<span id="kindSpan"></span>) <br />
|
||||
<br />
|
||||
<span>
|
||||
for the selected identity
|
||||
<span
|
||||
style="font-weight: 500"
|
||||
class="nick-INSERT color-primary"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Card2 for signEvent -->
|
||||
<div id="card2SignEvent" class="card sam-mt sam-ml sam-mr">
|
||||
<div id="card2SignEvent_json" class="json"></div>
|
||||
</div>
|
||||
|
||||
<!-- Card for nip04.encrypt -->
|
||||
<div id="cardNip04Encrypt" class="card sam-mt sam-ml sam-mr">
|
||||
<span style="text-align: center">
|
||||
<b><span class="host-INSERT color-primary"></span></b>
|
||||
is requesting permission to<br />
|
||||
<br />
|
||||
<b class="color-primary">encrypt a text</b> (NIP04) <br />
|
||||
<br />
|
||||
<span>
|
||||
for the selected identity
|
||||
<span
|
||||
style="font-weight: 500"
|
||||
class="nick-INSERT color-primary"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Card2 for nip04.encrypt -->
|
||||
<div id="card2Nip04Encrypt" class="card sam-mt sam-ml sam-mr">
|
||||
<div id="card2Nip04Encrypt_text" class="text"></div>
|
||||
</div>
|
||||
|
||||
<!-- Card for nip04.decrypt -->
|
||||
<div id="cardNip04Decrypt" class="card sam-mt sam-ml sam-mr">
|
||||
<span style="text-align: center">
|
||||
<b><span class="host-INSERT color-primary"></span></b>
|
||||
is requesting permission to<br />
|
||||
<br />
|
||||
<b class="color-primary">decrypt a text</b> (NIP04) <br />
|
||||
<br />
|
||||
<span>
|
||||
for the selected identity
|
||||
<span
|
||||
style="font-weight: 500"
|
||||
class="nick-INSERT color-primary"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Card2 for nip04.decrypt -->
|
||||
<div id="card2Nip04Decrypt" class="card sam-mt sam-ml sam-mr">
|
||||
<div id="card2Nip04Decrypt_text" class="text"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!------------->
|
||||
<!-- ACTIONS -->
|
||||
<!------------->
|
||||
<div class="sam-footer-grid-2">
|
||||
<div class="btn-group">
|
||||
<button id="rejectButton" type="button" class="btn btn-secondary">
|
||||
Reject
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary dropdown-toggle dropdown-toggle-split"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<button id="rejectJustOnceButton" class="dropdown-item">
|
||||
just once
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button id="approveButton" type="button" class="btn btn-primary">
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary dropdown-toggle dropdown-toggle-split"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<button id="approveJustOnceButton" class="dropdown-item" href="#">
|
||||
just once
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="prompt.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1
projects/chrome/src/app/app.component.html
Normal file
1
projects/chrome/src/app/app.component.html
Normal file
@@ -0,0 +1 @@
|
||||
<router-outlet></router-outlet>
|
||||
0
projects/chrome/src/app/app.component.scss
Normal file
0
projects/chrome/src/app/app.component.scss
Normal file
29
projects/chrome/src/app/app.component.spec.ts
Normal file
29
projects/chrome/src/app/app.component.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AppComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have the 'chrome' title`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('chrome');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, chrome');
|
||||
});
|
||||
});
|
||||
20
projects/chrome/src/app/app.component.ts
Normal file
20
projects/chrome/src/app/app.component.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { LoggerService } from '@common';
|
||||
import { StartupService } from './services/startup/startup.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss',
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
readonly #startup = inject(StartupService);
|
||||
readonly #logger = inject(LoggerService);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.#logger.initialize('Gooti Chrome Extension');
|
||||
this.#startup.startOver();
|
||||
}
|
||||
}
|
||||
8
projects/chrome/src/app/app.config.ts
Normal file
8
projects/chrome/src/app/app.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)]
|
||||
};
|
||||
90
projects/chrome/src/app/app.routes.ts
Normal file
90
projects/chrome/src/app/app.routes.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { HomeComponent } from './components/home/home.component';
|
||||
import { VaultLoginComponent } from './components/vault-login/vault-login.component';
|
||||
import { VaultCreateComponent } from './components/vault-create/vault-create.component';
|
||||
import { HomeComponent as VaultCreateHomeComponent } from './components/vault-create/home/home.component';
|
||||
import { NewComponent as VaultCreateNewComponent } from './components/vault-create/new/new.component';
|
||||
import { WelcomeComponent } from './components/welcome/welcome.component';
|
||||
import { IdentitiesComponent } from './components/home/identities/identities.component';
|
||||
import { IdentityComponent } from './components/home/identity/identity.component';
|
||||
import { InfoComponent } from './components/home/info/info.component';
|
||||
import { SettingsComponent } from './components/home/settings/settings.component';
|
||||
import { NewIdentityComponent } from './components/new-identity/new-identity.component';
|
||||
import { EditIdentityComponent } from './components/edit-identity/edit-identity.component';
|
||||
import { HomeComponent as EditIdentityHomeComponent } from './components/edit-identity/home/home.component';
|
||||
import { KeysComponent as EditIdentityKeysComponent } from './components/edit-identity/keys/keys.component';
|
||||
import { PermissionsComponent as EditIdentityPermissionsComponent } from './components/edit-identity/permissions/permissions.component';
|
||||
import { RelaysComponent as EditIdentityRelaysComponent } from './components/edit-identity/relays/relays.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: 'welcome',
|
||||
component: WelcomeComponent,
|
||||
},
|
||||
{
|
||||
path: 'vault-login',
|
||||
component: VaultLoginComponent,
|
||||
},
|
||||
{
|
||||
path: 'vault-create',
|
||||
component: VaultCreateComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'home',
|
||||
component: VaultCreateHomeComponent,
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
component: VaultCreateNewComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'home',
|
||||
component: HomeComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'identities',
|
||||
component: IdentitiesComponent,
|
||||
},
|
||||
{
|
||||
path: 'identity',
|
||||
component: IdentityComponent,
|
||||
},
|
||||
{
|
||||
path: 'info',
|
||||
component: InfoComponent,
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
component: SettingsComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'new-identity',
|
||||
component: NewIdentityComponent,
|
||||
},
|
||||
{
|
||||
path: 'edit-identity/:id',
|
||||
component: EditIdentityComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'home',
|
||||
component: EditIdentityHomeComponent,
|
||||
},
|
||||
{
|
||||
path: 'keys',
|
||||
component: EditIdentityKeysComponent,
|
||||
},
|
||||
{
|
||||
path: 'permissions',
|
||||
component: EditIdentityPermissionsComponent,
|
||||
},
|
||||
{
|
||||
path: 'relays',
|
||||
component: EditIdentityRelaysComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
29
projects/chrome/src/app/common/data/chrome-meta-handler.ts
Normal file
29
projects/chrome/src/app/common/data/chrome-meta-handler.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { GootiMetaData, GootiMetaHandler } from '@common';
|
||||
|
||||
export class ChromeMetaHandler extends GootiMetaHandler {
|
||||
async loadFullData(): Promise<Partial<Record<string, any>>> {
|
||||
const dataWithPossibleAlienProperties = await chrome.storage.local.get(
|
||||
null
|
||||
);
|
||||
|
||||
if (Object.keys(dataWithPossibleAlienProperties).length === 0) {
|
||||
return dataWithPossibleAlienProperties;
|
||||
}
|
||||
|
||||
const data: Partial<Record<string, any>> = {};
|
||||
this.metaProperties.forEach((property) => {
|
||||
data[property] = dataWithPossibleAlienProperties[property];
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async saveFullData(data: GootiMetaData): Promise<void> {
|
||||
await chrome.storage.local.set(data);
|
||||
}
|
||||
|
||||
async clearData(): Promise<void> {
|
||||
await chrome.storage.local.remove(this.metaProperties);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { BrowserSessionData, BrowserSessionHandler } from '@common';
|
||||
|
||||
export class ChromeSessionHandler extends BrowserSessionHandler {
|
||||
async loadFullData(): Promise<Partial<Record<string, any>>> {
|
||||
return chrome.storage.session.get(null);
|
||||
}
|
||||
|
||||
async saveFullData(data: BrowserSessionData): Promise<void> {
|
||||
await chrome.storage.session.set(data);
|
||||
}
|
||||
|
||||
async clearData(): Promise<void> {
|
||||
await chrome.storage.session.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
BrowserSyncData,
|
||||
BrowserSyncHandler,
|
||||
Identity_ENCRYPTED,
|
||||
Permission_ENCRYPTED,
|
||||
Relay_ENCRYPTED,
|
||||
} from '@common';
|
||||
|
||||
/**
|
||||
* Handles the browser "sync data" when the user does not want to sync anything.
|
||||
* It uses the chrome.storage.local API to store the data. Since we also use this API
|
||||
* to store local Gooti system data (like the user's decision to not sync), we
|
||||
* have to exclude these properties from the sync data.
|
||||
*/
|
||||
export class ChromeSyncNoHandler extends BrowserSyncHandler {
|
||||
async loadUnmigratedData(): Promise<Partial<Record<string, any>>> {
|
||||
const data = await chrome.storage.local.get(null);
|
||||
|
||||
// Remove any available "ignore properties".
|
||||
this.ignoreProperties.forEach((property) => {
|
||||
delete data[property];
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
async saveAndSetFullData(data: BrowserSyncData): Promise<void> {
|
||||
await chrome.storage.local.set(data);
|
||||
this.setFullData(data);
|
||||
}
|
||||
|
||||
async saveAndSetPartialData_Permissions(data: {
|
||||
permissions: Permission_ENCRYPTED[];
|
||||
}): Promise<void> {
|
||||
await chrome.storage.local.set(data);
|
||||
this.setPartialData_Permissions(data);
|
||||
}
|
||||
|
||||
async saveAndSetPartialData_Identities(data: {
|
||||
identities: Identity_ENCRYPTED[];
|
||||
}): Promise<void> {
|
||||
await chrome.storage.local.set(data);
|
||||
this.setPartialData_Identities(data);
|
||||
}
|
||||
|
||||
async saveAndSetPartialData_SelectedIdentityId(data: {
|
||||
selectedIdentityId: string | null;
|
||||
}): Promise<void> {
|
||||
await chrome.storage.local.set(data);
|
||||
this.setPartialData_SelectedIdentityId(data);
|
||||
}
|
||||
|
||||
async saveAndSetPartialData_Relays(data: {
|
||||
relays: Relay_ENCRYPTED[];
|
||||
}): Promise<void> {
|
||||
await chrome.storage.local.set(data);
|
||||
this.setPartialData_Relays(data);
|
||||
}
|
||||
|
||||
async clearData(): Promise<void> {
|
||||
const props = Object.keys(await this.loadUnmigratedData());
|
||||
await chrome.storage.local.remove(props);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
BrowserSyncData,
|
||||
Identity_ENCRYPTED,
|
||||
Permission_ENCRYPTED,
|
||||
BrowserSyncHandler,
|
||||
Relay_ENCRYPTED,
|
||||
} from '@common';
|
||||
|
||||
/**
|
||||
* Handles the browser sync operations when the browser sync is enabled.
|
||||
* If it's not enabled, it behaves like the local extension storage (which is fine).
|
||||
*/
|
||||
export class ChromeSyncYesHandler extends BrowserSyncHandler {
|
||||
async loadUnmigratedData(): Promise<Partial<Record<string, any>>> {
|
||||
return await chrome.storage.sync.get(null);
|
||||
}
|
||||
|
||||
async saveAndSetFullData(data: BrowserSyncData): Promise<void> {
|
||||
await chrome.storage.sync.set(data);
|
||||
this.setFullData(data);
|
||||
}
|
||||
|
||||
async saveAndSetPartialData_Permissions(data: {
|
||||
permissions: Permission_ENCRYPTED[];
|
||||
}): Promise<void> {
|
||||
await chrome.storage.sync.set(data);
|
||||
this.setPartialData_Permissions(data);
|
||||
}
|
||||
|
||||
async saveAndSetPartialData_Identities(data: {
|
||||
identities: Identity_ENCRYPTED[];
|
||||
}): Promise<void> {
|
||||
await chrome.storage.sync.set(data);
|
||||
this.setPartialData_Identities(data);
|
||||
}
|
||||
|
||||
async saveAndSetPartialData_SelectedIdentityId(data: {
|
||||
selectedIdentityId: string | null;
|
||||
}): Promise<void> {
|
||||
await chrome.storage.sync.set(data);
|
||||
this.setPartialData_SelectedIdentityId(data);
|
||||
}
|
||||
|
||||
async saveAndSetPartialData_Relays(data: {
|
||||
relays: Relay_ENCRYPTED[];
|
||||
}): Promise<void> {
|
||||
await chrome.storage.sync.set(data);
|
||||
this.setPartialData_Relays(data);
|
||||
}
|
||||
|
||||
async clearData(): Promise<void> {
|
||||
await chrome.storage.sync.clear();
|
||||
}
|
||||
}
|
||||
95
projects/chrome/src/app/common/extensions/array.ts
Normal file
95
projects/chrome/src/app/common/extensions/array.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare global {
|
||||
interface Array<T> {
|
||||
/**
|
||||
* Sorts the array by the provided property and returns a new sorted array.
|
||||
* Default sorting is ASC. You can apply DESC sorting by using the optional parameter "order = 'desc'"
|
||||
*/
|
||||
sortBy<K>(keyFunction: (t: T) => K, order?: 'asc' | 'desc'): T[];
|
||||
|
||||
/** Check if the array is empty. */
|
||||
empty(): boolean;
|
||||
|
||||
groupBy<K, R>(
|
||||
keyFunction: (t: T) => K,
|
||||
reduceFn: (items: T[]) => R
|
||||
): Map<K, R>;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.prototype.empty) {
|
||||
Array.prototype.empty = function (): boolean {
|
||||
return this.length === 0;
|
||||
};
|
||||
}
|
||||
|
||||
if (!Array.prototype.sortBy) {
|
||||
Array.prototype.sortBy = function <T, K>(
|
||||
keyFunction: (t: T) => K,
|
||||
order?: string
|
||||
): T[] {
|
||||
if (this.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// determine sort order (asc or desc / asc is default)
|
||||
let asc = true;
|
||||
if (order === 'desc') {
|
||||
asc = false;
|
||||
}
|
||||
|
||||
const arrayClone = Array.from(this) as any[];
|
||||
const firstSortProperty = keyFunction(arrayClone[0]);
|
||||
|
||||
if (typeof firstSortProperty === 'string') {
|
||||
// string in-place sort
|
||||
arrayClone.sort((a, b) => {
|
||||
if (asc) {
|
||||
return ('' + (keyFunction(a) as unknown as string)).localeCompare(
|
||||
keyFunction(b) as unknown as string
|
||||
);
|
||||
}
|
||||
|
||||
return ('' + (keyFunction(b) as unknown as string)).localeCompare(
|
||||
keyFunction(a) as unknown as string
|
||||
);
|
||||
});
|
||||
} else if (typeof firstSortProperty === 'number') {
|
||||
// number in-place sort
|
||||
if (asc) {
|
||||
arrayClone.sort(
|
||||
(a, b) => Number(keyFunction(a)) - Number(keyFunction(b))
|
||||
);
|
||||
} else {
|
||||
arrayClone.sort(
|
||||
(a, b) => Number(keyFunction(b)) - Number(keyFunction(a))
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error('sortBy is not implemented for that type!');
|
||||
}
|
||||
|
||||
return arrayClone;
|
||||
};
|
||||
}
|
||||
|
||||
if (!Array.prototype.groupBy) {
|
||||
Array.prototype.groupBy = function <T>(
|
||||
fn: (item: T) => any,
|
||||
reduceFn: (items: T[]) => any
|
||||
): Map<any, any> {
|
||||
const result = new Map<any, any>();
|
||||
|
||||
const distinctKeys = new Set<any>(this.map((x) => fn(x)));
|
||||
|
||||
for (const distinctKey of distinctKeys) {
|
||||
const distinctKeyItems = this.filter((x) => fn(x) === distinctKey);
|
||||
|
||||
result.set(distinctKey, reduceFn(distinctKeyItems));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="custom-header">
|
||||
<lib-icon-button
|
||||
class="button"
|
||||
icon="chevron-left"
|
||||
(click)="onClickCancel()"
|
||||
></lib-icon-button>
|
||||
|
||||
<span class="text">{{ identity?.nick }} </span>
|
||||
</div>
|
||||
|
||||
<div class="edit-identity-outlet">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
@@ -0,0 +1,47 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
|
||||
.custom-header {
|
||||
padding-top: var(--size);
|
||||
padding-bottom: var(--size);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto;
|
||||
align-items: center;
|
||||
background: var(--background);
|
||||
|
||||
.button {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 2;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 2;
|
||||
justify-self: start;
|
||||
margin-left: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.text {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 2;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 2;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
justify-self: center;
|
||||
height: 32px;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-identity-outlet {
|
||||
flex-grow: 1;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EditIdentityComponent } from './edit-identity.component';
|
||||
|
||||
describe('EditIdentityComponent', () => {
|
||||
let component: EditIdentityComponent;
|
||||
let fixture: ComponentFixture<EditIdentityComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [EditIdentityComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(EditIdentityComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
|
||||
import { IconButtonComponent, Identity_DECRYPTED, StorageService } from '@common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-identity',
|
||||
templateUrl: './edit-identity.component.html',
|
||||
styleUrl: './edit-identity.component.scss',
|
||||
imports: [RouterOutlet, IconButtonComponent],
|
||||
})
|
||||
export class EditIdentityComponent implements OnInit {
|
||||
identity?: Identity_DECRYPTED;
|
||||
previousRoute?: string;
|
||||
|
||||
readonly #activatedRoute = inject(ActivatedRoute);
|
||||
readonly #storage = inject(StorageService);
|
||||
readonly #router = inject(Router);
|
||||
|
||||
constructor() {
|
||||
// Must be called in the constructor and NOT in ngOnInit.
|
||||
this.previousRoute = this.#router
|
||||
.getCurrentNavigation()
|
||||
?.previousNavigation?.extractedUrl.toString();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const selectedIdentityId = this.#activatedRoute.snapshot.params['id'];
|
||||
if (!selectedIdentityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.identity = this.#storage
|
||||
.getBrowserSessionHandler()
|
||||
.browserSessionData?.identities.find((x) => x.id === selectedIdentityId);
|
||||
}
|
||||
|
||||
onClickCancel() {
|
||||
if (!this.previousRoute) {
|
||||
return;
|
||||
}
|
||||
this.#router.navigateByUrl(this.previousRoute);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<lib-nav-item text="Keys" (click)="onClickNavigateTo('keys')"></lib-nav-item>
|
||||
|
||||
<lib-nav-item
|
||||
text="Relays"
|
||||
(click)="onClickNavigateTo('relays')"
|
||||
></lib-nav-item>
|
||||
|
||||
<lib-nav-item
|
||||
text="Permissions"
|
||||
(click)="onClickNavigateTo('permissions')"
|
||||
></lib-nav-item>
|
||||
|
||||
<div class="sam-flex-grow"></div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-danger"
|
||||
(click)="
|
||||
confirm.show(
|
||||
'Do you really want to delete this identity?',
|
||||
onConfirmDeletion.bind(this)
|
||||
)
|
||||
"
|
||||
>
|
||||
Delete Identity
|
||||
</button>
|
||||
|
||||
<lib-confirm #confirm> </lib-confirm>
|
||||
@@ -0,0 +1,8 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: var(--size);
|
||||
padding-right: var(--size);
|
||||
padding-bottom: var(--size);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HomeComponent } from './home.component';
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
let fixture: ComponentFixture<HomeComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [HomeComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HomeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { NavItemComponent } from '../../../../../../common/src/lib/components/nav-item/nav-item.component';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ConfirmComponent, Identity_DECRYPTED, StorageService } from '@common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
imports: [NavItemComponent, ConfirmComponent],
|
||||
templateUrl: './home.component.html',
|
||||
styleUrl: './home.component.scss',
|
||||
})
|
||||
export class HomeComponent implements OnInit {
|
||||
identity?: Identity_DECRYPTED;
|
||||
|
||||
readonly #activatedRoute = inject(ActivatedRoute);
|
||||
readonly #storage = inject(StorageService);
|
||||
readonly #router = inject(Router);
|
||||
|
||||
ngOnInit(): void {
|
||||
const identityId = this.#activatedRoute.parent?.snapshot.params['id'];
|
||||
if (!identityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#initialize(identityId);
|
||||
}
|
||||
|
||||
onClickNavigateTo(destination: 'keys' | 'permissions' | 'relays') {
|
||||
this.#router.navigateByUrl(
|
||||
`/edit-identity/${this.identity?.id}/${destination}`
|
||||
);
|
||||
}
|
||||
|
||||
async onConfirmDeletion() {
|
||||
await this.#storage.deleteIdentity(this.identity?.id);
|
||||
await this.#router.navigateByUrl('/home/identities');
|
||||
}
|
||||
|
||||
#initialize(selectedIdentityId: string) {
|
||||
this.identity = this.#storage
|
||||
.getBrowserSessionHandler()
|
||||
.browserSessionData?.identities.find((x) => x.id === selectedIdentityId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<div class="header-pane">
|
||||
<lib-icon-button
|
||||
icon="chevron-left"
|
||||
(click)="navigateBack()"
|
||||
></lib-icon-button>
|
||||
<span>Keys</span>
|
||||
</div>
|
||||
|
||||
@if(identity) {
|
||||
<span>Public Key</span>
|
||||
|
||||
<!-- PUBKEY NPUB -->
|
||||
<div class="sam-mt-h sam-flex-row gap">
|
||||
<span class="text-muted" style="width: 48px">NPUB</span>
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="pubkeyNpubInput"
|
||||
#pubkeyNpubInput
|
||||
type="text"
|
||||
class="form-control"
|
||||
[ngModel]="identity.pubkeyNpub"
|
||||
[readOnly]="true"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
(click)="
|
||||
copyToClipboard(identity.pubkeyNpub); toast.show('Copied to clipboard')
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="bi bi-copy"
|
||||
[class.bi-eye]="pubkeyNpubInput.type === 'password'"
|
||||
[class.bi-eye-slash]="pubkeyNpubInput.type === 'text'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PUBKEY HEX -->
|
||||
<div class="sam-mt-h sam-flex-row gap">
|
||||
<span class="text-muted" style="width: 48px">HEX</span>
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="pubkeyHexInput"
|
||||
#pubkeyHexInput
|
||||
type="text"
|
||||
class="form-control"
|
||||
[ngModel]="identity.pubkeyHex"
|
||||
[readOnly]="true"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
(click)="
|
||||
copyToClipboard(identity.pubkeyHex); toast.show('Copied to clipboard')
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="bi bi-copy"
|
||||
[class.bi-eye]="pubkeyHexInput.type === 'password'"
|
||||
[class.bi-eye-slash]="pubkeyHexInput.type === 'text'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="sam-mt-2">Private Key</span>
|
||||
|
||||
<!-- PRIVATE NSEC -->
|
||||
<div class="sam-mt-h sam-flex-row gap">
|
||||
<span class="text-muted" style="width: 48px">NSEC</span>
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="privkeyNsecInput"
|
||||
#privkeyNsecInput
|
||||
type="password"
|
||||
class="form-control"
|
||||
[ngModel]="identity.privkeyNsec"
|
||||
[readOnly]="true"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
(click)="
|
||||
copyToClipboard(identity.privkeyNsec); toast.show('Copied to clipboard')
|
||||
"
|
||||
>
|
||||
<i class="bi bi-copy"></i>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
(click)="toggleType(privkeyNsecInput)"
|
||||
>
|
||||
<i
|
||||
class="bi bi-eye"
|
||||
[class.bi-eye]="privkeyNsecInput.type === 'password'"
|
||||
[class.bi-eye-slash]="privkeyNsecInput.type === 'text'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PRIVATE HEX -->
|
||||
<div class="sam-mt-h sam-flex-row gap">
|
||||
<span class="text-muted" style="width: 48px">HEX</span>
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="privkeyHexInput"
|
||||
#privkeyHexInput
|
||||
type="password"
|
||||
class="form-control"
|
||||
[ngModel]="identity.privkeyHex"
|
||||
[readOnly]="true"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
(click)="
|
||||
copyToClipboard(identity.privkeyHex); toast.show('Copied to clipboard')
|
||||
"
|
||||
>
|
||||
<i class="bi bi-copy"></i>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
(click)="toggleType(privkeyHexInput)"
|
||||
>
|
||||
<i
|
||||
class="bi bi-eye"
|
||||
[class.bi-eye]="privkeyHexInput.type === 'password'"
|
||||
[class.bi-eye-slash]="privkeyHexInput.type === 'text'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<lib-toast #toast [bottom]="16"></lib-toast>
|
||||
@@ -0,0 +1,19 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: var(--size);
|
||||
padding-right: var(--size);
|
||||
|
||||
.header-pane {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
column-gap: var(--size-h);
|
||||
align-items: center;
|
||||
padding-bottom: var(--size);
|
||||
background-color: var(--background);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { KeysComponent } from './keys.component';
|
||||
|
||||
describe('KeysComponent', () => {
|
||||
let component: KeysComponent;
|
||||
let fixture: ComponentFixture<KeysComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [KeysComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(KeysComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import {
|
||||
NavComponent,
|
||||
NostrHelper,
|
||||
StorageService,
|
||||
ToastComponent,
|
||||
} from '@common';
|
||||
import { IconButtonComponent } from '../../../../../../common/src/lib/components/icon-button/icon-button.component';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
interface CustomIdentity {
|
||||
id: string;
|
||||
nick: string;
|
||||
privkeyNsec: string;
|
||||
privkeyHex: string;
|
||||
pubkeyNpub: string;
|
||||
pubkeyHex: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-keys',
|
||||
imports: [IconButtonComponent, FormsModule, ToastComponent],
|
||||
templateUrl: './keys.component.html',
|
||||
styleUrl: './keys.component.scss',
|
||||
})
|
||||
export class KeysComponent extends NavComponent implements OnInit {
|
||||
identity?: CustomIdentity;
|
||||
|
||||
readonly #activatedRoute = inject(ActivatedRoute);
|
||||
readonly #storage = inject(StorageService);
|
||||
|
||||
ngOnInit(): void {
|
||||
const identityId = this.#activatedRoute.parent?.snapshot.params['id'];
|
||||
if (!identityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#initialize(identityId);
|
||||
}
|
||||
|
||||
copyToClipboard(text: string) {
|
||||
navigator.clipboard.writeText(text);
|
||||
}
|
||||
|
||||
toggleType(element: HTMLInputElement) {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
} else {
|
||||
element.type = 'password';
|
||||
}
|
||||
}
|
||||
|
||||
async #initialize(identityId: string) {
|
||||
const identity = this.#storage
|
||||
.getBrowserSessionHandler()
|
||||
.browserSessionData?.identities.find((x) => x.id === identityId);
|
||||
|
||||
if (!identity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pubkey = NostrHelper.pubkeyFromPrivkey(identity.privkey);
|
||||
|
||||
this.identity = {
|
||||
id: identity.id,
|
||||
nick: identity.nick,
|
||||
privkeyHex: identity.privkey,
|
||||
privkeyNsec: NostrHelper.privkey2nsec(identity.privkey),
|
||||
pubkeyHex: pubkey,
|
||||
pubkeyNpub: NostrHelper.pubkey2npub(pubkey),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<div class="header-pane">
|
||||
<lib-icon-button icon="chevron-left" (click)="navigateBack()"></lib-icon-button>
|
||||
<span>Permissions</span>
|
||||
</div>
|
||||
|
||||
@if(hostsPermissions.length === 0) {
|
||||
<span class="text-muted" style="font-size: 12px">
|
||||
Nothing configured so far.
|
||||
</span>
|
||||
} @for(hostPermissions of hostsPermissions; track hostPermissions) {
|
||||
<div class="permissions-card">
|
||||
<span style="margin-bottom: 4px; font-weight: 500">
|
||||
{{ hostPermissions.host }}
|
||||
</span>
|
||||
|
||||
@for(permission of hostPermissions.permissions; track permission) {
|
||||
<div class="permission">
|
||||
<span
|
||||
[class.action-allow]="permission.methodPolicy === 'allow'"
|
||||
[class.action-deny]="permission.methodPolicy === 'deny'"
|
||||
>{{ permission.methodPolicy }}</span
|
||||
>
|
||||
<span class="text-muted">{{ permission.method }}</span>
|
||||
@if(typeof permission.kind !== 'undefined') {
|
||||
<span>(kind {{ permission.kind }})</span>
|
||||
}
|
||||
<div class="sam-flex-grow"></div>
|
||||
<lib-icon-button
|
||||
icon="trash"
|
||||
title="Revoke permission"
|
||||
(click)="onClickRevokePermission(permission)"
|
||||
></lib-icon-button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: var(--size);
|
||||
padding-right: var(--size);
|
||||
|
||||
.header-pane {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
column-gap: var(--size-h);
|
||||
align-items: center;
|
||||
padding-bottom: var(--size);
|
||||
background-color: var(--background);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.permissions-card {
|
||||
background: var(--background-light);
|
||||
border-radius: 8px;
|
||||
padding: calc(var(--size) / 2) var(--size);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.permission {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
column-gap: var(--size);
|
||||
font-size: 12px;
|
||||
margin-left: -8px;
|
||||
padding-left: 8px;
|
||||
margin-right: -8px;
|
||||
padding-right: 8px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--background-light-hover);
|
||||
}
|
||||
|
||||
.action-allow {
|
||||
background: var(--bs-green);
|
||||
border-radius: 4px;
|
||||
padding: 0 4px;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-deny {
|
||||
background: var(--bs-danger-border-subtle);
|
||||
border-radius: 4px;
|
||||
padding: 0 4px;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PermissionsComponent } from './permissions.component';
|
||||
|
||||
describe('PermissionsComponent', () => {
|
||||
let component: PermissionsComponent;
|
||||
let fixture: ComponentFixture<PermissionsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PermissionsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PermissionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import {
|
||||
IconButtonComponent,
|
||||
Identity_DECRYPTED,
|
||||
NavComponent,
|
||||
Permission_DECRYPTED,
|
||||
StorageService,
|
||||
} from '@common';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
interface HostPermissions {
|
||||
host: string;
|
||||
permissions: Permission_DECRYPTED[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-permissions',
|
||||
imports: [IconButtonComponent],
|
||||
templateUrl: './permissions.component.html',
|
||||
styleUrl: './permissions.component.scss',
|
||||
})
|
||||
export class PermissionsComponent extends NavComponent implements OnInit {
|
||||
identity?: Identity_DECRYPTED;
|
||||
hostsPermissions: HostPermissions[] = [];
|
||||
|
||||
readonly #activatedRoute = inject(ActivatedRoute);
|
||||
readonly #storage = inject(StorageService);
|
||||
|
||||
ngOnInit(): void {
|
||||
const selectedIdentityId =
|
||||
this.#activatedRoute.parent?.snapshot.params['id'];
|
||||
if (!selectedIdentityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#initialize(selectedIdentityId);
|
||||
}
|
||||
|
||||
async onClickRevokePermission(permission: Permission_DECRYPTED) {
|
||||
await this.#storage.deletePermission(permission.id);
|
||||
this.#buildHostsPermissions(this.identity?.id);
|
||||
}
|
||||
|
||||
#initialize(identityId: string) {
|
||||
this.identity = this.#storage
|
||||
.getBrowserSessionHandler()
|
||||
.browserSessionData?.identities.find((x) => x.id === identityId);
|
||||
|
||||
if (!this.identity) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#buildHostsPermissions(identityId);
|
||||
}
|
||||
|
||||
#buildHostsPermissions(identityId: string | undefined) {
|
||||
if (!identityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hostsPermissions = [];
|
||||
|
||||
const hostPermissions = (
|
||||
this.#storage.getBrowserSessionHandler().browserSessionData
|
||||
?.permissions ?? []
|
||||
)
|
||||
.filter((x) => x.identityId === identityId)
|
||||
.sortBy((x) => x.host)
|
||||
.groupBy(
|
||||
(x) => x.host,
|
||||
(y) => y
|
||||
);
|
||||
|
||||
hostPermissions.forEach((permissions, host) => {
|
||||
this.hostsPermissions.push({
|
||||
host: host,
|
||||
permissions: permissions.sortBy((x) => x.method),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<!-- RELAY_TEMPLATE -->
|
||||
<ng-template #relayTemplate let-relay="relay">
|
||||
<div class="sam-flex-row gap relay">
|
||||
<div class="sam-flex-column sam-flex-grow">
|
||||
<span>{{ relay.url | visualRelay }}</span>
|
||||
<div class="sam-flex-row gap-h">
|
||||
<lib-relay-rw
|
||||
type="read"
|
||||
[(model)]="relay.read"
|
||||
(modelChange)="onRelayChanged(relay)"
|
||||
></lib-relay-rw>
|
||||
<lib-relay-rw
|
||||
type="write"
|
||||
[(model)]="relay.write"
|
||||
(modelChange)="onRelayChanged(relay)"
|
||||
></lib-relay-rw>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<lib-icon-button
|
||||
icon="trash"
|
||||
title="Remove relay"
|
||||
(click)="onClickRemoveRelay(relay)"
|
||||
style="margin-top: 4px"
|
||||
></lib-icon-button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div class="header-pane">
|
||||
<lib-icon-button
|
||||
icon="chevron-left"
|
||||
(click)="navigateBack()"
|
||||
></lib-icon-button>
|
||||
<span>Relays</span>
|
||||
</div>
|
||||
|
||||
<div class="sam-mb-2 sam-flex-row gap">
|
||||
<div class="sam-flex-column sam-flex-grow">
|
||||
<input
|
||||
type="text"
|
||||
(focus)="addRelayInputHasFocus = true"
|
||||
(blur)="addRelayInputHasFocus = false"
|
||||
[placeholder]="addRelayInputHasFocus ? 'server.com' : 'Add a relay'"
|
||||
class="form-control"
|
||||
[(ngModel)]="newRelay.url"
|
||||
(ngModelChange)="evaluateCanAdd()"
|
||||
/>
|
||||
<div class="sam-flex-row gap-h" style="margin-top: 4px">
|
||||
<lib-relay-rw
|
||||
class="sam-flex-grow"
|
||||
type="read"
|
||||
[(model)]="newRelay.read"
|
||||
(modelChange)="evaluateCanAdd()"
|
||||
></lib-relay-rw>
|
||||
<lib-relay-rw
|
||||
class="sam-flex-grow"
|
||||
type="write"
|
||||
[(model)]="newRelay.write"
|
||||
(modelChange)="evaluateCanAdd()"
|
||||
></lib-relay-rw>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
style="height: 100%"
|
||||
(click)="onClickAddRelay()"
|
||||
[disabled]="!canAdd"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@for(relay of relays; track relay) {
|
||||
<ng-container
|
||||
*ngTemplateOutlet="relayTemplate; context: { relay: relay }"
|
||||
></ng-container>
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: var(--size);
|
||||
padding-right: var(--size);
|
||||
|
||||
.header-pane {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
column-gap: var(--size-h);
|
||||
align-items: center;
|
||||
padding-bottom: var(--size);
|
||||
background-color: var(--background);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.relay {
|
||||
margin-bottom: 4px;
|
||||
padding: 4px 8px 6px 8px;
|
||||
border-radius: 8px;
|
||||
background: var(--background-light);
|
||||
|
||||
&:hover {
|
||||
background: var(--background-light-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RelaysComponent } from './relays.component';
|
||||
|
||||
describe('RelaysComponent', () => {
|
||||
let component: RelaysComponent;
|
||||
let fixture: ComponentFixture<RelaysComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RelaysComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RelaysComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
import { NgTemplateOutlet } from '@angular/common';
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import {
|
||||
IconButtonComponent,
|
||||
Identity_DECRYPTED,
|
||||
NavComponent,
|
||||
Relay_DECRYPTED,
|
||||
RelayRwComponent,
|
||||
StorageService,
|
||||
VisualRelayPipe,
|
||||
} from '@common';
|
||||
|
||||
interface NewRelay {
|
||||
url: string;
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-relays',
|
||||
imports: [
|
||||
IconButtonComponent,
|
||||
FormsModule,
|
||||
RelayRwComponent,
|
||||
NgTemplateOutlet,
|
||||
VisualRelayPipe,
|
||||
],
|
||||
templateUrl: './relays.component.html',
|
||||
styleUrl: './relays.component.scss',
|
||||
})
|
||||
export class RelaysComponent extends NavComponent implements OnInit {
|
||||
identity?: Identity_DECRYPTED;
|
||||
relays: Relay_DECRYPTED[] = [];
|
||||
addRelayInputHasFocus = false;
|
||||
newRelay: NewRelay = {
|
||||
url: '',
|
||||
read: true,
|
||||
write: true,
|
||||
};
|
||||
canAdd = false;
|
||||
|
||||
readonly #activatedRoute = inject(ActivatedRoute);
|
||||
readonly #storage = inject(StorageService);
|
||||
|
||||
ngOnInit(): void {
|
||||
const selectedIdentityId =
|
||||
this.#activatedRoute.parent?.snapshot.params['id'];
|
||||
if (!selectedIdentityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#loadData(selectedIdentityId);
|
||||
}
|
||||
|
||||
evaluateCanAdd() {
|
||||
let canAdd = true;
|
||||
|
||||
if (!this.newRelay.url) {
|
||||
canAdd = false;
|
||||
} else if (!this.newRelay.read && !this.newRelay.write) {
|
||||
canAdd = false;
|
||||
}
|
||||
|
||||
this.canAdd = canAdd;
|
||||
}
|
||||
|
||||
async onClickRemoveRelay(relay: Relay_DECRYPTED) {
|
||||
if (!this.identity) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.#storage.deleteRelay(relay.id);
|
||||
this.#loadData(this.identity.id);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
async onClickAddRelay() {
|
||||
if (!this.identity) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.#storage.addRelay({
|
||||
identityId: this.identity.id,
|
||||
url: 'wss://' + this.newRelay.url.toLowerCase(),
|
||||
read: this.newRelay.read,
|
||||
write: this.newRelay.write,
|
||||
});
|
||||
|
||||
this.newRelay = {
|
||||
url: '',
|
||||
read: true,
|
||||
write: true,
|
||||
};
|
||||
this.evaluateCanAdd();
|
||||
this.#loadData(this.identity.id);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
async onRelayChanged(relay: Relay_DECRYPTED) {
|
||||
try {
|
||||
await this.#storage.updateRelay(relay);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
#loadData(identityId: string) {
|
||||
this.identity = this.#storage
|
||||
.getBrowserSessionHandler()
|
||||
.browserSessionData?.identities.find((x) => x.id === identityId);
|
||||
|
||||
const relays: Relay_DECRYPTED[] = [];
|
||||
(this.#storage.getBrowserSessionHandler().browserSessionData?.relays ?? [])
|
||||
.filter((x) => x.identityId === identityId)
|
||||
.forEach((x) => {
|
||||
relays.push(JSON.parse(JSON.stringify(x)));
|
||||
});
|
||||
this.relays = relays;
|
||||
}
|
||||
}
|
||||
36
projects/chrome/src/app/components/home/home.component.html
Normal file
36
projects/chrome/src/app/components/home/home.component.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<div class="tab-content">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<a
|
||||
class="tab"
|
||||
routerLink="/home/identity"
|
||||
routerLinkActive="active"
|
||||
title="Your selected identity"
|
||||
>
|
||||
<i class="bi bi-person-circle"></i>
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="tab"
|
||||
routerLink="/home/identities"
|
||||
routerLinkActive="active"
|
||||
title="Identities"
|
||||
>
|
||||
<i class="bi bi-people-fill"></i>
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="tab"
|
||||
routerLink="/home/settings"
|
||||
routerLinkActive="active"
|
||||
title="Settings"
|
||||
>
|
||||
<i class="bi bi-gear"></i>
|
||||
</a>
|
||||
|
||||
<a class="tab" routerLink="/home/info" routerLinkActive="active" title="Info">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
43
projects/chrome/src/app/components/home/home.component.scss
Normal file
43
projects/chrome/src/app/components/home/home.component.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.tab-content {
|
||||
height: calc(100% - 60px);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
height: 60px;
|
||||
min-height: 60px;
|
||||
background: var(--background-light);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
a {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
|
||||
color: gray;
|
||||
border-top: 3px solid transparent;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--background-light-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #ffffff;
|
||||
border-top: 3px solid #0d6efd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HomeComponent } from './home.component';
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
let fixture: ComponentFixture<HomeComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [HomeComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HomeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
10
projects/chrome/src/app/components/home/home.component.ts
Normal file
10
projects/chrome/src/app/components/home/home.component.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterModule, RouterOutlet } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
templateUrl: './home.component.html',
|
||||
styleUrl: './home.component.scss',
|
||||
imports: [RouterOutlet, RouterModule],
|
||||
})
|
||||
export class HomeComponent {}
|
||||
@@ -0,0 +1,78 @@
|
||||
<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->
|
||||
<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->
|
||||
<div class="custom-header" style="position: sticky; top: 0">
|
||||
<span class="text">Identities </span>
|
||||
|
||||
<button class="button btn btn-primary btn-sm" (click)="onClickNewIdentity()">
|
||||
<div class="sam-flex-row gap-h">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
<span>New</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@let sessionData = storage.getBrowserSessionHandler().browserSessionData;
|
||||
<!-- - -->
|
||||
@let identities = sessionData?.identities ?? []; @if(identities.length === 0) {
|
||||
<div
|
||||
style="
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<span class="sam-text-muted">
|
||||
Create your first identity by clicking on the button in the upper right
|
||||
corner.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
} @for(identity of identities; track identity) {
|
||||
<div
|
||||
class="identity"
|
||||
style="overflow: hidden"
|
||||
(click)="onClickEditIdentity(identity)"
|
||||
>
|
||||
@let isSelected = identity.id === sessionData?.selectedIdentityId;
|
||||
|
||||
<span
|
||||
class="no-select"
|
||||
style="overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap"
|
||||
[class.not-active]="!isSelected"
|
||||
>
|
||||
{{ identity.nick }}
|
||||
</span>
|
||||
|
||||
<div class="sam-flex-grow"></div>
|
||||
|
||||
@if(isSelected) {
|
||||
<lib-icon-button
|
||||
icon="star-fill"
|
||||
title="Edit identity"
|
||||
style="pointer-events: none; color: var(--bs-pink)"
|
||||
></lib-icon-button>
|
||||
}
|
||||
|
||||
<div class="buttons sam-flex-row gap-h">
|
||||
@if(!isSelected) {
|
||||
<lib-icon-button
|
||||
icon="star-fill"
|
||||
title="Select identity"
|
||||
(click)="
|
||||
onClickSwitchIdentity(identity.id, $event);
|
||||
toast.show('Identity changed')
|
||||
"
|
||||
></lib-icon-button>
|
||||
}
|
||||
</div>
|
||||
<lib-icon-button
|
||||
icon="arrow-right"
|
||||
title="Edit identity"
|
||||
style="pointer-events: none"
|
||||
></lib-icon-button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<lib-toast #toast></lib-toast>
|
||||
@@ -0,0 +1,68 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
padding-left: var(--size);
|
||||
padding-right: var(--size);
|
||||
|
||||
.custom-header {
|
||||
padding-top: var(--size);
|
||||
padding-bottom: var(--size);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto;
|
||||
align-items: center;
|
||||
background: var(--background);
|
||||
|
||||
.button {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 2;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 2;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.text {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 2;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 2;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
justify-self: center;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.identity {
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
padding-right: 8px;
|
||||
background: var(--background-light);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
.not-active {
|
||||
//color: #525b6a;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--background-light-hover);
|
||||
|
||||
.buttons {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { IdentitiesComponent } from './identities.component';
|
||||
|
||||
describe('IdentitiesComponent', () => {
|
||||
let component: IdentitiesComponent;
|
||||
let fixture: ComponentFixture<IdentitiesComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [IdentitiesComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(IdentitiesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
IconButtonComponent,
|
||||
Identity_DECRYPTED,
|
||||
StorageService,
|
||||
ToastComponent,
|
||||
} from '@common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-identities',
|
||||
templateUrl: './identities.component.html',
|
||||
styleUrl: './identities.component.scss',
|
||||
imports: [IconButtonComponent, ToastComponent],
|
||||
})
|
||||
export class IdentitiesComponent {
|
||||
readonly storage = inject(StorageService);
|
||||
|
||||
readonly #router = inject(Router);
|
||||
|
||||
onClickNewIdentity() {
|
||||
this.#router.navigateByUrl('/new-identity');
|
||||
}
|
||||
|
||||
onClickEditIdentity(identity: Identity_DECRYPTED) {
|
||||
this.#router.navigateByUrl(`/edit-identity/${identity.id}/home`);
|
||||
}
|
||||
|
||||
async onClickSwitchIdentity(identityId: string, event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
await this.storage.switchIdentity(identityId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->
|
||||
<div class="sam-text-header">
|
||||
<span>You</span>
|
||||
</div>
|
||||
|
||||
<div class="vertically-centered">
|
||||
<div class="sam-flex-column center">
|
||||
<div class="sam-flex-column gap center">
|
||||
<div class="picture-frame" [class.padding]="!loadedData.profile?.image">
|
||||
<img
|
||||
[src]="
|
||||
!loadedData.profile?.image
|
||||
? 'person-fill.svg'
|
||||
: loadedData.profile?.image
|
||||
"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->
|
||||
<span class="name" (click)="onClickShowDetails()">
|
||||
{{ selectedIdentity?.nick }}
|
||||
</span>
|
||||
|
||||
@if(loadedData.profile) {
|
||||
<div class="sam-flex-row gap-h">
|
||||
@if(loadedData.validating) {
|
||||
<i class="bi bi-circle color-activity"></i>
|
||||
} @else { @if(loadedData.nip05isValidated) {
|
||||
<i class="bi bi-patch-check sam-color-primary"></i>
|
||||
} @else {
|
||||
<i class="bi bi-exclamation-octagon-fill sam-color-danger"></i>
|
||||
} }
|
||||
|
||||
<span class="sam-color-primary">{{
|
||||
loadedData.profile.nip05 | visualNip05
|
||||
}}</span>
|
||||
</div>
|
||||
} @else {
|
||||
<span> </span>
|
||||
}
|
||||
|
||||
<lib-pubkey
|
||||
[value]="selectedIdentityNpub ?? 'na'"
|
||||
[first]="14"
|
||||
[last]="8"
|
||||
(click)="
|
||||
copyToClipboard(selectedIdentityNpub);
|
||||
toast.show('Copied to clipboard')
|
||||
"
|
||||
></lib-pubkey>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<lib-toast #toast></lib-toast>
|
||||
@@ -0,0 +1,41 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.vertically-centered {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
max-width: 343px;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.picture-frame {
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
border: 2px solid white;
|
||||
border-radius: 100%;
|
||||
&.padding {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.color-activity {
|
||||
color: var(--bs-border-color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { IdentityComponent } from './identity.component';
|
||||
|
||||
describe('IdentityComponent', () => {
|
||||
let component: IdentityComponent;
|
||||
let fixture: ComponentFixture<IdentityComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [IdentityComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(IdentityComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
Identity_DECRYPTED,
|
||||
NostrHelper,
|
||||
PubkeyComponent,
|
||||
StorageService,
|
||||
ToastComponent,
|
||||
VisualNip05Pipe,
|
||||
} from '@common';
|
||||
import NDK, { NDKUserProfile } from '@nostr-dev-kit/ndk';
|
||||
|
||||
interface LoadedData {
|
||||
profile: NDKUserProfile | undefined;
|
||||
nip05: string | undefined;
|
||||
nip05isValidated: boolean | undefined;
|
||||
validating: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-identity',
|
||||
imports: [PubkeyComponent, VisualNip05Pipe, ToastComponent],
|
||||
templateUrl: './identity.component.html',
|
||||
styleUrl: './identity.component.scss',
|
||||
})
|
||||
export class IdentityComponent implements OnInit {
|
||||
selectedIdentity: Identity_DECRYPTED | undefined;
|
||||
selectedIdentityNpub: string | undefined;
|
||||
loadedData: LoadedData = {
|
||||
profile: undefined,
|
||||
nip05: undefined,
|
||||
nip05isValidated: undefined,
|
||||
validating: false,
|
||||
};
|
||||
|
||||
readonly #storage = inject(StorageService);
|
||||
readonly #router = inject(Router);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.#loadData();
|
||||
}
|
||||
|
||||
copyToClipboard(pubkey: string | undefined) {
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(pubkey);
|
||||
}
|
||||
|
||||
onClickShowDetails() {
|
||||
if (!this.selectedIdentity) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#router.navigateByUrl(
|
||||
`/edit-identity/${this.selectedIdentity.id}/home`
|
||||
);
|
||||
}
|
||||
|
||||
async #loadData() {
|
||||
try {
|
||||
const selectedIdentityId =
|
||||
this.#storage.getBrowserSessionHandler().browserSessionData
|
||||
?.selectedIdentityId ?? null;
|
||||
|
||||
const identity = this.#storage
|
||||
.getBrowserSessionHandler()
|
||||
.browserSessionData?.identities.find(
|
||||
(x) => x.id === selectedIdentityId
|
||||
);
|
||||
|
||||
if (!identity) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedIdentity = identity;
|
||||
const pubkey = NostrHelper.pubkeyFromPrivkey(identity.privkey);
|
||||
this.selectedIdentityNpub = NostrHelper.pubkey2npub(pubkey);
|
||||
|
||||
// Determine the user's relays to check for his profile.
|
||||
const relays =
|
||||
this.#storage
|
||||
.getBrowserSessionHandler()
|
||||
.browserSessionData?.relays.filter(
|
||||
(x) => x.identityId === identity.id
|
||||
) ?? [];
|
||||
if (relays.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relevantRelays = relays.filter((x) => x.write).map((x) => x.url);
|
||||
|
||||
// Fetch the user's profile.
|
||||
const ndk = new NDK({
|
||||
explicitRelayUrls: relevantRelays,
|
||||
});
|
||||
|
||||
await ndk.connect();
|
||||
|
||||
const user = ndk.getUser({
|
||||
pubkey: NostrHelper.pubkeyFromPrivkey(identity.privkey),
|
||||
//relayUrls: relevantRelays,
|
||||
});
|
||||
this.loadedData.profile = (await user.fetchProfile()) ?? undefined;
|
||||
if (this.loadedData.profile?.nip05) {
|
||||
this.loadedData.validating = true;
|
||||
this.loadedData.nip05isValidated =
|
||||
(await user.validateNip05(this.loadedData.profile.nip05)) ??
|
||||
undefined;
|
||||
this.loadedData.validating = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<div class="sam-text-header">
|
||||
<span> Gooti </span>
|
||||
</div>
|
||||
|
||||
<span>Version {{ version }}</span>
|
||||
|
||||
<br />
|
||||
|
||||
<span> Website </span>
|
||||
<a href="https://getgooti.com" target="_blank">www.getgooti.com</a>
|
||||
|
||||
<br />
|
||||
|
||||
<span> Source code</span>
|
||||
<a
|
||||
href="https://github.com/sam-hayes-org/gooti-extension"
|
||||
target="_blank"
|
||||
>
|
||||
github.com/sam-hayes-org/gooti-extension
|
||||
</a>
|
||||
|
||||
<div class="sam-flex-grow"></div>
|
||||
|
||||
<div class="sam-card sam-mb" style="align-items: center">
|
||||
<span>
|
||||
Made with <i class="bi bi-heart-fill" style="color: red"></i> by
|
||||
<a href="https://sam-hayes.org" target="_blank">Sam Hayes</a>
|
||||
</span>
|
||||
|
||||
<lib-pubkey
|
||||
class="sam-mt-h"
|
||||
value="npub1tgyjshvelwj73t3jy0n3xllgt03elkapfl3k3n0x2wkunegkgrwssfp0u4"
|
||||
(click)="toast.show('Copied to clipboard')"
|
||||
></lib-pubkey>
|
||||
</div>
|
||||
|
||||
<lib-toast #toast [bottom]="188"></lib-toast>
|
||||
@@ -0,0 +1,9 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow-y: auto;
|
||||
padding-left: var(--size);
|
||||
padding-right: var(--size);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { InfoComponent } from './info.component';
|
||||
|
||||
describe('InfoComponent', () => {
|
||||
let component: InfoComponent;
|
||||
let fixture: ComponentFixture<InfoComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [InfoComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(InfoComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { PubkeyComponent, ToastComponent } from '@common';
|
||||
import packageJson from '../../../../../../../package.json';
|
||||
|
||||
@Component({
|
||||
selector: 'app-info',
|
||||
imports: [PubkeyComponent, ToastComponent],
|
||||
templateUrl: './info.component.html',
|
||||
styleUrl: './info.component.scss',
|
||||
})
|
||||
export class InfoComponent {
|
||||
version = packageJson.custom.chrome.version;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<div class="sam-text-header">
|
||||
<span> Settings </span>
|
||||
</div>
|
||||
|
||||
<span>SYNC: {{ syncFlow }}</span>
|
||||
|
||||
<button class="btn btn-primary" (click)="onClickExportVault()">
|
||||
Export Vault
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
(click)="
|
||||
confirm.show(
|
||||
'Do you really want to import a vault? All existing data will be overwritten.',
|
||||
onImportVault.bind(fileInput)
|
||||
)
|
||||
"
|
||||
>
|
||||
Import Vault
|
||||
</button>
|
||||
|
||||
<div class="sam-flex-grow"></div>
|
||||
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
(click)="
|
||||
confirm.show(
|
||||
'Do you really want to delete your vault with all identities?',
|
||||
onDeleteVault.bind(this)
|
||||
)
|
||||
"
|
||||
>
|
||||
Delete Vault
|
||||
</button>
|
||||
|
||||
<lib-confirm #confirm> </lib-confirm>
|
||||
|
||||
<input
|
||||
#fileInput
|
||||
class="file-input"
|
||||
type="file"
|
||||
(change)="onImportFileChange($event)"
|
||||
accept=".json"
|
||||
/>
|
||||
@@ -0,0 +1,14 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: var(--size);
|
||||
overflow-y: auto;
|
||||
padding-left: var(--size);
|
||||
padding-right: var(--size);
|
||||
|
||||
.file-input {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SettingsComponent } from './settings.component';
|
||||
|
||||
describe('SettingsComponent', () => {
|
||||
let component: SettingsComponent;
|
||||
let fixture: ComponentFixture<SettingsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SettingsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SettingsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import {
|
||||
BrowserSyncData,
|
||||
BrowserSyncFlow,
|
||||
ConfirmComponent,
|
||||
DateHelper,
|
||||
StorageService,
|
||||
} from '@common';
|
||||
import { StartupService } from '../../../services/startup/startup.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
imports: [ConfirmComponent],
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrl: './settings.component.scss',
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
syncFlow: string | undefined;
|
||||
|
||||
readonly #storage = inject(StorageService);
|
||||
readonly #startup = inject(StartupService);
|
||||
|
||||
ngOnInit(): void {
|
||||
const vault = JSON.stringify(
|
||||
this.#storage.getBrowserSyncHandler().browserSyncData
|
||||
);
|
||||
console.log(vault.length / 1024 + ' KB');
|
||||
|
||||
switch (this.#storage.getGootiMetaHandler().gootiMetaData?.syncFlow) {
|
||||
case BrowserSyncFlow.NO_SYNC:
|
||||
this.syncFlow = 'Off';
|
||||
break;
|
||||
|
||||
case BrowserSyncFlow.BROWSER_SYNC:
|
||||
this.syncFlow = 'Google Chrome';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async onDeleteVault() {
|
||||
try {
|
||||
await this.#storage.deleteVault();
|
||||
this.#startup.startOver();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
onImportVault() {
|
||||
(this as unknown as HTMLInputElement).click();
|
||||
}
|
||||
|
||||
async onImportFileChange(event: Event) {
|
||||
try {
|
||||
const element = event.currentTarget as HTMLInputElement;
|
||||
const file = element.files !== null ? element.files[0] : undefined;
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = await file.text();
|
||||
const vault = JSON.parse(text) as BrowserSyncData;
|
||||
|
||||
await this.#storage.deleteVault(true);
|
||||
await this.#storage.importVault(vault);
|
||||
this.#storage.isInitialized = false;
|
||||
this.#startup.startOver();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
async onClickExportVault() {
|
||||
const jsonVault = this.#storage.exportVault();
|
||||
|
||||
const dateTimeString = DateHelper.dateToISOLikeButLocal(new Date());
|
||||
const fileName = `Gooti Chrome - Vault Export - ${dateTimeString}.json`;
|
||||
|
||||
this.#downloadJson(jsonVault, fileName);
|
||||
}
|
||||
|
||||
#downloadJson(jsonString: string, fileName: string) {
|
||||
const dataStr =
|
||||
'data:text/json;charset=utf-8,' + encodeURIComponent(jsonString);
|
||||
const downloadAnchorNode = document.createElement('a');
|
||||
downloadAnchorNode.setAttribute('href', dataStr);
|
||||
downloadAnchorNode.setAttribute('download', fileName);
|
||||
document.body.appendChild(downloadAnchorNode);
|
||||
downloadAnchorNode.click();
|
||||
downloadAnchorNode.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<div class="sam-text-header">
|
||||
<span>New Identity</span>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<input
|
||||
id="nickElement"
|
||||
type="text"
|
||||
placeholder="Nick"
|
||||
class="form-control form-control-lg"
|
||||
style="font-size: 1rem"
|
||||
[(ngModel)]="identity.nick"
|
||||
autocomplete="off"
|
||||
(ngModelChange)="validateCanSave()"
|
||||
/>
|
||||
|
||||
<div class="sam-mt input-group mb-3">
|
||||
<input
|
||||
id="privkeyInputElement"
|
||||
#privkeyInputElement
|
||||
type="password"
|
||||
placeholder="Private Key (HEX or NSEC)"
|
||||
class="form-control form-control-lg"
|
||||
style="font-size: 1rem"
|
||||
[(ngModel)]="identity.privkeyInput"
|
||||
autocomplete="off"
|
||||
(ngModelChange)="validateCanSave()"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
(click)="toggleType(privkeyInputElement)"
|
||||
>
|
||||
<i
|
||||
class="bi bi-eye"
|
||||
[class.bi-eye]="privkeyInputElement.type === 'password'"
|
||||
[class.bi-eye-slash]="privkeyInputElement.type === 'text'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="sam-mt"
|
||||
(click)="onClickGeneratePrivkey()"
|
||||
type="button"
|
||||
class="btn btn-link"
|
||||
>
|
||||
Generate private key
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sam-footer-grid-2">
|
||||
<button type="button" class="btn btn-secondary" (click)="navigateBack()">
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<button
|
||||
[disabled]="!canSave"
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
(click)="onClickSave()"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!----------->
|
||||
<!-- ALERT -->
|
||||
<!----------->
|
||||
@if(alertMessage) {
|
||||
<div
|
||||
style="
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
align-self: center;
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
"
|
||||
>
|
||||
<div class="alert alert-danger sam-flex-row gap" role="alert">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<span>{{ alertMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.content {
|
||||
padding-left: var(--size);
|
||||
padding-right: var(--size);
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NewIdentityComponent } from './new-identity.component';
|
||||
|
||||
describe('NewIdentityComponent', () => {
|
||||
let component: NewIdentityComponent;
|
||||
let fixture: ComponentFixture<NewIdentityComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NewIdentityComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NewIdentityComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { AfterViewInit, Component, inject } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { NavComponent, NostrHelper, StorageService } from '@common';
|
||||
import { generateSecretKey } from 'nostr-tools';
|
||||
import { bytesToHex } from '@noble/hashes/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-new-identity',
|
||||
templateUrl: './new-identity.component.html',
|
||||
styleUrl: './new-identity.component.scss',
|
||||
imports: [FormsModule],
|
||||
})
|
||||
export class NewIdentityComponent
|
||||
extends NavComponent
|
||||
implements AfterViewInit
|
||||
{
|
||||
readonly identity = {
|
||||
nick: '',
|
||||
privkeyInput: '',
|
||||
};
|
||||
canSave = false;
|
||||
alertMessage: any | undefined;
|
||||
|
||||
readonly #storage = inject(StorageService);
|
||||
readonly #router = inject(Router);
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
document.getElementById('nickElement')?.focus();
|
||||
}
|
||||
|
||||
toggleType(element: HTMLInputElement) {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
} else {
|
||||
element.type = 'password';
|
||||
}
|
||||
}
|
||||
|
||||
onClickGeneratePrivkey() {
|
||||
const sk = generateSecretKey();
|
||||
const privkey = bytesToHex(sk);
|
||||
|
||||
this.identity.privkeyInput = NostrHelper.privkey2nsec(privkey);
|
||||
this.validateCanSave();
|
||||
}
|
||||
|
||||
validateCanSave() {
|
||||
if (!this.identity.nick || !this.identity.privkeyInput) {
|
||||
this.canSave = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
NostrHelper.getNostrPrivkeyObject(
|
||||
this.identity.privkeyInput.toLocaleLowerCase()
|
||||
);
|
||||
this.canSave = true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
this.canSave = false;
|
||||
}
|
||||
}
|
||||
|
||||
async onClickSave() {
|
||||
if (!this.canSave) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.identity.nick || !this.identity.privkeyInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.#storage.addIdentity({
|
||||
nick: this.identity.nick,
|
||||
privkeyString: this.identity.privkeyInput,
|
||||
});
|
||||
this.#router.navigateByUrl('/home/identities');
|
||||
} catch (error: any) {
|
||||
this.alertMessage = error?.message;
|
||||
setTimeout(() => {
|
||||
this.alertMessage = undefined;
|
||||
}, 4500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<div class="sam-text-header">
|
||||
<span>Gooti</span>
|
||||
</div>
|
||||
|
||||
<div class="vertically-centered">
|
||||
<div class="sam-flex-column center">
|
||||
<div class="sam-flex-column gap" style="align-items: center">
|
||||
<div class="logo-frame">
|
||||
<img src="gooti.svg" height="120" width="120" alt=""/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="sam-mt-2 btn btn-primary"
|
||||
(click)="router.navigateByUrl('/vault-create/new')"
|
||||
>
|
||||
<div class="sam-flex-row gap-h">
|
||||
<i class="bi bi-plus-circle" style="height: 22px"></i>
|
||||
<span>Create a new vault</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<span class="sam-text-muted">or</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
(click)="fileInput.click()"
|
||||
>
|
||||
<span>Import a vault</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
#fileInput
|
||||
class="file-input"
|
||||
type="file"
|
||||
(change)="onImportFileChange($event)"
|
||||
accept=".json"
|
||||
/>
|
||||
@@ -0,0 +1,17 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.vertically-centered {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HomeComponent } from './home.component';
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
let fixture: ComponentFixture<HomeComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [HomeComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HomeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { BrowserSyncData, StorageService } from '@common';
|
||||
import { StartupService } from '../../../services/startup/startup.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
imports: [],
|
||||
templateUrl: './home.component.html',
|
||||
styleUrl: './home.component.scss',
|
||||
})
|
||||
export class HomeComponent {
|
||||
readonly router = inject(Router);
|
||||
readonly #storage = inject(StorageService);
|
||||
readonly #startup = inject(StartupService);
|
||||
|
||||
async onImportFileChange(event: Event) {
|
||||
try {
|
||||
const element = event.currentTarget as HTMLInputElement;
|
||||
const file = element.files !== null ? element.files[0] : undefined;
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = await file.text();
|
||||
const vault = JSON.parse(text) as BrowserSyncData;
|
||||
console.log(vault);
|
||||
|
||||
await this.#storage.importVault(vault);
|
||||
this.#startup.startOver();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<div class="sam-text-header">
|
||||
<span>Gooti</span>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="sam-flex-column gap" style="align-items: center">
|
||||
<div class="logo-frame">
|
||||
<img src="gooti.svg" height="120" width="120" alt=""/>
|
||||
</div>
|
||||
|
||||
<span class="sam-mt-2"> Please define a password for your vault. </span>
|
||||
|
||||
<small class="sam-text-muted">
|
||||
Sensitive data is encypted before it is stored.
|
||||
</small>
|
||||
|
||||
<div class="sam-mt input-group">
|
||||
<input
|
||||
#passwordInputElement
|
||||
type="password"
|
||||
class="form-control form-control-lg"
|
||||
style="font-size: 1rem"
|
||||
placeholder="vault password"
|
||||
[(ngModel)]="password"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
(click)="toggleType(passwordInputElement)"
|
||||
>
|
||||
<i
|
||||
class="bi bi-eye"
|
||||
[class.bi-eye]="passwordInputElement.type === 'password'"
|
||||
[class.bi-eye-slash]="passwordInputElement.type === 'text'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
[disabled]="!password || password.length < 4"
|
||||
type="button"
|
||||
class="sam-mt btn btn-primary"
|
||||
(click)="createVault()"
|
||||
>
|
||||
Create vault
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
color-scheme: dark;
|
||||
|
||||
.custom-header {
|
||||
padding-top: var(--size);
|
||||
padding-bottom: var(--size);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto;
|
||||
align-items: center;
|
||||
|
||||
.back {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 2;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 2;
|
||||
justify-self: start;
|
||||
padding-left: var(--size);
|
||||
}
|
||||
|
||||
.text {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 2;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 2;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
justify-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
padding: 0 var(--size) var(--size) var(--size);
|
||||
}
|
||||
|
||||
.logo-frame {
|
||||
border: 2px solid #0d6efd;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NewComponent } from './new.component';
|
||||
|
||||
describe('NewComponent', () => {
|
||||
let component: NewComponent;
|
||||
let fixture: ComponentFixture<NewComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NewComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NewComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { NavComponent, StorageService } from '@common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-new',
|
||||
imports: [FormsModule],
|
||||
templateUrl: './new.component.html',
|
||||
styleUrl: './new.component.scss',
|
||||
})
|
||||
export class NewComponent extends NavComponent {
|
||||
password = '';
|
||||
|
||||
readonly #router = inject(Router);
|
||||
readonly #storage = inject(StorageService);
|
||||
|
||||
toggleType(element: HTMLInputElement) {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
} else {
|
||||
element.type = 'password';
|
||||
}
|
||||
}
|
||||
|
||||
async createVault() {
|
||||
if (!this.password) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.#storage.createNewVault(this.password);
|
||||
this.#router.navigateByUrl('/home/identities');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<router-outlet></router-outlet>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { VaultCreateComponent } from './vault-create.component';
|
||||
|
||||
describe('VaultCreateComponent', () => {
|
||||
let component: VaultCreateComponent;
|
||||
let fixture: ComponentFixture<VaultCreateComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [VaultCreateComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VaultCreateComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-vault-create',
|
||||
imports: [RouterOutlet],
|
||||
templateUrl: './vault-create.component.html',
|
||||
styleUrl: './vault-create.component.scss'
|
||||
})
|
||||
export class VaultCreateComponent {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<div class="sam-text-header">
|
||||
<span class="brand">Gooti</span>
|
||||
</div>
|
||||
|
||||
<div class="content-login-vault">
|
||||
<div class="sam-flex-column gap" style="align-items: center">
|
||||
<div class="logo-frame">
|
||||
<img src="gooti.svg" height="120" width="120" alt=""/>
|
||||
</div>
|
||||
|
||||
<div class="sam-mt-2 input-group">
|
||||
<input
|
||||
#passwordInputElement
|
||||
type="password"
|
||||
class="form-control"
|
||||
placeholder="vault password"
|
||||
[(ngModel)]="loginPassword"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
(click)="toggleType(passwordInputElement)"
|
||||
>
|
||||
<i
|
||||
class="bi bi-eye"
|
||||
[class.bi-eye]="passwordInputElement.type === 'password'"
|
||||
[class.bi-eye-slash]="passwordInputElement.type === 'text'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
[disabled]="!loginPassword"
|
||||
type="button"
|
||||
class="sam-mt btn btn-primary"
|
||||
(click)="loginVault()"
|
||||
>
|
||||
<div class="sam-flex-row gap-h">
|
||||
<i class="bi bi-box-arrow-in-right"></i>
|
||||
<span>Sign in</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="sam-mt"
|
||||
(click)="
|
||||
confirm.show(
|
||||
'Do you really want to delete your vault? All existing data will be lost.',
|
||||
onClickDeleteVault.bind(this)
|
||||
)
|
||||
"
|
||||
type="button"
|
||||
class="btn btn-link"
|
||||
>
|
||||
Delete Vault
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!----------->
|
||||
<!-- ALERT -->
|
||||
<!----------->
|
||||
@if(showInvalidPasswordAlert) {
|
||||
<div style="position: absolute; bottom: 0; align-self: center">
|
||||
<div class="alert alert-danger sam-flex-row gap" role="alert">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<span>Invalid password</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<lib-confirm #confirm> </lib-confirm>
|
||||
@@ -0,0 +1,19 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-items: center;
|
||||
|
||||
.logo-frame {
|
||||
border: 2px solid #0d6efd;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.content-login-vault {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
padding: 0 var(--size) var(--size) var(--size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { VaultLoginComponent } from './vault-login.component';
|
||||
|
||||
describe('VaultLoginComponent', () => {
|
||||
let component: VaultLoginComponent;
|
||||
let fixture: ComponentFixture<VaultLoginComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [VaultLoginComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VaultLoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { ConfirmComponent, StorageService } from '@common';
|
||||
import { StartupService } from '../../services/startup/startup.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-vault-login',
|
||||
templateUrl: './vault-login.component.html',
|
||||
styleUrl: './vault-login.component.scss',
|
||||
imports: [FormsModule, ConfirmComponent],
|
||||
})
|
||||
export class VaultLoginComponent {
|
||||
loginPassword = '';
|
||||
showInvalidPasswordAlert = false;
|
||||
|
||||
readonly #storage = inject(StorageService);
|
||||
readonly #router = inject(Router);
|
||||
readonly #startup = inject(StartupService);
|
||||
|
||||
toggleType(element: HTMLInputElement) {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
} else {
|
||||
element.type = 'password';
|
||||
}
|
||||
}
|
||||
|
||||
async loginVault() {
|
||||
if (!this.loginPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.#storage.unlockVault(this.loginPassword);
|
||||
this.#router.navigateByUrl('/home/identities');
|
||||
} catch (error) {
|
||||
this.showInvalidPasswordAlert = true;
|
||||
console.log(error);
|
||||
window.setTimeout(() => {
|
||||
this.showInvalidPasswordAlert = false;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
async onClickDeleteVault() {
|
||||
try {
|
||||
await this.#storage.deleteVault();
|
||||
this.#startup.startOver();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<div class="sam-text-header sam-mb-2">
|
||||
<span>Gooti Setup - Sync Preference</span>
|
||||
</div>
|
||||
|
||||
<span class="sam-text-muted sam-text-md sam-text-align-center2">
|
||||
Gooti always encrypts sensitive data like private keys and site permissions
|
||||
independent of the chosen sync mode.
|
||||
</span>
|
||||
|
||||
<span class="sam-mt sam-text-lg">Sync : Google Chrome</span>
|
||||
|
||||
<span class="sam-text-muted sam-text-md sam-text-align-center2">
|
||||
Your encrypted data is synced between Google Chrome browser instances. You
|
||||
need to be signed in with your Google account.
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="sam-mt btn btn-primary"
|
||||
(click)="onClickSync(true)"
|
||||
>
|
||||
<span> Sync ON</span>
|
||||
</button>
|
||||
|
||||
<span class="sam-mt-2 sam-text-lg">Offline</span>
|
||||
|
||||
<span class="sam-text-muted sam-text-md">
|
||||
Your encrypted data is never uploaded to any servers. It remains in your local
|
||||
browser instance.
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="sam-mt sam-mb-2 btn btn-secondary"
|
||||
(click)="onClickSync(false)"
|
||||
>
|
||||
<span> Sync OFF</span>
|
||||
</button>
|
||||
|
||||
<div class="sam-flex-grow"></div>
|
||||
|
||||
<span class="sam-text-muted sam-text-md sam-mb">
|
||||
Your preference can later be changed at any time.
|
||||
</span>
|
||||
@@ -0,0 +1,8 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding-left: var(--size);
|
||||
padding-right: var(--size);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { WelcomeComponent } from './welcome.component';
|
||||
|
||||
describe('WelcomeComponent', () => {
|
||||
let component: WelcomeComponent;
|
||||
let fixture: ComponentFixture<WelcomeComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [WelcomeComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(WelcomeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { BrowserSyncFlow, StorageService } from '@common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-welcome',
|
||||
imports: [],
|
||||
templateUrl: './welcome.component.html',
|
||||
styleUrl: './welcome.component.scss',
|
||||
})
|
||||
export class WelcomeComponent {
|
||||
readonly router = inject(Router);
|
||||
readonly #storage = inject(StorageService);
|
||||
|
||||
async onClickSync(enabled: boolean) {
|
||||
const flow: BrowserSyncFlow = enabled
|
||||
? BrowserSyncFlow.BROWSER_SYNC
|
||||
: BrowserSyncFlow.NO_SYNC;
|
||||
|
||||
await this.#storage.enableBrowserSyncFlow(flow);
|
||||
|
||||
// In case the user has selected the BROWSER_SYNC flow,
|
||||
// we have to check if there is sync data available (e.g. from
|
||||
// another browser instance).
|
||||
// If so, navigate to /vault-login, otherwise to /vault-create/home.
|
||||
if (flow === BrowserSyncFlow.BROWSER_SYNC) {
|
||||
const browserSyncData =
|
||||
await this.#storage.loadAndMigrateBrowserSyncData();
|
||||
|
||||
if (
|
||||
typeof browserSyncData !== 'undefined' &&
|
||||
Object.keys(browserSyncData).length > 0
|
||||
) {
|
||||
await this.router.navigateByUrl('/vault-login');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await this.router.navigateByUrl('/vault-create/home');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { StartupService } from './startup.service';
|
||||
|
||||
describe('StartupService', () => {
|
||||
let service: StartupService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(StartupService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
88
projects/chrome/src/app/services/startup/startup.service.ts
Normal file
88
projects/chrome/src/app/services/startup/startup.service.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { LoggerService, StorageService, StorageServiceConfig } from '@common';
|
||||
import { ChromeSessionHandler } from '../../common/data/chrome-session-handler';
|
||||
import { ChromeSyncYesHandler } from '../../common/data/chrome-sync-yes-handler';
|
||||
import { ChromeSyncNoHandler } from '../../common/data/chrome-sync-no-handler';
|
||||
import { ChromeMetaHandler } from '../../common/data/chrome-meta-handler';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StartupService {
|
||||
readonly #logger = inject(LoggerService);
|
||||
readonly #storage = inject(StorageService);
|
||||
readonly #router = inject(Router);
|
||||
|
||||
async startOver() {
|
||||
const storageConfig: StorageServiceConfig = {
|
||||
browserSessionHandler: new ChromeSessionHandler(),
|
||||
browserSyncYesHandler: new ChromeSyncYesHandler(),
|
||||
browserSyncNoHandler: new ChromeSyncNoHandler(),
|
||||
gootiMetaHandler: new ChromeMetaHandler(),
|
||||
};
|
||||
|
||||
this.#storage.initialize(storageConfig);
|
||||
|
||||
// Step 0:
|
||||
storageConfig.browserSyncNoHandler.setIgnoreProperties(
|
||||
storageConfig.gootiMetaHandler.metaProperties
|
||||
);
|
||||
|
||||
// Step 1: Load the gooti's user settings
|
||||
const gootiMetaData = await this.#storage.loadGootiMetaData();
|
||||
if (typeof gootiMetaData?.syncFlow === 'undefined') {
|
||||
// Very first run. The user has not set up Gooti yet.
|
||||
this.#router.navigateByUrl('/welcome');
|
||||
return;
|
||||
}
|
||||
this.#storage.enableBrowserSyncFlow(gootiMetaData.syncFlow);
|
||||
|
||||
// Load the browser session data.
|
||||
const browserSessionData = await this.#storage.loadBrowserSessionData();
|
||||
|
||||
if (!browserSessionData) {
|
||||
await this.#initializeFlow_A();
|
||||
} else {
|
||||
await this.#initializeFlow_B();
|
||||
}
|
||||
}
|
||||
|
||||
async #initializeFlow_A() {
|
||||
// Starting with NO browser session data available.
|
||||
//
|
||||
// This could be because the browser sync data was
|
||||
// never loaded before OR it was attempted, but
|
||||
// there is no browser sync data.
|
||||
|
||||
this.#logger.log('No browser session data available.');
|
||||
|
||||
// Check if there is NO browser sync data.
|
||||
const browserSyncData = await this.#storage.loadAndMigrateBrowserSyncData();
|
||||
if (browserSyncData) {
|
||||
// There is browser sync data. Route to the VAULT LOGIN to enable the session.
|
||||
this.#router.navigateByUrl('/vault-login');
|
||||
} else {
|
||||
// There is NO browser sync data. Route to the VAULT CREATION to enable the session.
|
||||
this.#router.navigateByUrl('/vault-create/home');
|
||||
}
|
||||
}
|
||||
|
||||
async #initializeFlow_B() {
|
||||
// Stating with browser session data available. The user has already unlocked the vault before.
|
||||
// Route to VAULT HOME.
|
||||
|
||||
this.#logger.log('Browser session data is available.');
|
||||
|
||||
// Also load the browser sync data. This is needed, if the user adds or deletes anything.
|
||||
await this.#storage.loadAndMigrateBrowserSyncData();
|
||||
|
||||
const selectedIdentityId =
|
||||
this.#storage.getBrowserSessionHandler().browserSessionData
|
||||
?.selectedIdentityId;
|
||||
|
||||
this.#router.navigateByUrl(
|
||||
`/home/${selectedIdentityId ? 'identity' : 'identities'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
283
projects/chrome/src/background-common.ts
Normal file
283
projects/chrome/src/background-common.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
BrowserSessionData,
|
||||
BrowserSyncData,
|
||||
BrowserSyncFlow,
|
||||
CryptoHelper,
|
||||
GootiMetaData,
|
||||
Identity_DECRYPTED,
|
||||
Nip07Method,
|
||||
Nip07MethodPolicy,
|
||||
NostrHelper,
|
||||
Permission_DECRYPTED,
|
||||
Permission_ENCRYPTED,
|
||||
} from '@common';
|
||||
import { ChromeMetaHandler } from './app/common/data/chrome-meta-handler';
|
||||
import { Event, EventTemplate, finalizeEvent, nip04 } from 'nostr-tools';
|
||||
|
||||
export const debug = function (message: any) {
|
||||
const dateString = new Date().toISOString();
|
||||
console.log(`[Gooti - ${dateString}]: ${JSON.stringify(message)}`);
|
||||
};
|
||||
|
||||
export type PromptResponse =
|
||||
| 'reject'
|
||||
| 'reject-once'
|
||||
| 'approve'
|
||||
| 'approve-once';
|
||||
|
||||
export interface PromptResponseMessage {
|
||||
id: string;
|
||||
response: PromptResponse;
|
||||
}
|
||||
|
||||
export interface BackgroundRequestMessage {
|
||||
method: Nip07Method;
|
||||
params: any;
|
||||
host: string;
|
||||
}
|
||||
|
||||
export const getBrowserSessionData = async function (): Promise<
|
||||
BrowserSessionData | undefined
|
||||
> {
|
||||
const browserSessionData = await chrome.storage.session.get(null);
|
||||
if (Object.keys(browserSessionData).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return browserSessionData as BrowserSessionData;
|
||||
};
|
||||
|
||||
export const getBrowserSyncData = async function (): Promise<
|
||||
BrowserSyncData | undefined
|
||||
> {
|
||||
const gootiMetaHandler = new ChromeMetaHandler();
|
||||
const gootiMetaData =
|
||||
(await gootiMetaHandler.loadFullData()) as GootiMetaData;
|
||||
|
||||
let browserSyncData: BrowserSyncData | undefined;
|
||||
|
||||
if (gootiMetaData.syncFlow === BrowserSyncFlow.NO_SYNC) {
|
||||
browserSyncData = (await chrome.storage.local.get(null)) as BrowserSyncData;
|
||||
} else if (gootiMetaData.syncFlow === BrowserSyncFlow.BROWSER_SYNC) {
|
||||
browserSyncData = (await chrome.storage.sync.get(null)) as BrowserSyncData;
|
||||
}
|
||||
|
||||
return browserSyncData;
|
||||
};
|
||||
|
||||
export const savePermissionsToBrowserSyncStorage = async function (
|
||||
permissions: Permission_ENCRYPTED[]
|
||||
): Promise<void> {
|
||||
const gootiMetaHandler = new ChromeMetaHandler();
|
||||
const gootiMetaData =
|
||||
(await gootiMetaHandler.loadFullData()) as GootiMetaData;
|
||||
|
||||
if (gootiMetaData.syncFlow === BrowserSyncFlow.NO_SYNC) {
|
||||
await chrome.storage.local.set({ permissions });
|
||||
} else if (gootiMetaData.syncFlow === BrowserSyncFlow.BROWSER_SYNC) {
|
||||
await chrome.storage.sync.set({ permissions });
|
||||
}
|
||||
};
|
||||
|
||||
export const checkPermissions = function (
|
||||
browserSessionData: BrowserSessionData,
|
||||
identity: Identity_DECRYPTED,
|
||||
host: string,
|
||||
method: Nip07Method,
|
||||
params: any
|
||||
): boolean | undefined {
|
||||
const permissions = browserSessionData.permissions.filter(
|
||||
(x) =>
|
||||
x.identityId === identity.id && x.host === host && x.method === method
|
||||
);
|
||||
|
||||
if (permissions.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (method === 'getPublicKey') {
|
||||
// No evaluation of params required.
|
||||
return permissions.every((x) => x.methodPolicy === 'allow');
|
||||
}
|
||||
|
||||
if (method === 'getRelays') {
|
||||
// No evaluation of params required.
|
||||
return permissions.every((x) => x.methodPolicy === 'allow');
|
||||
}
|
||||
|
||||
if (method === 'signEvent') {
|
||||
// Evaluate params.
|
||||
const eventTemplate = params as EventTemplate;
|
||||
if (
|
||||
permissions.find(
|
||||
(x) => x.methodPolicy === 'allow' && typeof x.kind === 'undefined'
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
permissions.some(
|
||||
(x) => x.methodPolicy === 'allow' && x.kind === eventTemplate.kind
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
permissions.some(
|
||||
(x) => x.methodPolicy === 'deny' && x.kind === eventTemplate.kind
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (method === 'nip04.encrypt') {
|
||||
// No evaluation of params required.
|
||||
return permissions.every((x) => x.methodPolicy === 'allow');
|
||||
}
|
||||
|
||||
if (method === 'nip04.decrypt') {
|
||||
// No evaluation of params required.
|
||||
return permissions.every((x) => x.methodPolicy === 'allow');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const storePermission = async function (
|
||||
browserSessionData: BrowserSessionData,
|
||||
identity: Identity_DECRYPTED,
|
||||
host: string,
|
||||
method: Nip07Method,
|
||||
methodPolicy: Nip07MethodPolicy,
|
||||
kind?: number
|
||||
) {
|
||||
const browserSyncData = await getBrowserSyncData();
|
||||
if (!browserSyncData) {
|
||||
throw new Error(`Could not retrieve sync data`);
|
||||
}
|
||||
|
||||
const permission: Permission_DECRYPTED = {
|
||||
id: crypto.randomUUID(),
|
||||
identityId: identity.id,
|
||||
host,
|
||||
method,
|
||||
methodPolicy,
|
||||
kind,
|
||||
};
|
||||
|
||||
// Store session data
|
||||
await chrome.storage.session.set({
|
||||
permissions: [...browserSessionData.permissions, permission],
|
||||
});
|
||||
|
||||
// Encrypt permission to store in sync storage (depending on sync flow).
|
||||
const encryptedPermission = await encryptPermission(
|
||||
permission,
|
||||
browserSessionData.iv,
|
||||
browserSessionData.vaultPassword as string
|
||||
);
|
||||
|
||||
await savePermissionsToBrowserSyncStorage([
|
||||
...browserSyncData.permissions,
|
||||
encryptedPermission,
|
||||
]);
|
||||
};
|
||||
|
||||
export const getPosition = async function (width: number, height: number) {
|
||||
let left = 0;
|
||||
let top = 0;
|
||||
|
||||
try {
|
||||
const lastFocused = await chrome.windows.getLastFocused();
|
||||
|
||||
if (
|
||||
lastFocused &&
|
||||
lastFocused.top !== undefined &&
|
||||
lastFocused.left !== undefined &&
|
||||
lastFocused.width !== undefined &&
|
||||
lastFocused.height !== undefined
|
||||
) {
|
||||
// Position window in the center of the lastFocused window
|
||||
top = Math.round(lastFocused.top + (lastFocused.height - height) / 2);
|
||||
left = Math.round(lastFocused.left + (lastFocused.width - width) / 2);
|
||||
} else {
|
||||
console.error('Last focused window properties are undefined.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting window position:', error);
|
||||
}
|
||||
|
||||
return {
|
||||
top,
|
||||
left,
|
||||
};
|
||||
};
|
||||
|
||||
export const signEvent = function (
|
||||
eventTemplate: EventTemplate,
|
||||
privkey: string
|
||||
): Event {
|
||||
return finalizeEvent(eventTemplate, NostrHelper.hex2bytes(privkey));
|
||||
};
|
||||
|
||||
export const nip04Encrypt = async function (
|
||||
privkey: string,
|
||||
peerPubkey: string,
|
||||
plaintext: string
|
||||
): Promise<string> {
|
||||
return await nip04.encrypt(
|
||||
NostrHelper.hex2bytes(privkey),
|
||||
peerPubkey,
|
||||
plaintext
|
||||
);
|
||||
};
|
||||
|
||||
export const nip04Decrypt = async function (
|
||||
privkey: string,
|
||||
peerPubkey: string,
|
||||
ciphertext: string
|
||||
): Promise<string> {
|
||||
return await nip04.decrypt(
|
||||
NostrHelper.hex2bytes(privkey),
|
||||
peerPubkey,
|
||||
ciphertext
|
||||
);
|
||||
};
|
||||
|
||||
const encryptPermission = async function (
|
||||
permission: Permission_DECRYPTED,
|
||||
iv: string,
|
||||
password: string
|
||||
): Promise<Permission_ENCRYPTED> {
|
||||
const encryptedPermission: Permission_ENCRYPTED = {
|
||||
id: await encrypt(permission.id, iv, password),
|
||||
identityId: await encrypt(permission.identityId, iv, password),
|
||||
host: await encrypt(permission.host, iv, password),
|
||||
method: await encrypt(permission.method, iv, password),
|
||||
methodPolicy: await encrypt(permission.methodPolicy, iv, password),
|
||||
};
|
||||
|
||||
if (typeof permission.kind !== 'undefined') {
|
||||
encryptedPermission.kind = await encrypt(
|
||||
permission.kind.toString(),
|
||||
iv,
|
||||
password
|
||||
);
|
||||
}
|
||||
|
||||
return encryptedPermission;
|
||||
};
|
||||
|
||||
const encrypt = async function (
|
||||
value: string,
|
||||
iv: string,
|
||||
password: string
|
||||
): Promise<string> {
|
||||
return await CryptoHelper.encrypt(value, iv, password);
|
||||
};
|
||||
148
projects/chrome/src/background.ts
Normal file
148
projects/chrome/src/background.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { NostrHelper } from '@common';
|
||||
import {
|
||||
BackgroundRequestMessage,
|
||||
checkPermissions,
|
||||
debug,
|
||||
getBrowserSessionData,
|
||||
getPosition,
|
||||
nip04Decrypt,
|
||||
nip04Encrypt,
|
||||
PromptResponse,
|
||||
PromptResponseMessage,
|
||||
signEvent,
|
||||
storePermission,
|
||||
} from './background-common';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
type Relays = Record<string, { read: boolean; write: boolean }>;
|
||||
|
||||
const openPrompts = new Map<
|
||||
string,
|
||||
{
|
||||
resolve: (response: PromptResponse) => void;
|
||||
reject: (reason?: any) => void;
|
||||
}
|
||||
>();
|
||||
|
||||
browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
|
||||
debug('Message received');
|
||||
const request = message as BackgroundRequestMessage | PromptResponseMessage;
|
||||
debug(request);
|
||||
|
||||
if ((request as PromptResponseMessage)?.id) {
|
||||
// Handle prompt response
|
||||
const promptResponse = request as PromptResponseMessage;
|
||||
const openPrompt = openPrompts.get(promptResponse.id);
|
||||
if (!openPrompt) {
|
||||
throw new Error(
|
||||
'Prompt response could not be matched to any previous request.'
|
||||
);
|
||||
}
|
||||
|
||||
openPrompt.resolve(promptResponse.response);
|
||||
openPrompts.delete(promptResponse.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const browserSessionData = await getBrowserSessionData();
|
||||
|
||||
if (!browserSessionData) {
|
||||
throw new Error('Gooti vault not unlocked by the user.');
|
||||
}
|
||||
|
||||
const currentIdentity = browserSessionData.identities.find(
|
||||
(x) => x.id === browserSessionData.selectedIdentityId
|
||||
);
|
||||
|
||||
if (!currentIdentity) {
|
||||
throw new Error('No Nostr identity available at endpoint.');
|
||||
}
|
||||
|
||||
const req = request as BackgroundRequestMessage;
|
||||
const permissionState = checkPermissions(
|
||||
browserSessionData,
|
||||
currentIdentity,
|
||||
req.host,
|
||||
req.method,
|
||||
req.params
|
||||
);
|
||||
|
||||
if (permissionState === false) {
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
|
||||
if (permissionState === undefined) {
|
||||
// Ask user for permission.
|
||||
const width = 375;
|
||||
const height = 600;
|
||||
const { top, left } = await getPosition(width, height);
|
||||
|
||||
const base64Event = Buffer.from(
|
||||
JSON.stringify(req.params ?? {}, undefined, 2)
|
||||
).toString('base64');
|
||||
|
||||
const response = await new Promise<PromptResponse>((resolve, reject) => {
|
||||
const id = crypto.randomUUID();
|
||||
openPrompts.set(id, { resolve, reject });
|
||||
browser.windows.create({
|
||||
type: 'popup',
|
||||
url: `prompt.html?method=${req.method}&host=${req.host}&id=${id}&nick=${currentIdentity.nick}&event=${base64Event}`,
|
||||
height,
|
||||
width,
|
||||
top,
|
||||
left,
|
||||
});
|
||||
});
|
||||
debug(response);
|
||||
if (response === 'approve' || response === 'reject') {
|
||||
await storePermission(
|
||||
browserSessionData,
|
||||
currentIdentity,
|
||||
req.host,
|
||||
req.method,
|
||||
response === 'approve' ? 'allow' : 'deny',
|
||||
req.params?.kind
|
||||
);
|
||||
}
|
||||
|
||||
if (['reject', 'reject-once'].includes(response)) {
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
} else {
|
||||
debug('Request allowed (via saved permission).');
|
||||
}
|
||||
|
||||
const relays: Relays = {};
|
||||
switch (req.method) {
|
||||
case 'getPublicKey':
|
||||
return NostrHelper.pubkeyFromPrivkey(currentIdentity.privkey);
|
||||
|
||||
case 'signEvent':
|
||||
return signEvent(req.params, currentIdentity.privkey);
|
||||
|
||||
case 'getRelays':
|
||||
browserSessionData.relays.forEach((x) => {
|
||||
relays[x.url] = { read: x.read, write: x.write };
|
||||
});
|
||||
return relays;
|
||||
|
||||
case 'nip04.encrypt':
|
||||
return await nip04Encrypt(
|
||||
currentIdentity.privkey,
|
||||
req.params.peerPubkey,
|
||||
req.params.plaintext
|
||||
);
|
||||
|
||||
case 'nip04.decrypt':
|
||||
return await nip04Decrypt(
|
||||
currentIdentity.privkey,
|
||||
req.params.peerPubkey,
|
||||
req.params.ciphertext
|
||||
);
|
||||
|
||||
default:
|
||||
throw new Error(`Not supported request method '${req.method}'.`);
|
||||
}
|
||||
});
|
||||
42
projects/chrome/src/gooti-content-script.ts
Normal file
42
projects/chrome/src/gooti-content-script.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import browser from 'webextension-polyfill';
|
||||
import { BackgroundRequestMessage } from './background-common';
|
||||
|
||||
// Inject the script that will provide window.nostr
|
||||
// The script needs to run before any other scripts from the real
|
||||
// page run (and maybe check for window.nostr).
|
||||
const script = document.createElement('script');
|
||||
script.setAttribute('async', 'false');
|
||||
script.setAttribute('type', 'text/javascript');
|
||||
script.setAttribute('src', browser.runtime.getURL('gooti-extension.js'));
|
||||
(document.head || document.documentElement).appendChild(script);
|
||||
|
||||
// listen for messages from that script
|
||||
window.addEventListener('message', async (message) => {
|
||||
// We will also receive our own messages, that we sent.
|
||||
// We have to ignore them (they will not have a params field).
|
||||
|
||||
if (message.source !== window) return;
|
||||
if (!message.data) return;
|
||||
if (!message.data.params) return;
|
||||
if (message.data.ext !== 'gooti') return;
|
||||
|
||||
// pass on to background
|
||||
let response;
|
||||
try {
|
||||
const request: BackgroundRequestMessage = {
|
||||
method: message.data.method,
|
||||
params: message.data.params,
|
||||
host: location.host,
|
||||
};
|
||||
|
||||
response = await browser.runtime.sendMessage(request);
|
||||
} catch (error) {
|
||||
response = { error };
|
||||
}
|
||||
|
||||
// return response
|
||||
window.postMessage(
|
||||
{ id: message.data.id, ext: 'gooti', response },
|
||||
message.origin
|
||||
);
|
||||
});
|
||||
128
projects/chrome/src/gooti-extension.ts
Normal file
128
projects/chrome/src/gooti-extension.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Event, EventTemplate } from 'nostr-tools';
|
||||
import { Nip07Method } from '@common';
|
||||
|
||||
type Relays = Record<string, { read: boolean; write: boolean }>;
|
||||
|
||||
class Messenger {
|
||||
#requests = new Map<
|
||||
string,
|
||||
{
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (reason: any) => void;
|
||||
}
|
||||
>();
|
||||
|
||||
constructor() {
|
||||
window.addEventListener('message', this.#handleCallResponse.bind(this));
|
||||
}
|
||||
|
||||
async request(method: Nip07Method, params: any): Promise<any> {
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.#requests.set(id, { resolve, reject });
|
||||
window.postMessage(
|
||||
{
|
||||
id,
|
||||
ext: 'gooti',
|
||||
method,
|
||||
params,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#handleCallResponse(message: MessageEvent) {
|
||||
// We also will receive our own messages, that we sent.
|
||||
// We have to ignore them (they will not have a response field).
|
||||
if (
|
||||
!message.data ||
|
||||
message.data.response === null ||
|
||||
message.data.response === undefined ||
|
||||
message.data.ext !== 'gooti' ||
|
||||
!this.#requests.has(message.data.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.data.response.error) {
|
||||
this.#requests.get(message.data.id)?.reject(message.data.response.error);
|
||||
} else {
|
||||
this.#requests.get(message.data.id)?.resolve(message.data.response);
|
||||
}
|
||||
|
||||
this.#requests.delete(message.data.id);
|
||||
}
|
||||
}
|
||||
|
||||
const nostr = {
|
||||
messenger: new Messenger(),
|
||||
|
||||
async getPublicKey(): Promise<string> {
|
||||
debug('getPublicKey received');
|
||||
const pubkey = await this.messenger.request('getPublicKey', {});
|
||||
debug(`getPublicKey response:`);
|
||||
debug(pubkey);
|
||||
return pubkey;
|
||||
},
|
||||
|
||||
async signEvent(event: EventTemplate): Promise<Event> {
|
||||
debug('signEvent received');
|
||||
const signedEvent = await this.messenger.request('signEvent', event);
|
||||
debug('signEvent response:');
|
||||
debug(signedEvent);
|
||||
return signedEvent;
|
||||
},
|
||||
|
||||
async getRelays(): Promise<Relays> {
|
||||
debug('getRelays received');
|
||||
const relays = (await this.messenger.request('getRelays', {})) as Relays;
|
||||
debug('getRelays response:');
|
||||
debug(relays);
|
||||
return relays;
|
||||
},
|
||||
|
||||
nip04: {
|
||||
that: this,
|
||||
|
||||
async encrypt(peerPubkey: string, plaintext: string): Promise<string> {
|
||||
debug('nip04.encrypt received');
|
||||
const ciphertext = (await nostr.messenger.request('nip04.encrypt', {
|
||||
peerPubkey,
|
||||
plaintext,
|
||||
})) as string;
|
||||
debug('nip04.encrypt response:');
|
||||
debug(ciphertext);
|
||||
return ciphertext;
|
||||
},
|
||||
|
||||
async decrypt(peerPubkey: string, ciphertext: string): Promise<string> {
|
||||
debug('nip04.decrypt received');
|
||||
const plaintext = (await nostr.messenger.request('nip04.decrypt', {
|
||||
peerPubkey,
|
||||
ciphertext,
|
||||
})) as string;
|
||||
debug('nip04.decrypt response:');
|
||||
debug(plaintext);
|
||||
return plaintext;
|
||||
},
|
||||
},
|
||||
|
||||
// nip44: {
|
||||
// async encrypt(peer, plaintext) {
|
||||
// return window.nostr._call('nip44.encrypt', { peer, plaintext });
|
||||
// },
|
||||
|
||||
// async decrypt(peer, ciphertext) {
|
||||
// return window.nostr._call('nip44.decrypt', { peer, ciphertext });
|
||||
// },
|
||||
// },
|
||||
};
|
||||
|
||||
window.nostr = nostr as any;
|
||||
|
||||
const debug = function (value: any) {
|
||||
console.log(JSON.stringify(value));
|
||||
};
|
||||
13
projects/chrome/src/index.html
Normal file
13
projects/chrome/src/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Gooti</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- <link rel="icon" type="image/x-icon" href="favicon.ico"> -->
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
14
projects/chrome/src/main.ts
Normal file
14
projects/chrome/src/main.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import './app/common/extensions/array';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
||||
console.error(err)
|
||||
);
|
||||
|
||||
// declare global {
|
||||
// interface Window {
|
||||
// nostr: any;
|
||||
// }
|
||||
// }
|
||||
167
projects/chrome/src/prompt.ts
Normal file
167
projects/chrome/src/prompt.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import browser from 'webextension-polyfill';
|
||||
import { Buffer } from 'buffer';
|
||||
import { Nip07Method } from '@common';
|
||||
import { PromptResponse, PromptResponseMessage } from './background-common';
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const id = params.get('id') as string;
|
||||
const method = params.get('method') as Nip07Method;
|
||||
const host = params.get('host') as string;
|
||||
const nick = params.get('nick') as string;
|
||||
const event = Buffer.from(params.get('event') as string, 'base64').toString();
|
||||
|
||||
let title = '';
|
||||
switch (method) {
|
||||
case 'getPublicKey':
|
||||
title = 'Get Public Key';
|
||||
break;
|
||||
|
||||
case 'signEvent':
|
||||
title = 'Sign Event';
|
||||
break;
|
||||
|
||||
case 'nip04.encrypt':
|
||||
title = 'Encrypt';
|
||||
break;
|
||||
|
||||
case 'nip04.decrypt':
|
||||
title = 'Decrypt';
|
||||
break;
|
||||
|
||||
case 'getRelays':
|
||||
title = 'Get Relays';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const titleSpanElement = document.getElementById('titleSpan');
|
||||
if (titleSpanElement) {
|
||||
titleSpanElement.innerText = title;
|
||||
}
|
||||
|
||||
Array.from(document.getElementsByClassName('nick-INSERT')).forEach(
|
||||
(element) => {
|
||||
(element as HTMLElement).innerText = nick;
|
||||
}
|
||||
);
|
||||
|
||||
Array.from(document.getElementsByClassName('host-INSERT')).forEach(
|
||||
(element) => {
|
||||
(element as HTMLElement).innerText = host;
|
||||
}
|
||||
);
|
||||
|
||||
const kindSpanElement = document.getElementById('kindSpan');
|
||||
if (kindSpanElement) {
|
||||
kindSpanElement.innerText = JSON.parse(event).kind;
|
||||
}
|
||||
|
||||
const cardGetPublicKeyElement = document.getElementById('cardGetPublicKey');
|
||||
if (cardGetPublicKeyElement) {
|
||||
if (method === 'getPublicKey') {
|
||||
// Do nothing.
|
||||
} else {
|
||||
cardGetPublicKeyElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const cardGetRelaysElement = document.getElementById('cardGetRelays');
|
||||
if (cardGetRelaysElement) {
|
||||
if (method === 'getRelays') {
|
||||
// Do nothing.
|
||||
} else {
|
||||
cardGetRelaysElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const cardSignEventElement = document.getElementById('cardSignEvent');
|
||||
const card2SignEventElement = document.getElementById('card2SignEvent');
|
||||
if (cardSignEventElement && card2SignEventElement) {
|
||||
if (method === 'signEvent') {
|
||||
const card2SignEvent_jsonElement = document.getElementById(
|
||||
'card2SignEvent_json'
|
||||
);
|
||||
if (card2SignEvent_jsonElement) {
|
||||
card2SignEvent_jsonElement.innerText = event;
|
||||
}
|
||||
} else {
|
||||
cardSignEventElement.style.display = 'none';
|
||||
card2SignEventElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const cardNip04EncryptElement = document.getElementById('cardNip04Encrypt');
|
||||
const card2Nip04EncryptElement = document.getElementById('card2Nip04Encrypt');
|
||||
if (cardNip04EncryptElement && card2Nip04EncryptElement) {
|
||||
if (method === 'nip04.encrypt') {
|
||||
const card2Nip04Encrypt_textElement = document.getElementById(
|
||||
'card2Nip04Encrypt_text'
|
||||
);
|
||||
if (card2Nip04Encrypt_textElement) {
|
||||
const eventObject: { peerPubkey: string; plaintext: string } =
|
||||
JSON.parse(event);
|
||||
card2Nip04Encrypt_textElement.innerText = eventObject.plaintext;
|
||||
}
|
||||
} else {
|
||||
cardNip04EncryptElement.style.display = 'none';
|
||||
card2Nip04EncryptElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const cardNip04DecryptElement = document.getElementById('cardNip04Decrypt');
|
||||
const card2Nip04DecryptElement = document.getElementById('card2Nip04Decrypt');
|
||||
if (cardNip04DecryptElement && card2Nip04DecryptElement) {
|
||||
if (method === 'nip04.decrypt') {
|
||||
const card2Nip04Decrypt_textElement = document.getElementById(
|
||||
'card2Nip04Decrypt_text'
|
||||
);
|
||||
if (card2Nip04Decrypt_textElement) {
|
||||
const eventObject: { peerPubkey: string; ciphertext: string } =
|
||||
JSON.parse(event);
|
||||
card2Nip04Decrypt_textElement.innerText = eventObject.ciphertext;
|
||||
}
|
||||
} else {
|
||||
cardNip04DecryptElement.style.display = 'none';
|
||||
card2Nip04DecryptElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Functions
|
||||
//
|
||||
|
||||
function deliver(response: PromptResponse) {
|
||||
const message: PromptResponseMessage = {
|
||||
id,
|
||||
response,
|
||||
};
|
||||
|
||||
browser.runtime.sendMessage(message);
|
||||
window.close();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const rejectJustOnceButton = document.getElementById('rejectJustOnceButton');
|
||||
rejectJustOnceButton?.addEventListener('click', () => {
|
||||
deliver('reject-once');
|
||||
});
|
||||
|
||||
const rejectButton = document.getElementById('rejectButton');
|
||||
rejectButton?.addEventListener('click', () => {
|
||||
deliver('reject');
|
||||
});
|
||||
|
||||
const approveJustOnceButton = document.getElementById(
|
||||
'approveJustOnceButton'
|
||||
);
|
||||
approveJustOnceButton?.addEventListener('click', () => {
|
||||
deliver('approve-once');
|
||||
});
|
||||
|
||||
const approveButton = document.getElementById('approveButton');
|
||||
approveButton?.addEventListener('click', () => {
|
||||
deliver('approve');
|
||||
});
|
||||
});
|
||||
20
projects/chrome/src/styles.scss
Normal file
20
projects/chrome/src/styles.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
@use "sass:meta";
|
||||
|
||||
@include meta.load-css("../../../node_modules/bootstrap/scss/bootstrap");
|
||||
@include meta.load-css(
|
||||
"../../../node_modules/bootstrap-icons/font/bootstrap-icons.min.css"
|
||||
);
|
||||
|
||||
// Load the common styles
|
||||
@include meta.load-css("../../common/src/lib/styles/styles.scss");
|
||||
|
||||
body {
|
||||
height: 600px;
|
||||
width: 375px;
|
||||
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
background: var(--background);
|
||||
|
||||
margin: 0;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user