first chrome implementation
This commit is contained in:
5
projects/common/src/lib/common/nav-component.ts
Normal file
5
projects/common/src/lib/common/nav-component.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class NavComponent {
|
||||
navigateBack() {
|
||||
window.history.back();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<!-- eslint-disable @angular-eslint/template/elements-content -->
|
||||
<div [id]="idString" class="modal fade" data-bs-backdrop="static" tabindex="-1">
|
||||
<div
|
||||
class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollable"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Please confirm</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
(click)="modal?.hide(); no.emit()"
|
||||
></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span>{{ message }}</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="modal?.hide()">
|
||||
No
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" (click)="onClickYes()">
|
||||
Yes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
:host {
|
||||
.modal {
|
||||
--bs-modal-margin: 32px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ConfirmComponent } from './confirm.component';
|
||||
|
||||
describe('ConfirmComponent', () => {
|
||||
let component: ConfirmComponent;
|
||||
let fixture: ComponentFixture<ConfirmComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ConfirmComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ConfirmComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { AfterViewInit, Component, EventEmitter, Output } from '@angular/core';
|
||||
import * as bootstrap from 'bootstrap';
|
||||
|
||||
@Component({
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: 'lib-confirm',
|
||||
imports: [],
|
||||
templateUrl: './confirm.component.html',
|
||||
styleUrl: './confirm.component.scss',
|
||||
})
|
||||
export class ConfirmComponent implements AfterViewInit {
|
||||
@Output() yes = new EventEmitter<void>();
|
||||
@Output() no = new EventEmitter<void>();
|
||||
|
||||
message: string | undefined;
|
||||
onYes: ((() => Promise<void>) | (() => void)) | undefined;
|
||||
modal: bootstrap.Modal | undefined;
|
||||
|
||||
readonly idString = crypto.randomUUID();
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
const myModalEl = document.getElementById(this.idString);
|
||||
if (!myModalEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.modal = new bootstrap.Modal(myModalEl);
|
||||
}
|
||||
|
||||
onClickYes() {
|
||||
this.modal?.hide();
|
||||
if (typeof this.onYes !== 'undefined') {
|
||||
this.onYes();
|
||||
}
|
||||
}
|
||||
|
||||
show(message: string, onYes: (() => Promise<void>) | (() => void)): void {
|
||||
this.message = message;
|
||||
this.onYes = onYes;
|
||||
this.modal?.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="icon-button">
|
||||
<i [class]="'bi bi-' + icon"></i>
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
:host {
|
||||
.icon-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
//padding: 10px;
|
||||
|
||||
border-radius: 100%;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { IconButtonComponent } from './icon-button.component';
|
||||
|
||||
describe('IconButtonComponent', () => {
|
||||
let component: IconButtonComponent;
|
||||
let fixture: ComponentFixture<IconButtonComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [IconButtonComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(IconButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: 'lib-icon-button',
|
||||
imports: [],
|
||||
templateUrl: './icon-button.component.html',
|
||||
styleUrl: './icon-button.component.scss',
|
||||
})
|
||||
export class IconButtonComponent {
|
||||
@Input({ required: true }) icon!: string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<span class="sam-no-select text">
|
||||
{{ text }}
|
||||
</span>
|
||||
|
||||
<div class="sam-flex-grow"></div>
|
||||
|
||||
<div class="buttons sam-flex-row gap-h">
|
||||
<lib-icon-button
|
||||
icon="arrow-right"
|
||||
style="pointer-events: none"
|
||||
></lib-icon-button>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
:host {
|
||||
cursor: pointer;
|
||||
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;
|
||||
|
||||
&:hover {
|
||||
background: var(--background-light-hover);
|
||||
}
|
||||
|
||||
.text {
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NavItemComponent } from './nav-item.component';
|
||||
|
||||
describe('NavItemComponent', () => {
|
||||
let component: NavItemComponent;
|
||||
let fixture: ComponentFixture<NavItemComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NavItemComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NavItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { IconButtonComponent } from "../icon-button/icon-button.component";
|
||||
|
||||
@Component({
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: 'lib-nav-item',
|
||||
imports: [IconButtonComponent],
|
||||
templateUrl: './nav-item.component.html',
|
||||
styleUrl: './nav-item.component.scss',
|
||||
})
|
||||
export class NavItemComponent {
|
||||
@Input({ required: true }) text!: string;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<span [style.color]="color">{{ npubString }}</span>
|
||||
|
||||
<lib-icon-button icon="copy" (click)="copyToClipboard()"></lib-icon-button>
|
||||
@@ -0,0 +1,6 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
column-gap: calc(var(--size) / 2);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PubkeyComponent } from './pubkey.component';
|
||||
|
||||
describe('PubkeyComponent', () => {
|
||||
let component: PubkeyComponent;
|
||||
let fixture: ComponentFixture<PubkeyComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PubkeyComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PubkeyComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { NostrHelper } from '@common';
|
||||
import { IconButtonComponent } from "../icon-button/icon-button.component";
|
||||
|
||||
@Component({
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: 'lib-pubkey',
|
||||
imports: [IconButtonComponent],
|
||||
templateUrl: './pubkey.component.html',
|
||||
styleUrl: './pubkey.component.scss',
|
||||
})
|
||||
export class PubkeyComponent implements OnInit {
|
||||
@Input({ required: true }) value!: string;
|
||||
@Input() first = 9;
|
||||
@Input() last = 5;
|
||||
@Input() color = '#dee2e6bf';
|
||||
|
||||
npub: string | undefined;
|
||||
npubString: string | undefined;
|
||||
|
||||
ngOnInit(): void {
|
||||
const pubkeyObject = NostrHelper.getNostrPubkeyObject(this.value);
|
||||
this.npub = pubkeyObject.npub;
|
||||
this.npubString = NostrHelper.splitKey(
|
||||
pubkeyObject.npub,
|
||||
this.first,
|
||||
this.last
|
||||
);
|
||||
}
|
||||
|
||||
copyToClipboard() {
|
||||
if (!this.npub) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(this.npub);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<span>{{ type }}</span>
|
||||
@@ -0,0 +1,31 @@
|
||||
:host {
|
||||
border-radius: 4px;
|
||||
padding: 0px 4px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
border: 1px solid transparent;
|
||||
min-width: 40px;
|
||||
min-height: 20px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&.read {
|
||||
&:not(.is-selected) {
|
||||
border: 1px solid var(--bs-green);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background: var(--bs-green);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.read) {
|
||||
&:not(.is-selected) {
|
||||
border: 1px solid var(--bs-primary);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background: var(--bs-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RelayRwComponent } from './relay-rw.component';
|
||||
|
||||
describe('RelayRwComponent', () => {
|
||||
let component: RelayRwComponent;
|
||||
let fixture: ComponentFixture<RelayRwComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RelayRwComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RelayRwComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: 'lib-relay-rw',
|
||||
imports: [],
|
||||
templateUrl: './relay-rw.component.html',
|
||||
styleUrl: './relay-rw.component.scss',
|
||||
})
|
||||
export class RelayRwComponent {
|
||||
@Input({ required: true }) type!: 'read' | 'write';
|
||||
@Input({ required: true }) model!: boolean;
|
||||
@Output() modelChange = new EventEmitter<boolean>();
|
||||
|
||||
@HostBinding('class.read') get isRead() {
|
||||
return this.type === 'read';
|
||||
}
|
||||
|
||||
@HostBinding('class.is-selected') get isSelected() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
@HostListener('click') onClick() {
|
||||
this.model = !this.model;
|
||||
this.modelChange.emit(this.model);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<div
|
||||
[id]="idString"
|
||||
class="toast hide"
|
||||
style="width: 100%"
|
||||
role="alert"
|
||||
>
|
||||
<div class="toast-body">{{ message }}</div>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
:host {
|
||||
position: absolute;
|
||||
align-self: center;
|
||||
//bottom: 76px;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ToastComponent } from './toast.component';
|
||||
|
||||
describe('ToastComponent', () => {
|
||||
let component: ToastComponent;
|
||||
let fixture: ComponentFixture<ToastComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ToastComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ToastComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
37
projects/common/src/lib/components/toast/toast.component.ts
Normal file
37
projects/common/src/lib/components/toast/toast.component.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { AfterViewInit, Component, HostBinding, Input } from '@angular/core';
|
||||
import * as bootstrap from 'bootstrap';
|
||||
|
||||
@Component({
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: 'lib-toast',
|
||||
imports: [],
|
||||
templateUrl: './toast.component.html',
|
||||
styleUrl: './toast.component.scss',
|
||||
})
|
||||
export class ToastComponent implements AfterViewInit {
|
||||
@Input() message: string | undefined;
|
||||
|
||||
@Input()
|
||||
@HostBinding('style.bottom.px')
|
||||
bottom = 76;
|
||||
|
||||
readonly idString = crypto.randomUUID();
|
||||
|
||||
toast: bootstrap.Toast | undefined;
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
const myToastEl = document.getElementById(this.idString);
|
||||
if (!myToastEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.toast = new bootstrap.Toast(myToastEl, { delay: 2000 });
|
||||
}
|
||||
|
||||
show(message?: string) {
|
||||
if (message) {
|
||||
this.message = message;
|
||||
}
|
||||
this.toast?.show();
|
||||
}
|
||||
}
|
||||
93
projects/common/src/lib/helpers/crypto-helper.ts
Normal file
93
projects/common/src/lib/helpers/crypto-helper.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
export class CryptoHelper {
|
||||
/**
|
||||
* Generate a base64 encoded IV.
|
||||
*/
|
||||
static generateIV(): string {
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
return Buffer.from(iv).toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash (SHA-256) a text string.
|
||||
*/
|
||||
static async hash(text: string): Promise<string> {
|
||||
const textUint8 = new TextEncoder().encode(text); // encode as (utf-8) Uint8Array
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', textUint8); // hash the message
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
||||
const hashHex = hashArray
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join(''); // convert bytes to hex string
|
||||
return hashHex;
|
||||
}
|
||||
|
||||
static v4(): string {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
static async deriveKey(password: string): Promise<CryptoKey> {
|
||||
const algo = {
|
||||
name: 'PBKDF2',
|
||||
hash: 'SHA-256',
|
||||
salt: new TextEncoder().encode('3e7cdebd-3b4c-4125-a18c-05750cad8ec3'),
|
||||
iterations: 1000,
|
||||
};
|
||||
return crypto.subtle.deriveKey(
|
||||
algo,
|
||||
await crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(password),
|
||||
{
|
||||
name: algo.name,
|
||||
},
|
||||
false,
|
||||
['deriveKey']
|
||||
),
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256,
|
||||
},
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
|
||||
static async encrypt(
|
||||
text: string,
|
||||
ivBase64String: string,
|
||||
password: string
|
||||
): Promise<string> {
|
||||
const algo = {
|
||||
name: 'AES-GCM',
|
||||
length: 256,
|
||||
iv: Buffer.from(ivBase64String, 'base64'),
|
||||
};
|
||||
|
||||
const cipherText = await crypto.subtle.encrypt(
|
||||
algo,
|
||||
await CryptoHelper.deriveKey(password),
|
||||
new TextEncoder().encode(text)
|
||||
);
|
||||
return Buffer.from(cipherText).toString('base64');
|
||||
}
|
||||
|
||||
static async decrypt(
|
||||
encryptedBase64String: string,
|
||||
ivBase64String: string,
|
||||
password: string
|
||||
): Promise<string> {
|
||||
const algo = {
|
||||
name: 'AES-GCM',
|
||||
length: 256,
|
||||
iv: Buffer.from(ivBase64String, 'base64'),
|
||||
};
|
||||
return new TextDecoder().decode(
|
||||
await crypto.subtle.decrypt(
|
||||
algo,
|
||||
await CryptoHelper.deriveKey(password),
|
||||
Buffer.from(encryptedBase64String, 'base64')
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
10
projects/common/src/lib/helpers/date-helper.ts
Normal file
10
projects/common/src/lib/helpers/date-helper.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export class DateHelper {
|
||||
static dateToISOLikeButLocal(date: Date): string {
|
||||
const offsetMs = date.getTimezoneOffset() * 60 * 1000;
|
||||
const msLocal = date.getTime() - offsetMs;
|
||||
const dateLocal = new Date(msLocal);
|
||||
const iso = dateLocal.toISOString();
|
||||
const isoLocal = iso.slice(0, 19).replace('T', ' ').replaceAll(':', '.');
|
||||
return isoLocal;
|
||||
}
|
||||
}
|
||||
128
projects/common/src/lib/helpers/nostr-helper.ts
Normal file
128
projects/common/src/lib/helpers/nostr-helper.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { bech32 } from '@scure/base';
|
||||
import * as utils from '@noble/curves/abstract/utils';
|
||||
import { getPublicKey } from 'nostr-tools';
|
||||
|
||||
export interface NostrHexObject {
|
||||
represents: string;
|
||||
hex: string;
|
||||
}
|
||||
|
||||
export interface NostrPubkeyObject {
|
||||
hex: string;
|
||||
npub: string;
|
||||
}
|
||||
|
||||
export interface NostrPrivkeyObject {
|
||||
hex: string;
|
||||
nsec: string;
|
||||
}
|
||||
|
||||
export class NostrHelper {
|
||||
static getNostrPrivkeyObject(nsec_OR_hex: string): NostrPrivkeyObject {
|
||||
// 1. Assume we got an nsec.
|
||||
// Try to generate hex value.
|
||||
try {
|
||||
const hexObject = this.#nSomething2hexObject(nsec_OR_hex);
|
||||
if (hexObject.represents !== 'nsec') {
|
||||
throw new Error('The provided string is NOT an nsec.');
|
||||
}
|
||||
|
||||
// Everything is fine. The provided string IS an nsec.
|
||||
return {
|
||||
hex: hexObject.hex,
|
||||
nsec: nsec_OR_hex,
|
||||
};
|
||||
} catch (error) {
|
||||
// Continue.
|
||||
}
|
||||
|
||||
// 2. Assume we got an hex.
|
||||
// Try to generate the nsec.
|
||||
try {
|
||||
const nsec = NostrHelper.privkey2nsec(nsec_OR_hex);
|
||||
return {
|
||||
hex: nsec_OR_hex,
|
||||
nsec,
|
||||
};
|
||||
} catch (error) {
|
||||
// Continue;
|
||||
}
|
||||
|
||||
throw new Error('Could not convert the provided string into nsec/hex.');
|
||||
}
|
||||
|
||||
static getNostrPubkeyObject(npub_OR_hex: string): NostrPubkeyObject {
|
||||
// 1. Assume we got an npub.
|
||||
// Try to generate hex value.
|
||||
try {
|
||||
const hexObject = this.#nSomething2hexObject(npub_OR_hex);
|
||||
if (hexObject.represents !== 'npub') {
|
||||
throw new Error('The provided string is NOT an npub.');
|
||||
}
|
||||
|
||||
// Everything is fine. The provided string IS an npub.
|
||||
return {
|
||||
hex: hexObject.hex,
|
||||
npub: npub_OR_hex,
|
||||
};
|
||||
} catch (error) {
|
||||
// Continue.
|
||||
}
|
||||
|
||||
// 2. Assume we got an hex.
|
||||
// Try to generate the npub.
|
||||
try {
|
||||
const npub = NostrHelper.pubkey2npub(npub_OR_hex);
|
||||
return {
|
||||
hex: npub_OR_hex,
|
||||
npub,
|
||||
};
|
||||
} catch (error) {
|
||||
// Continue;
|
||||
}
|
||||
|
||||
throw new Error('Could not convert the provided string into npub/hex.');
|
||||
}
|
||||
|
||||
static pubkey2npub(hex: string): string {
|
||||
const data = utils.hexToBytes(hex);
|
||||
const words = bech32.toWords(data);
|
||||
return bech32.encode('npub', words, 5000);
|
||||
}
|
||||
|
||||
static privkey2nsec(hex: string): string {
|
||||
const data = utils.hexToBytes(hex);
|
||||
const words = bech32.toWords(data);
|
||||
return bech32.encode('nsec', words, 5000);
|
||||
}
|
||||
|
||||
static pubkeyFromPrivkey(hex: string): string {
|
||||
const privkeyBytes = utils.hexToBytes(hex);
|
||||
return getPublicKey(privkeyBytes);
|
||||
}
|
||||
|
||||
static hex2bytes(hex: string): Uint8Array {
|
||||
return utils.hexToBytes(hex);
|
||||
}
|
||||
|
||||
static splitKey(text: string, first: number, last: number): string {
|
||||
const part1 = text.slice(0, first);
|
||||
const part2 = '...';
|
||||
const part3 = text.slice(-last);
|
||||
return `${part1}${part2}${part3}`;
|
||||
}
|
||||
|
||||
static #nSomething2hexObject(nSomething: string): NostrHexObject {
|
||||
const { prefix, words } = bech32.decode(
|
||||
nSomething as `${string}1${string}`,
|
||||
5000
|
||||
);
|
||||
const data = new Uint8Array(bech32.fromWords(words));
|
||||
|
||||
return {
|
||||
represents: prefix,
|
||||
hex: utils.bytesToHex(data),
|
||||
};
|
||||
}
|
||||
}
|
||||
8
projects/common/src/lib/helpers/text-helper.ts
Normal file
8
projects/common/src/lib/helpers/text-helper.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export class TextHelper {
|
||||
/**
|
||||
* Takes a string returns something like "\<first-x-chars>...\<last-y-chars>""
|
||||
*/
|
||||
static split(text: string, first: number, last: number): string {
|
||||
return `${text.slice(0, first)}...${text.slice(-last)}`;
|
||||
}
|
||||
}
|
||||
8
projects/common/src/lib/models/nostr.ts
Normal file
8
projects/common/src/lib/models/nostr.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type Nip07Method =
|
||||
| 'signEvent'
|
||||
| 'getPublicKey'
|
||||
| 'getRelays'
|
||||
| 'nip04.encrypt'
|
||||
| 'nip04.decrypt';
|
||||
|
||||
export type Nip07MethodPolicy = 'allow' | 'deny';
|
||||
18
projects/common/src/lib/pipes/visual-nip05.pipe.ts
Normal file
18
projects/common/src/lib/pipes/visual-nip05.pipe.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'visualNip05',
|
||||
})
|
||||
export class VisualNip05Pipe implements PipeTransform {
|
||||
transform(value: string | undefined): string {
|
||||
if (typeof value === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (value.startsWith('_@')) {
|
||||
return value.split('_@')[1];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
10
projects/common/src/lib/pipes/visual-relay.pipe.ts
Normal file
10
projects/common/src/lib/pipes/visual-relay.pipe.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'visualRelay',
|
||||
})
|
||||
export class VisualRelayPipe implements PipeTransform {
|
||||
transform(value: string): string {
|
||||
return value.toLowerCase().replaceAll('wss://', '');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LoggerService } from './logger.service';
|
||||
|
||||
describe('LoggerService', () => {
|
||||
let service: LoggerService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(LoggerService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
29
projects/common/src/lib/services/logger/logger.service.ts
Normal file
29
projects/common/src/lib/services/logger/logger.service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LoggerService {
|
||||
#namespace: string | undefined;
|
||||
|
||||
initialize(namespace: string): void {
|
||||
this.#namespace = namespace;
|
||||
}
|
||||
|
||||
log(value: any) {
|
||||
this.#assureInitialized();
|
||||
|
||||
const nowString = new Date().toLocaleString();
|
||||
|
||||
console.log(`[${this.#namespace} - ${nowString}]`, JSON.stringify(value));
|
||||
}
|
||||
|
||||
#assureInitialized() {
|
||||
if (!this.#namespace) {
|
||||
throw new Error(
|
||||
'LoggerService not initialized. Please call initialize(..) first.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export abstract class BrowserLocalHandler {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { BrowserSessionData } from './types';
|
||||
|
||||
export abstract class BrowserSessionHandler {
|
||||
get browserSessionData(): BrowserSessionData | undefined {
|
||||
return this.#browserSessionData;
|
||||
}
|
||||
|
||||
#browserSessionData?: BrowserSessionData;
|
||||
|
||||
/**
|
||||
* Load the data from the browser session storage. It should be an empty object,
|
||||
* if no data is available yet (e.g. because the vault (from the browser sync data)
|
||||
* was not unlocked via password).
|
||||
*
|
||||
* ATTENTION: Make sure to call "setFullData(..)" afterwards to update the in-memory data.
|
||||
*/
|
||||
abstract loadFullData(): Promise<Partial<Record<string, any>>>;
|
||||
setFullData(data: BrowserSessionData) {
|
||||
this.#browserSessionData = JSON.parse(JSON.stringify(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the full data to the session data storage.
|
||||
*
|
||||
* ATTENTION: Make sure to call "setFullData(..)" afterwards of before to update the in-memory data.
|
||||
*/
|
||||
abstract saveFullData(data: BrowserSessionData): Promise<void>;
|
||||
|
||||
abstract clearData(): Promise<void>;
|
||||
}
|
||||
111
projects/common/src/lib/services/storage/browser-sync-handler.ts
Normal file
111
projects/common/src/lib/services/storage/browser-sync-handler.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
BrowserSyncData,
|
||||
Identity_ENCRYPTED,
|
||||
Permission_ENCRYPTED,
|
||||
Relay_ENCRYPTED,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* This class handles the data that is synced between browser instances.
|
||||
* In addition to the sensitive data that is encrypted, it also contains
|
||||
* some unencrypted properties (like, version and the vault hash).
|
||||
*/
|
||||
export abstract class BrowserSyncHandler {
|
||||
get browserSyncData(): BrowserSyncData | undefined {
|
||||
return this.#browserSyncData;
|
||||
}
|
||||
|
||||
get ignoreProperties(): string[] {
|
||||
return this.#ignoreProperties;
|
||||
}
|
||||
|
||||
#browserSyncData?: BrowserSyncData;
|
||||
#ignoreProperties: string[] = [];
|
||||
|
||||
setIgnoreProperties(properties: string[]) {
|
||||
this.#ignoreProperties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load data from the sync data storage. This data might be
|
||||
* outdated (i.e. it is unmigrated), so check the unencrypted property "version" after loading.
|
||||
* Also make sure to handle the "ignore properties" (if available).
|
||||
*/
|
||||
abstract loadUnmigratedData(): Promise<Partial<Record<string, any>>>;
|
||||
|
||||
/**
|
||||
* Persist the full data to the sync data storage.
|
||||
*
|
||||
* ATTENTION: In your implementation, make sure to call "setFullData(..)" at the end to update the in-memory data.
|
||||
*/
|
||||
abstract saveAndSetFullData(data: BrowserSyncData): Promise<void>;
|
||||
|
||||
setFullData(data: BrowserSyncData) {
|
||||
this.#browserSyncData = JSON.parse(JSON.stringify(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the permissions to the sync data storage.
|
||||
*
|
||||
* ATTENTION: In your implementation, make sure to call "setPartialData_Permissions(..)" at the end to update the in-memory data.
|
||||
*/
|
||||
abstract saveAndSetPartialData_Permissions(data: {
|
||||
permissions: Permission_ENCRYPTED[];
|
||||
}): Promise<void>;
|
||||
setPartialData_Permissions(data: { permissions: Permission_ENCRYPTED[] }) {
|
||||
if (!this.#browserSyncData) {
|
||||
return;
|
||||
}
|
||||
this.#browserSyncData.permissions = Array.from(data.permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the identities to the sync data storage.
|
||||
*
|
||||
* ATTENTION: In your implementation, make sure to call "setPartialData_Identities(..)" at the end to update the in-memory data.
|
||||
*/
|
||||
abstract saveAndSetPartialData_Identities(data: {
|
||||
identities: Identity_ENCRYPTED[];
|
||||
}): Promise<void>;
|
||||
|
||||
setPartialData_Identities(data: { identities: Identity_ENCRYPTED[] }) {
|
||||
if (!this.#browserSyncData) {
|
||||
return;
|
||||
}
|
||||
this.#browserSyncData.identities = Array.from(data.identities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the selected identity id to the sync data storage.
|
||||
*
|
||||
* ATTENTION: In your implementation, make sure to call "setPartialData_SelectedIdentityId(..)" at the end to update the in-memory data.
|
||||
*/
|
||||
abstract saveAndSetPartialData_SelectedIdentityId(data: {
|
||||
selectedIdentityId: string | null;
|
||||
}): Promise<void>;
|
||||
|
||||
setPartialData_SelectedIdentityId(data: {
|
||||
selectedIdentityId: string | null;
|
||||
}) {
|
||||
if (!this.#browserSyncData) {
|
||||
return;
|
||||
}
|
||||
this.#browserSyncData.selectedIdentityId = data.selectedIdentityId;
|
||||
}
|
||||
|
||||
abstract saveAndSetPartialData_Relays(data: {
|
||||
relays: Relay_ENCRYPTED[];
|
||||
}): Promise<void>;
|
||||
setPartialData_Relays(data: { relays: Relay_ENCRYPTED[] }) {
|
||||
if (!this.#browserSyncData) {
|
||||
return;
|
||||
}
|
||||
this.#browserSyncData.relays = Array.from(data.relays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data from the sync data storage.
|
||||
*/
|
||||
abstract clearData(): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { BrowserSyncFlow, GootiMetaData } from './types';
|
||||
|
||||
export abstract class GootiMetaHandler {
|
||||
get gootiMetaData(): GootiMetaData | undefined {
|
||||
return this.#gootiMetaData;
|
||||
}
|
||||
|
||||
#gootiMetaData?: GootiMetaData;
|
||||
|
||||
readonly metaProperties = ['syncFlow'];
|
||||
/**
|
||||
* Load the full data from the storage. If the storage is used for storing
|
||||
* other data (e.g. browser sync data when the user decided to NOT sync),
|
||||
* make sure to handle the "meta properties" to only load these.
|
||||
*
|
||||
* ATTENTION: Make sure to call "setFullData(..)" afterwards to update the in-memory data.
|
||||
*/
|
||||
abstract loadFullData(): Promise<Partial<Record<string, any>>>;
|
||||
|
||||
setFullData(data: GootiMetaData) {
|
||||
this.#gootiMetaData = data;
|
||||
}
|
||||
|
||||
abstract saveFullData(data: GootiMetaData): Promise<void>;
|
||||
|
||||
/**
|
||||
* Sets the browser sync flow for the user and immediately saves it.
|
||||
*/
|
||||
async setBrowserSyncFlow(flow: BrowserSyncFlow): Promise<void> {
|
||||
if (!this.#gootiMetaData) {
|
||||
this.#gootiMetaData = {
|
||||
syncFlow: flow,
|
||||
};
|
||||
} else {
|
||||
this.#gootiMetaData.syncFlow = flow;
|
||||
}
|
||||
|
||||
await this.saveFullData(this.#gootiMetaData);
|
||||
}
|
||||
|
||||
abstract clearData(): Promise<void>;
|
||||
}
|
||||
232
projects/common/src/lib/services/storage/related/identity.ts
Normal file
232
projects/common/src/lib/services/storage/related/identity.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import {
|
||||
CryptoHelper,
|
||||
Identity_DECRYPTED,
|
||||
Identity_ENCRYPTED,
|
||||
NostrHelper,
|
||||
StorageService,
|
||||
} from '@common';
|
||||
|
||||
export const addIdentity = async function (
|
||||
this: StorageService,
|
||||
data: {
|
||||
nick: string;
|
||||
privkeyString: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
const privkey = NostrHelper.getNostrPrivkeyObject(
|
||||
data.privkeyString.toLowerCase()
|
||||
).hex;
|
||||
|
||||
// Check if an identity with the same privkey already exists.
|
||||
const existingIdentity = (
|
||||
this.getBrowserSessionHandler().browserSessionData?.identities ?? []
|
||||
).find((x) => x.privkey === privkey);
|
||||
if (existingIdentity) {
|
||||
throw new Error(
|
||||
`An identity with the same private key already exists: ${existingIdentity.nick}`
|
||||
);
|
||||
}
|
||||
|
||||
const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
||||
if (!browserSessionData) {
|
||||
throw new Error('Browser session data is undefined.');
|
||||
}
|
||||
|
||||
const decryptedIdentity: Identity_DECRYPTED = {
|
||||
id: CryptoHelper.v4(),
|
||||
nick: data.nick,
|
||||
privkey,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Add the new identity to the session data.
|
||||
browserSessionData.identities.push(decryptedIdentity);
|
||||
let isFirstIdentity = false;
|
||||
if (browserSessionData.identities.length === 1) {
|
||||
isFirstIdentity = true;
|
||||
browserSessionData.selectedIdentityId = decryptedIdentity.id;
|
||||
}
|
||||
this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
||||
|
||||
// Encrypt the new identity and add it to the sync data.
|
||||
const encryptedIdentity = await encryptIdentity.call(this, decryptedIdentity);
|
||||
const encryptedIdentities = [
|
||||
...(this.getBrowserSyncHandler().browserSyncData?.identities ?? []),
|
||||
encryptedIdentity,
|
||||
];
|
||||
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_Identities({
|
||||
identities: encryptedIdentities,
|
||||
});
|
||||
|
||||
if (isFirstIdentity) {
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_SelectedIdentityId(
|
||||
{
|
||||
selectedIdentityId: encryptedIdentity.id,
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteIdentity = async function (
|
||||
this: StorageService,
|
||||
identityId: string | undefined
|
||||
): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
if (!identityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
||||
const browserSyncData = this.getBrowserSyncHandler().browserSyncData;
|
||||
if (!browserSessionData || !browserSyncData) {
|
||||
throw new Error('Browser session or sync data is undefined.');
|
||||
}
|
||||
|
||||
browserSessionData.identities = browserSessionData.identities.filter(
|
||||
(x) => x.id !== identityId
|
||||
);
|
||||
browserSessionData.permissions = browserSessionData.permissions.filter(
|
||||
(x) => x.identityId !== identityId
|
||||
);
|
||||
browserSessionData.relays = browserSessionData.relays.filter(
|
||||
(x) => x.identityId !== identityId
|
||||
);
|
||||
if (browserSessionData.selectedIdentityId === identityId) {
|
||||
// Choose another identity to be selected or null if there is none.
|
||||
browserSessionData.selectedIdentityId =
|
||||
browserSessionData.identities.length > 0
|
||||
? browserSessionData.identities[0].id
|
||||
: null;
|
||||
}
|
||||
await this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
||||
|
||||
// Handle Sync data.
|
||||
const encryptedIdentityId = await this.encrypt(identityId);
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_Identities({
|
||||
identities: browserSyncData.identities.filter(
|
||||
(x) => x.id !== encryptedIdentityId
|
||||
),
|
||||
});
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_Permissions({
|
||||
permissions: browserSyncData.permissions.filter(
|
||||
(x) => x.identityId !== encryptedIdentityId
|
||||
),
|
||||
});
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_Relays({
|
||||
relays: browserSyncData.relays.filter(
|
||||
(x) => x.identityId !== encryptedIdentityId
|
||||
),
|
||||
});
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_SelectedIdentityId({
|
||||
selectedIdentityId:
|
||||
browserSessionData.selectedIdentityId === null
|
||||
? null
|
||||
: await this.encrypt(browserSessionData.selectedIdentityId),
|
||||
});
|
||||
};
|
||||
|
||||
export const switchIdentity = async function (
|
||||
this: StorageService,
|
||||
identityId: string | null
|
||||
): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
// Check, if the identity really exists.
|
||||
const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
||||
|
||||
if (!browserSessionData?.identities.find((x) => x.id === identityId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
browserSessionData.selectedIdentityId = identityId;
|
||||
await this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
||||
|
||||
const encryptedIdentityId =
|
||||
identityId === null ? null : await this.encrypt(identityId);
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_SelectedIdentityId({
|
||||
selectedIdentityId: encryptedIdentityId,
|
||||
});
|
||||
};
|
||||
|
||||
export const encryptIdentity = async function (
|
||||
this: StorageService,
|
||||
identity: Identity_DECRYPTED
|
||||
): Promise<Identity_ENCRYPTED> {
|
||||
const encryptedIdentity: Identity_ENCRYPTED = {
|
||||
id: await this.encrypt(identity.id),
|
||||
nick: await this.encrypt(identity.nick),
|
||||
createdAt: await this.encrypt(identity.createdAt),
|
||||
privkey: await this.encrypt(identity.privkey),
|
||||
};
|
||||
|
||||
return encryptedIdentity;
|
||||
};
|
||||
|
||||
export const decryptIdentities = async function (
|
||||
this: StorageService,
|
||||
identities: Identity_ENCRYPTED[],
|
||||
withLockedVault: { iv: string; password: string } | undefined = undefined
|
||||
): Promise<Identity_DECRYPTED[]> {
|
||||
const decryptedIdentities: Identity_DECRYPTED[] = [];
|
||||
|
||||
for (const identity of identities) {
|
||||
const decryptedIdentity = await decryptIdentity.call(
|
||||
this,
|
||||
identity,
|
||||
withLockedVault
|
||||
);
|
||||
decryptedIdentities.push(decryptedIdentity);
|
||||
}
|
||||
|
||||
return decryptedIdentities;
|
||||
};
|
||||
|
||||
export const decryptIdentity = async function (
|
||||
this: StorageService,
|
||||
identity: Identity_ENCRYPTED,
|
||||
withLockedVault: { iv: string; password: string } | undefined = undefined
|
||||
): Promise<Identity_DECRYPTED> {
|
||||
if (typeof withLockedVault === 'undefined') {
|
||||
const decryptedIdentity: Identity_DECRYPTED = {
|
||||
id: await this.decrypt(identity.id, 'string'),
|
||||
nick: await this.decrypt(identity.nick, 'string'),
|
||||
createdAt: await this.decrypt(identity.createdAt, 'string'),
|
||||
privkey: await this.decrypt(identity.privkey, 'string'),
|
||||
};
|
||||
|
||||
return decryptedIdentity;
|
||||
}
|
||||
|
||||
const decryptedIdentity: Identity_DECRYPTED = {
|
||||
id: await this.decryptWithLockedVault(
|
||||
identity.id,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password
|
||||
),
|
||||
nick: await this.decryptWithLockedVault(
|
||||
identity.nick,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password
|
||||
),
|
||||
createdAt: await this.decryptWithLockedVault(
|
||||
identity.createdAt,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password
|
||||
),
|
||||
privkey: await this.decryptWithLockedVault(
|
||||
identity.privkey,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password
|
||||
),
|
||||
};
|
||||
|
||||
return decryptedIdentity;
|
||||
};
|
||||
111
projects/common/src/lib/services/storage/related/permission.ts
Normal file
111
projects/common/src/lib/services/storage/related/permission.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
Permission_DECRYPTED,
|
||||
Permission_ENCRYPTED,
|
||||
StorageService,
|
||||
} from '@common';
|
||||
|
||||
export const deletePermission = async function (
|
||||
this: StorageService,
|
||||
permissionId: string
|
||||
): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
||||
const browserSyncData = this.getBrowserSyncHandler().browserSyncData;
|
||||
if (!browserSessionData || !browserSyncData) {
|
||||
throw new Error('Browser session or sync data is undefined.');
|
||||
}
|
||||
|
||||
browserSessionData.permissions = browserSessionData.permissions.filter(
|
||||
(x) => x.id !== permissionId
|
||||
);
|
||||
await this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
||||
|
||||
const encryptedPermissionId = await this.encrypt(permissionId);
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_Permissions({
|
||||
permissions: browserSyncData.permissions.filter(
|
||||
(x) => x.id !== encryptedPermissionId
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
export const decryptPermission = async function (
|
||||
this: StorageService,
|
||||
permission: Permission_ENCRYPTED,
|
||||
withLockedVault: { iv: string; password: string } | undefined = undefined
|
||||
): Promise<Permission_DECRYPTED> {
|
||||
if (typeof withLockedVault === 'undefined') {
|
||||
const decryptedPermission: Permission_DECRYPTED = {
|
||||
id: await this.decrypt(permission.id, 'string'),
|
||||
identityId: await this.decrypt(permission.identityId, 'string'),
|
||||
method: await this.decrypt(permission.method, 'string'),
|
||||
methodPolicy: await this.decrypt(permission.methodPolicy, 'string'),
|
||||
host: await this.decrypt(permission.host, 'string'),
|
||||
};
|
||||
if (permission.kind) {
|
||||
decryptedPermission.kind = await this.decrypt(permission.kind, 'number');
|
||||
}
|
||||
return decryptedPermission;
|
||||
}
|
||||
|
||||
const decryptedPermission: Permission_DECRYPTED = {
|
||||
id: await this.decryptWithLockedVault(
|
||||
permission.id,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password
|
||||
),
|
||||
identityId: await this.decryptWithLockedVault(
|
||||
permission.identityId,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password
|
||||
),
|
||||
method: await this.decryptWithLockedVault(
|
||||
permission.method,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password
|
||||
),
|
||||
methodPolicy: await this.decryptWithLockedVault(
|
||||
permission.methodPolicy,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password
|
||||
),
|
||||
host: await this.decryptWithLockedVault(
|
||||
permission.host,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password
|
||||
),
|
||||
};
|
||||
if (permission.kind) {
|
||||
decryptedPermission.kind = await this.decryptWithLockedVault(
|
||||
permission.kind,
|
||||
'number',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password
|
||||
);
|
||||
}
|
||||
return decryptedPermission;
|
||||
};
|
||||
|
||||
export const decryptPermissions = async function (
|
||||
this: StorageService,
|
||||
permissions: Permission_ENCRYPTED[],
|
||||
withLockedVault: { iv: string; password: string } | undefined = undefined
|
||||
): Promise<Permission_DECRYPTED[]> {
|
||||
const decryptedPermissions: Permission_DECRYPTED[] = [];
|
||||
|
||||
for (const permission of permissions) {
|
||||
const decryptedPermission = await decryptPermission.call(
|
||||
this,
|
||||
permission,
|
||||
withLockedVault
|
||||
);
|
||||
decryptedPermissions.push(decryptedPermission);
|
||||
}
|
||||
|
||||
return decryptedPermissions;
|
||||
};
|
||||
209
projects/common/src/lib/services/storage/related/relay.ts
Normal file
209
projects/common/src/lib/services/storage/related/relay.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import {
|
||||
CryptoHelper,
|
||||
Relay_DECRYPTED,
|
||||
Relay_ENCRYPTED,
|
||||
StorageService,
|
||||
} from '@common';
|
||||
|
||||
export const addRelay = async function (
|
||||
this: StorageService,
|
||||
data: {
|
||||
identityId: string;
|
||||
url: string;
|
||||
write: boolean;
|
||||
read: boolean;
|
||||
}
|
||||
): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
// Check, if a relay with the same URL already exists for the identity.
|
||||
const existingRelay =
|
||||
this.getBrowserSessionHandler().browserSessionData?.relays.find(
|
||||
(x) =>
|
||||
x.url.toLowerCase() === data.url.toLowerCase() &&
|
||||
x.identityId === data.identityId
|
||||
);
|
||||
if (existingRelay) {
|
||||
throw new Error('A relay with the same URL already exists.');
|
||||
}
|
||||
|
||||
const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
||||
if (!browserSessionData) {
|
||||
throw new Error('Browser session data is undefined.');
|
||||
}
|
||||
|
||||
const decryptedRelay: Relay_DECRYPTED = {
|
||||
id: CryptoHelper.v4(),
|
||||
identityId: data.identityId,
|
||||
url: data.url,
|
||||
write: data.write,
|
||||
read: data.read,
|
||||
};
|
||||
|
||||
// Add the new relay to the session data.
|
||||
browserSessionData.relays.push(decryptedRelay);
|
||||
this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
||||
|
||||
// Encrypt the new relay and add it to the sync data.
|
||||
const encryptedRelay = await encryptRelay.call(this, decryptedRelay);
|
||||
const encryptedRelays = [
|
||||
...(this.getBrowserSyncHandler().browserSyncData?.relays ?? []),
|
||||
encryptedRelay,
|
||||
];
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_Relays({
|
||||
relays: encryptedRelays,
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteRelay = async function (
|
||||
this: StorageService,
|
||||
relayId: string
|
||||
): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
if (!relayId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
||||
const browserSyncData = this.getBrowserSyncHandler().browserSyncData;
|
||||
if (!browserSessionData || !browserSyncData) {
|
||||
throw new Error('Browser session or sync data is undefined.');
|
||||
}
|
||||
|
||||
browserSessionData.relays = browserSessionData.relays.filter(
|
||||
(x) => x.id !== relayId
|
||||
);
|
||||
await this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
||||
|
||||
// Handle Sync data.
|
||||
const encryptedRelayId = await this.encrypt(relayId);
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_Relays({
|
||||
relays: browserSyncData.relays.filter((x) => x.id !== encryptedRelayId),
|
||||
});
|
||||
};
|
||||
|
||||
export const updateRelay = async function (
|
||||
this: StorageService,
|
||||
relayClone: Relay_DECRYPTED
|
||||
): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
||||
const browserSyncData = this.getBrowserSyncHandler().browserSyncData;
|
||||
if (!browserSessionData || !browserSyncData) {
|
||||
throw new Error('Browser session or sync data is undefined.');
|
||||
}
|
||||
|
||||
const sessionRelay = browserSessionData.relays.find(
|
||||
(x) => x.id === relayClone.id
|
||||
);
|
||||
const encryptedRelayId = await this.encrypt(relayClone.id);
|
||||
const syncRelay = browserSyncData.relays.find(
|
||||
(x) => x.id === encryptedRelayId
|
||||
);
|
||||
if (!sessionRelay || !syncRelay) {
|
||||
throw new Error(
|
||||
'Relay not found in browser session or sync data for update.'
|
||||
);
|
||||
}
|
||||
|
||||
// Handle Session update.
|
||||
sessionRelay.read = relayClone.read;
|
||||
sessionRelay.write = relayClone.write;
|
||||
sessionRelay.url = relayClone.url;
|
||||
await this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
||||
|
||||
// Handle Sync update.
|
||||
syncRelay.read = await this.encrypt(relayClone.read.toString());
|
||||
syncRelay.write = await this.encrypt(relayClone.write.toString());
|
||||
syncRelay.url = await this.encrypt(relayClone.url);
|
||||
await this.getBrowserSyncHandler().saveAndSetPartialData_Relays({
|
||||
relays: browserSyncData.relays,
|
||||
});
|
||||
};
|
||||
|
||||
export const decryptRelay = async function (
|
||||
this: StorageService,
|
||||
relay: Relay_ENCRYPTED,
|
||||
withLockedVault: { iv: string; password: string } | undefined = undefined
|
||||
): Promise<Relay_DECRYPTED> {
|
||||
if (typeof withLockedVault === 'undefined') {
|
||||
const decryptedRelay: Relay_DECRYPTED = {
|
||||
id: await this.decrypt(relay.id, 'string'),
|
||||
identityId: await this.decrypt(relay.identityId, 'string'),
|
||||
url: await this.decrypt(relay.url, 'string'),
|
||||
read: await this.decrypt(relay.read, 'boolean'),
|
||||
write: await this.decrypt(relay.write, 'boolean'),
|
||||
};
|
||||
return decryptedRelay;
|
||||
}
|
||||
|
||||
const decryptedRelay: Relay_DECRYPTED = {
|
||||
id: await this.decryptWithLockedVault(
|
||||
relay.id,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password
|
||||
),
|
||||
identityId: await this.decryptWithLockedVault(
|
||||
relay.identityId,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password
|
||||
),
|
||||
url: await this.decryptWithLockedVault(
|
||||
relay.url,
|
||||
'string',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password
|
||||
),
|
||||
read: await this.decryptWithLockedVault(
|
||||
relay.read,
|
||||
'boolean',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password
|
||||
),
|
||||
write: await this.decryptWithLockedVault(
|
||||
relay.write,
|
||||
'boolean',
|
||||
withLockedVault.iv,
|
||||
withLockedVault.password
|
||||
),
|
||||
};
|
||||
return decryptedRelay;
|
||||
};
|
||||
|
||||
export const decryptRelays = async function (
|
||||
this: StorageService,
|
||||
relays: Relay_ENCRYPTED[],
|
||||
withLockedVault: { iv: string; password: string } | undefined = undefined
|
||||
): Promise<Relay_DECRYPTED[]> {
|
||||
const decryptedRelays: Relay_DECRYPTED[] = [];
|
||||
|
||||
for (const relay of relays) {
|
||||
const decryptedRelay = await decryptRelay.call(
|
||||
this,
|
||||
relay,
|
||||
withLockedVault
|
||||
);
|
||||
decryptedRelays.push(decryptedRelay);
|
||||
}
|
||||
|
||||
return decryptedRelays;
|
||||
};
|
||||
|
||||
export const encryptRelay = async function (
|
||||
this: StorageService,
|
||||
relay: Relay_DECRYPTED
|
||||
): Promise<Relay_ENCRYPTED> {
|
||||
const encryptedRelay: Relay_ENCRYPTED = {
|
||||
id: await this.encrypt(relay.id),
|
||||
identityId: await this.encrypt(relay.identityId),
|
||||
url: await this.encrypt(relay.url),
|
||||
read: await this.encrypt(relay.read.toString()),
|
||||
write: await this.encrypt(relay.write.toString()),
|
||||
};
|
||||
|
||||
return encryptedRelay;
|
||||
};
|
||||
128
projects/common/src/lib/services/storage/related/vault.ts
Normal file
128
projects/common/src/lib/services/storage/related/vault.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
BrowserSessionData,
|
||||
BrowserSyncData,
|
||||
CryptoHelper,
|
||||
StorageService,
|
||||
} from '@common';
|
||||
import { decryptIdentities } from './identity';
|
||||
import { decryptPermissions } from './permission';
|
||||
import { decryptRelays } from './relay';
|
||||
|
||||
export const createNewVault = async function (
|
||||
this: StorageService,
|
||||
password: string
|
||||
): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
const vaultHash = await CryptoHelper.hash(password);
|
||||
|
||||
const sessionData: BrowserSessionData = {
|
||||
iv: CryptoHelper.generateIV(),
|
||||
vaultPassword: password,
|
||||
identities: [],
|
||||
permissions: [],
|
||||
relays: [],
|
||||
selectedIdentityId: null,
|
||||
};
|
||||
await this.getBrowserSessionHandler().saveFullData(sessionData);
|
||||
this.getBrowserSessionHandler().setFullData(sessionData);
|
||||
|
||||
const syncData: BrowserSyncData = {
|
||||
version: this.latestVersion,
|
||||
iv: sessionData.iv,
|
||||
vaultHash,
|
||||
identities: [],
|
||||
permissions: [],
|
||||
relays: [],
|
||||
selectedIdentityId: null,
|
||||
};
|
||||
await this.getBrowserSyncHandler().saveAndSetFullData(syncData);
|
||||
};
|
||||
|
||||
export const unlockVault = async function (
|
||||
this: StorageService,
|
||||
password: string
|
||||
): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
let browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
||||
if (browserSessionData) {
|
||||
throw new Error(
|
||||
'Browser session data is available. Should only happen when the vault is unlocked'
|
||||
);
|
||||
}
|
||||
|
||||
const browserSyncData = this.getBrowserSyncHandler().browserSyncData;
|
||||
if (!browserSyncData) {
|
||||
throw new Error(
|
||||
'Browser sync data is not available. Should have been loaded before.'
|
||||
);
|
||||
}
|
||||
|
||||
const passwordHash = await CryptoHelper.hash(password);
|
||||
if (passwordHash !== browserSyncData.vaultHash) {
|
||||
throw new Error('Invalid password.');
|
||||
}
|
||||
|
||||
// Ok. Everything is fine. We can unlock the vault now.
|
||||
|
||||
// Decrypt the identities.
|
||||
const withLockedVault = {
|
||||
iv: browserSyncData.iv,
|
||||
password,
|
||||
};
|
||||
const decryptedIdentities = await decryptIdentities.call(
|
||||
this,
|
||||
browserSyncData.identities,
|
||||
withLockedVault
|
||||
);
|
||||
const decryptedPermissions = await decryptPermissions.call(
|
||||
this,
|
||||
browserSyncData.permissions,
|
||||
withLockedVault
|
||||
);
|
||||
const decryptedRelays = await decryptRelays.call(
|
||||
this,
|
||||
browserSyncData.relays,
|
||||
withLockedVault
|
||||
);
|
||||
const decryptedSelectedIdentityId =
|
||||
browserSyncData.selectedIdentityId === null
|
||||
? null
|
||||
: await this.decryptWithLockedVault(
|
||||
browserSyncData.selectedIdentityId,
|
||||
'string',
|
||||
browserSyncData.iv,
|
||||
password
|
||||
);
|
||||
|
||||
browserSessionData = {
|
||||
vaultPassword: password,
|
||||
iv: browserSyncData.iv,
|
||||
permissions: decryptedPermissions,
|
||||
identities: decryptedIdentities,
|
||||
selectedIdentityId: decryptedSelectedIdentityId,
|
||||
relays: decryptedRelays,
|
||||
};
|
||||
await this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
||||
this.getBrowserSessionHandler().setFullData(browserSessionData);
|
||||
};
|
||||
|
||||
export const deleteVault = async function (
|
||||
this: StorageService,
|
||||
doNotSetIsInitializedToFalse: boolean
|
||||
): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
const syncFlow = this.getGootiMetaHandler().gootiMetaData?.syncFlow;
|
||||
if (typeof syncFlow === 'undefined') {
|
||||
throw new Error('Sync flow is not set.');
|
||||
}
|
||||
|
||||
await this.getBrowserSyncHandler().clearData();
|
||||
await this.getBrowserSessionHandler().clearData();
|
||||
await this.getGootiMetaHandler().clearData();
|
||||
|
||||
if (!doNotSetIsInitializedToFalse) {
|
||||
this.isInitialized = false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
describe('StorageService', () => {
|
||||
let service: StorageService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(StorageService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
335
projects/common/src/lib/services/storage/storage.service.ts
Normal file
335
projects/common/src/lib/services/storage/storage.service.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BrowserSyncHandler } from './browser-sync-handler';
|
||||
import { BrowserSessionHandler } from './browser-session-handler';
|
||||
import {
|
||||
BrowserSessionData,
|
||||
BrowserSyncData,
|
||||
BrowserSyncFlow,
|
||||
GootiMetaData,
|
||||
Relay_DECRYPTED,
|
||||
} from './types';
|
||||
import { GootiMetaHandler } from './gooti-meta-handler';
|
||||
import { CryptoHelper } from '@common';
|
||||
import {
|
||||
addIdentity,
|
||||
deleteIdentity,
|
||||
switchIdentity,
|
||||
} from './related/identity';
|
||||
import { deletePermission } from './related/permission';
|
||||
import { createNewVault, deleteVault, unlockVault } from './related/vault';
|
||||
import { addRelay, deleteRelay, updateRelay } from './related/relay';
|
||||
|
||||
export interface StorageServiceConfig {
|
||||
browserSessionHandler: BrowserSessionHandler;
|
||||
browserSyncYesHandler: BrowserSyncHandler;
|
||||
browserSyncNoHandler: BrowserSyncHandler;
|
||||
gootiMetaHandler: GootiMetaHandler;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StorageService {
|
||||
readonly latestVersion = 1;
|
||||
isInitialized = false;
|
||||
|
||||
#browserSessionHandler!: BrowserSessionHandler;
|
||||
#browserSyncYesHandler!: BrowserSyncHandler;
|
||||
#browserSyncNoHandler!: BrowserSyncHandler;
|
||||
#gootiMetaHandler!: GootiMetaHandler;
|
||||
|
||||
initialize(config: StorageServiceConfig): void {
|
||||
if (this.isInitialized) {
|
||||
return;
|
||||
}
|
||||
this.#browserSessionHandler = config.browserSessionHandler;
|
||||
this.#browserSyncYesHandler = config.browserSyncYesHandler;
|
||||
this.#browserSyncNoHandler = config.browserSyncNoHandler;
|
||||
this.#gootiMetaHandler = config.gootiMetaHandler;
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
async enableBrowserSyncFlow(flow: BrowserSyncFlow): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
this.#gootiMetaHandler.setBrowserSyncFlow(flow);
|
||||
}
|
||||
|
||||
async loadGootiMetaData(): Promise<GootiMetaData | undefined> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
const data = await this.#gootiMetaHandler.loadFullData();
|
||||
if (Object.keys(data).length === 0) {
|
||||
// No data available yet.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.#gootiMetaHandler.setFullData(data as GootiMetaData);
|
||||
return data as GootiMetaData;
|
||||
}
|
||||
|
||||
async loadBrowserSessionData(): Promise<BrowserSessionData | undefined> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
const data = await this.#browserSessionHandler.loadFullData();
|
||||
if (Object.keys(data).length === 0) {
|
||||
// No data available yet (e.g. because the vault was not unlocked).
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Set the existing data for in-memory usage.
|
||||
this.#browserSessionHandler.setFullData(data as BrowserSessionData);
|
||||
return data as BrowserSessionData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and migrate the browser sync data. If no data is available yet,
|
||||
* the returned object is undefined.
|
||||
*/
|
||||
async loadAndMigrateBrowserSyncData(): Promise<BrowserSyncData | undefined> {
|
||||
this.assureIsInitialized();
|
||||
const unmigratedBrowserSyncData =
|
||||
await this.getBrowserSyncHandler().loadUnmigratedData();
|
||||
const { browserSyncData, migrationWasPerformed } =
|
||||
this.#migrateBrowserSyncData(unmigratedBrowserSyncData);
|
||||
|
||||
if (!browserSyncData) {
|
||||
// Nothing to do at this point.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// There is data. Check, if it was migrated.
|
||||
if (migrationWasPerformed) {
|
||||
// Persist the migrated data back to the browser sync storage.
|
||||
this.getBrowserSyncHandler().saveAndSetFullData(browserSyncData);
|
||||
} else {
|
||||
// Set the data for in-memory usage.
|
||||
this.getBrowserSyncHandler().setFullData(browserSyncData);
|
||||
}
|
||||
|
||||
return browserSyncData;
|
||||
}
|
||||
|
||||
async deleteVault(doNotSetIsInitializedToFalse = false) {
|
||||
await deleteVault.call(this, doNotSetIsInitializedToFalse);
|
||||
}
|
||||
|
||||
async unlockVault(password: string): Promise<void> {
|
||||
await unlockVault.call(this, password);
|
||||
}
|
||||
|
||||
async createNewVault(password: string): Promise<void> {
|
||||
await createNewVault.call(this, password);
|
||||
}
|
||||
|
||||
async addIdentity(data: {
|
||||
nick: string;
|
||||
privkeyString: string;
|
||||
}): Promise<void> {
|
||||
await addIdentity.call(this, data);
|
||||
}
|
||||
|
||||
async deleteIdentity(identityId: string | undefined): Promise<void> {
|
||||
await deleteIdentity.call(this, identityId);
|
||||
}
|
||||
|
||||
async switchIdentity(identityId: string | null): Promise<void> {
|
||||
await switchIdentity.call(this, identityId);
|
||||
}
|
||||
|
||||
async deletePermission(permissionId: string) {
|
||||
await deletePermission.call(this, permissionId);
|
||||
}
|
||||
|
||||
async addRelay(data: {
|
||||
identityId: string;
|
||||
url: string;
|
||||
write: boolean;
|
||||
read: boolean;
|
||||
}): Promise<void> {
|
||||
await addRelay.call(this, data);
|
||||
}
|
||||
|
||||
async deleteRelay(relayId: string): Promise<void> {
|
||||
await deleteRelay.call(this, relayId);
|
||||
}
|
||||
|
||||
async updateRelay(relayClone: Relay_DECRYPTED): Promise<void> {
|
||||
await updateRelay.call(this, relayClone);
|
||||
}
|
||||
|
||||
exportVault(): string {
|
||||
this.assureIsInitialized();
|
||||
const vaultJson = JSON.stringify(
|
||||
this.getBrowserSyncHandler().browserSyncData,
|
||||
undefined,
|
||||
4
|
||||
);
|
||||
return vaultJson;
|
||||
}
|
||||
|
||||
async importVault(allegedBrowserSyncData: BrowserSyncData) {
|
||||
this.assureIsInitialized();
|
||||
|
||||
const isValidData = this.#allegedBrowserSyncDataIsValid(
|
||||
allegedBrowserSyncData
|
||||
);
|
||||
if (!isValidData) {
|
||||
throw new Error('The imported data is not valid.');
|
||||
}
|
||||
|
||||
await this.getBrowserSyncHandler().saveAndSetFullData(
|
||||
allegedBrowserSyncData
|
||||
);
|
||||
}
|
||||
|
||||
getBrowserSyncHandler(): BrowserSyncHandler {
|
||||
this.assureIsInitialized();
|
||||
|
||||
switch (this.#gootiMetaHandler.gootiMetaData?.syncFlow) {
|
||||
case BrowserSyncFlow.NO_SYNC:
|
||||
return this.#browserSyncNoHandler;
|
||||
|
||||
case BrowserSyncFlow.BROWSER_SYNC:
|
||||
default:
|
||||
return this.#browserSyncYesHandler;
|
||||
}
|
||||
}
|
||||
|
||||
getBrowserSessionHandler(): BrowserSessionHandler {
|
||||
this.assureIsInitialized();
|
||||
|
||||
return this.#browserSessionHandler;
|
||||
}
|
||||
|
||||
getGootiMetaHandler(): GootiMetaHandler {
|
||||
this.assureIsInitialized();
|
||||
|
||||
return this.#gootiMetaHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception if the service is not initialized.
|
||||
*/
|
||||
assureIsInitialized(): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(
|
||||
'StorageService is not initialized. Please call "initialize(...)" before doing anything else.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async encrypt(value: string): Promise<string> {
|
||||
const browserSessionData =
|
||||
this.getBrowserSessionHandler().browserSessionData;
|
||||
if (!browserSessionData || !browserSessionData.vaultPassword) {
|
||||
throw new Error('Browser session data is undefined.');
|
||||
}
|
||||
|
||||
return CryptoHelper.encrypt(
|
||||
value,
|
||||
browserSessionData.iv,
|
||||
browserSessionData.vaultPassword
|
||||
);
|
||||
}
|
||||
|
||||
async decrypt(
|
||||
value: string,
|
||||
returnType: 'string' | 'number' | 'boolean'
|
||||
): Promise<any> {
|
||||
const browserSessionData =
|
||||
this.getBrowserSessionHandler().browserSessionData;
|
||||
if (!browserSessionData || !browserSessionData.vaultPassword) {
|
||||
throw new Error('Browser session data is undefined.');
|
||||
}
|
||||
|
||||
return this.decryptWithLockedVault(
|
||||
value,
|
||||
returnType,
|
||||
browserSessionData.iv,
|
||||
browserSessionData.vaultPassword
|
||||
);
|
||||
}
|
||||
|
||||
async decryptWithLockedVault(
|
||||
value: string,
|
||||
returnType: 'string' | 'number' | 'boolean',
|
||||
iv: string,
|
||||
password: string
|
||||
): Promise<any> {
|
||||
const decryptedValue = await CryptoHelper.decrypt(value, iv, password);
|
||||
|
||||
switch (returnType) {
|
||||
case 'number':
|
||||
return parseInt(decryptedValue);
|
||||
|
||||
case 'boolean':
|
||||
return decryptedValue === 'true';
|
||||
|
||||
case 'string':
|
||||
default:
|
||||
return decryptedValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate the browser sync data to the latest version.
|
||||
*/
|
||||
#migrateBrowserSyncData(browserSyncData: Partial<Record<string, any>>): {
|
||||
browserSyncData?: BrowserSyncData;
|
||||
migrationWasPerformed: boolean;
|
||||
} {
|
||||
if (Object.keys(browserSyncData).length === 0) {
|
||||
// First run. There is no browser sync data yet.
|
||||
return {
|
||||
browserSyncData: undefined,
|
||||
migrationWasPerformed: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Will be implemented if migration is required.
|
||||
return {
|
||||
browserSyncData: browserSyncData as BrowserSyncData,
|
||||
migrationWasPerformed: false,
|
||||
};
|
||||
}
|
||||
|
||||
#allegedBrowserSyncDataIsValid(data: BrowserSyncData): boolean {
|
||||
if (typeof data.iv === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof data.version !== 'number') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof data.vaultHash === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof data.selectedIdentityId === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof data.identities === 'undefined' ||
|
||||
!Array.isArray(data.identities)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof data.permissions === 'undefined' ||
|
||||
!Array.isArray(data.permissions)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof data.relays === 'undefined' || !Array.isArray(data.relays)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
84
projects/common/src/lib/services/storage/types.ts
Normal file
84
projects/common/src/lib/services/storage/types.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Nip07Method, Nip07MethodPolicy } from '@common';
|
||||
|
||||
export interface Permission_DECRYPTED {
|
||||
id: string;
|
||||
identityId: string;
|
||||
host: string;
|
||||
method: Nip07Method;
|
||||
methodPolicy: Nip07MethodPolicy;
|
||||
kind?: number;
|
||||
}
|
||||
|
||||
export interface Permission_ENCRYPTED {
|
||||
id: string;
|
||||
identityId: string;
|
||||
host: string;
|
||||
method: string;
|
||||
methodPolicy: string;
|
||||
kind?: string;
|
||||
}
|
||||
|
||||
export interface Identity_DECRYPTED {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
nick: string;
|
||||
privkey: string;
|
||||
}
|
||||
|
||||
export type Identity_ENCRYPTED = Identity_DECRYPTED;
|
||||
|
||||
export interface Relay_DECRYPTED {
|
||||
id: string;
|
||||
identityId: string;
|
||||
url: string;
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
}
|
||||
|
||||
export interface Relay_ENCRYPTED {
|
||||
id: string;
|
||||
identityId: string;
|
||||
url: string;
|
||||
read: string;
|
||||
write: string;
|
||||
}
|
||||
|
||||
export interface BrowserSyncData_PART_Unencrypted {
|
||||
version: number;
|
||||
iv: string;
|
||||
vaultHash: string;
|
||||
}
|
||||
|
||||
export interface BrowserSyncData_PART_Encrypted {
|
||||
selectedIdentityId: string | null;
|
||||
permissions: Permission_ENCRYPTED[];
|
||||
identities: Identity_ENCRYPTED[];
|
||||
relays: Relay_ENCRYPTED[];
|
||||
}
|
||||
|
||||
export type BrowserSyncData = BrowserSyncData_PART_Unencrypted &
|
||||
BrowserSyncData_PART_Encrypted;
|
||||
|
||||
export enum BrowserSyncFlow {
|
||||
NO_SYNC = 0,
|
||||
BROWSER_SYNC = 1,
|
||||
GOOTI_SYNC = 2,
|
||||
CUSTOM_SYNC = 3,
|
||||
}
|
||||
|
||||
export interface BrowserSessionData {
|
||||
// The following properties purely come from the browser session storage
|
||||
// and will never be going into the browser sync storage.
|
||||
vaultPassword?: string;
|
||||
|
||||
// The following properties initially come from the browser sync storage.
|
||||
iv: string;
|
||||
permissions: Permission_DECRYPTED[];
|
||||
identities: Identity_DECRYPTED[];
|
||||
selectedIdentityId: string | null;
|
||||
relays: Relay_DECRYPTED[];
|
||||
}
|
||||
|
||||
export interface GootiMetaData {
|
||||
syncFlow?: number; // 0 = no sync, 1 = browser sync, (future: 2 = Gooti sync, 3 = Custom sync (bring your own sync))
|
||||
}
|
||||
7
projects/common/src/lib/styles/_color.scss
Normal file
7
projects/common/src/lib/styles/_color.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.sam-color-primary {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.sam-color-danger {
|
||||
color: var(--bs-danger);
|
||||
}
|
||||
37
projects/common/src/lib/styles/_common.scss
Normal file
37
projects/common/src/lib/styles/_common.scss
Normal file
@@ -0,0 +1,37 @@
|
||||
.sam-text-header {
|
||||
background: var(--background);
|
||||
z-index: 20;
|
||||
padding-top: var(--size);
|
||||
padding-bottom: var(--size);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.sam-footer-grid-2 {
|
||||
height: 60px;
|
||||
min-height: 60px;
|
||||
background: var(--background-light);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto;
|
||||
align-items: center;
|
||||
column-gap: var(--size);
|
||||
padding-left: var(--size);
|
||||
padding-right: var(--size);
|
||||
}
|
||||
|
||||
.sam-card {
|
||||
padding: var(--size);
|
||||
background: var(--background-light);
|
||||
border-radius: 8px;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
38
projects/common/src/lib/styles/_flex.scss
Normal file
38
projects/common/src/lib/styles/_flex.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
.sam-flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.gap {
|
||||
row-gap: var(--size);
|
||||
}
|
||||
|
||||
&.gap-h {
|
||||
row-gap: var(--size-h);
|
||||
}
|
||||
|
||||
&.center {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.sam-flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
&.gap {
|
||||
column-gap: var(--size);
|
||||
}
|
||||
|
||||
&.gap-h {
|
||||
column-gap: var(--size-h);
|
||||
}
|
||||
}
|
||||
|
||||
.sam-flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.sam-align-self-center {
|
||||
align-self: center;
|
||||
}
|
||||
0
projects/common/src/lib/styles/_font.scss
Normal file
0
projects/common/src/lib/styles/_font.scss
Normal file
55
projects/common/src/lib/styles/_spacing.scss
Normal file
55
projects/common/src/lib/styles/_spacing.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
.sam-mt {
|
||||
margin-top: var(--size);
|
||||
}
|
||||
|
||||
.sam-mt-2 {
|
||||
margin-top: var(--size-2);
|
||||
}
|
||||
|
||||
.sam-mt-h {
|
||||
margin-top: var(--size-h);
|
||||
}
|
||||
|
||||
.sam-mb {
|
||||
margin-bottom: var(--size);
|
||||
}
|
||||
|
||||
.sam-mb-2 {
|
||||
margin-bottom: var(--size-2);
|
||||
}
|
||||
|
||||
.sam-mb-h {
|
||||
margin-bottom: var(--size-h);
|
||||
}
|
||||
|
||||
.sam-mr {
|
||||
margin-right: var(--size);
|
||||
}
|
||||
|
||||
.sam-mr-h {
|
||||
margin-right: var(--size-h);
|
||||
}
|
||||
|
||||
.sam-ml {
|
||||
margin-left: var(--size);
|
||||
}
|
||||
|
||||
.sam-ml-h {
|
||||
margin-left: var(--size-h);
|
||||
}
|
||||
|
||||
.sam-pl {
|
||||
padding-left: var(--size);
|
||||
}
|
||||
|
||||
.sam-pl-h {
|
||||
padding-left: var(--size-h);
|
||||
}
|
||||
|
||||
.sam-pr {
|
||||
padding-right: var(--size);
|
||||
}
|
||||
|
||||
.sam-pr-h {
|
||||
padding-right: var(--size-h);
|
||||
}
|
||||
19
projects/common/src/lib/styles/_typography.scss
Normal file
19
projects/common/src/lib/styles/_typography.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
.sam-text-muted {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.sam-text-lg {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.sam-text-md {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sam-text-sm {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sam-text-align-center {
|
||||
text-align: center;
|
||||
}
|
||||
17
projects/common/src/lib/styles/styles.scss
Normal file
17
projects/common/src/lib/styles/styles.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
@use "_flex.scss";
|
||||
@use "_common.scss";
|
||||
@use "_typography.scss";
|
||||
@use "_spacing.scss";
|
||||
@use "_color.scss";
|
||||
|
||||
:root {
|
||||
--size-2: 32px;
|
||||
--size: 16px;
|
||||
--size-h: 8px;
|
||||
|
||||
--background: #161c26;
|
||||
--background-light: #202733;
|
||||
--background-light-hover: #383844;
|
||||
--border: #525b6a;
|
||||
--primary: #0d6efd;
|
||||
}
|
||||
Reference in New Issue
Block a user