first chrome implementation
This commit is contained in:
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'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user