diff --git a/app/src/app/admin/admin.css b/app/src/app/admin/admin.css
new file mode 100644
index 0000000..2392e7e
--- /dev/null
+++ b/app/src/app/admin/admin.css
@@ -0,0 +1,87 @@
+.example-action-buttons {
+ padding-bottom: 20px;
+}
+
+.example-headers-align .mat-expansion-panel-header-description {
+ justify-content: space-between;
+ align-items: center;
+}
+
+.example-headers-align .mat-mdc-form-field + .mat-mdc-form-field {
+ margin-left: 8px;
+}
+
+.online {
+ margin-left: 0.2em;
+ margin-bottom: -0.2em;
+}
+
+.relay-status-0 {
+ color: silver;
+}
+
+.relay-status-1 {
+ color: green;
+}
+
+.relay-status-2 {
+ color: orange;
+}
+
+.relay-status-3 {
+ color: red;
+}
+
+.relay-status-4 {
+ color: rgb(49, 49, 210);
+}
+
+.relay-read-disabled {
+ color: rgb(49, 49, 210) !important;
+}
+
+.relay-disabled {
+ color: rgb(234, 136, 9) !important;
+}
+
+.primary-relay {
+ color: rgb(198, 3, 181);
+}
+
+.relay-options {
+ margin-top: 0.4em;
+ margin-bottom: 0.2em;
+}
+
+.settings-action-buttons {
+ padding-top: 0.8em;
+ padding-bottom: 1em;
+}
+
+.settings-action-buttons button {
+ margin-bottom: 1em;
+ margin-right: 1em;
+}
+
+/* When changing the sidenav-content to flex, the toolbar does not render properly, so a minor hack is needed. */
+@media only screen and (max-width: 599px) {
+ .settings-action-buttons button {
+ width: 100%;
+ margin-right: 0;
+ }
+
+ .mat-expansion-panel-header-title {
+ flex-grow: 2 !important;
+ }
+
+ .mat-expansion-panel-header-description {
+ flex-grow: 1 !important;
+ }
+}
+
+.relay-button {
+ margin-top: 0.8em;
+}
+.options-slider {
+ margin-left: 1em;
+}
diff --git a/app/src/app/admin/admin.html b/app/src/app/admin/admin.html
new file mode 100644
index 0000000..fc7c11b
--- /dev/null
+++ b/app/src/app/admin/admin.html
@@ -0,0 +1,178 @@
+
+
+
+
+
+ manage_accounts
+ {{'Admin.Users' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ settings_applications
+ {{ 'App.Settings' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/app/admin/admin.ts b/app/src/app/admin/admin.ts
new file mode 100644
index 0000000..c43e01d
--- /dev/null
+++ b/app/src/app/admin/admin.ts
@@ -0,0 +1,270 @@
+import { Component, ViewChild } from '@angular/core';
+import { MatDialog } from '@angular/material/dialog';
+import { MatAccordion } from '@angular/material/expansion';
+import { nip19, Relay } from 'nostr-tools';
+import { ApplicationState } from '../services/applicationstate';
+import { StorageService } from '../services/storage';
+import { EventService } from '../services/event';
+import { NostrRelay } from '../services/interfaces';
+import { ProfileService } from '../services/profile';
+import { RelayService } from '../services/relay';
+import { ThemeService } from '../services/theme';
+import { AddRelayDialog, AddRelayDialogData } from '../shared/add-relay-dialog/add-relay-dialog';
+import { OptionsService } from '../services/options';
+import { MatSnackBar } from '@angular/material/snack-bar';
+import { DataService } from '../services/data';
+import { NostrService } from '../services/nostr';
+import { UploadService } from '../services/upload';
+import { PasswordDialog, PasswordDialogData } from '../shared/password-dialog/password-dialog';
+import { SecurityService } from '../services/security';
+import * as QRCode from 'qrcode';
+import { TranslateService } from '@ngx-translate/core';
+
+@Component({
+ selector: 'app-admin',
+ templateUrl: './admin.html',
+ styleUrls: ['./admin.css'],
+})
+export class AdminComponent {
+ @ViewChild(MatAccordion) accordion!: MatAccordion;
+
+ wiped = false;
+ wipedNonFollow = false;
+ wipedNotes = false;
+ open = false;
+
+ constructor(
+ public uploadService: UploadService,
+ private nostr: NostrService,
+ public optionsService: OptionsService,
+ public relayService: RelayService,
+ public dialog: MatDialog,
+ public appState: ApplicationState,
+ private profileService: ProfileService,
+ public theme: ThemeService,
+ public db: StorageService,
+ private snackBar: MatSnackBar,
+ public dataService: DataService,
+ private security: SecurityService,
+ public translate: TranslateService
+ ) {}
+
+ toggle() {
+ if (this.open) {
+ this.open = false;
+ this.accordion.closeAll();
+ } else {
+ this.open = true;
+ this.accordion.openAll();
+ }
+ }
+
+ openMediaPlayer() {
+ this.optionsService.values.showMediaPlayer = true;
+ }
+
+ async primaryRelay(relay: NostrRelay) {
+ this.optionsService.values.primaryRelay = relay.url;
+ this.optionsService.save();
+ }
+
+ // async deleteRelay(relay: Relay) {
+ // await this.relayService.deleteRelay(relay.url);
+ // }
+
+ async deleteRelays() {
+ await this.relayService.deleteRelays([]);
+ }
+
+ async clearProfileCache() {
+ // await this.profileService.wipeNonFollow();
+ this.wipedNonFollow = true;
+ }
+
+ // async onRelayChanged(relay: NostrRelay) {
+ // if (relay.metadata.enabled && relay.metadata.read) {
+ // await relay.connect();
+ // } else if (!relay.metadata.read) {
+ // await relay.close();
+ // } else {
+ // await relay.close();
+ // }
+
+ // await this.relayService.putRelayMetadata(relay.metadata);
+ // }
+
+ async clearNotesCache() {
+ // await this.feedService.wipe();
+ this.wipedNotes = true;
+ }
+
+ async getDefaultRelays() {
+ // Append the default relays.
+ await this.relayService.appendRelays(this.nostr.defaultRelays);
+ }
+
+ // private getPublicPublicKeys() {
+ // console.log(this.profileService.following);
+ // const items: string[] = [];
+
+ // for (let i = 0; i < this.circleService.circles.length; i++) {
+ // const circle = this.circleService.circles[i];
+
+ // if (circle.public) {
+ // const profiles = this.getFollowingInCircle(circle.id);
+ // const pubkeys = profiles.map((p) => p.pubkey);
+ // items.push(...pubkeys);
+ // }
+ // }
+
+ // return items;
+ // }
+
+ async getRelays() {
+ const relays = await this.nostr.relays();
+
+ // Append the default relays.
+ await this.relayService.appendRelays(relays);
+ }
+
+ ngOnInit() {
+ this.appState.updateTitle('Settings');
+ this.appState.showBackButton = false;
+ this.appState.actions = [
+ {
+ icon: 'add_circle',
+ tooltip: 'Add Relay',
+ click: () => {
+ this.addRelay();
+ },
+ },
+ ];
+
+ this.hasPrivateKey = localStorage.getItem('blockcore:notes:nostr:prvkey') != null;
+ }
+
+ registerHandler(protocol: string, parameter: string) {
+ // navigator.registerProtocolHandler(protocol, `./index.html?${parameter}=%s`);
+ navigator.registerProtocolHandler(protocol, `/?${parameter}=%s`);
+ }
+
+ addRelay(): void {
+ const dialogRef = this.dialog.open(AddRelayDialog, {
+ data: { read: true, write: true },
+ maxWidth: '100vw',
+ panelClass: 'full-width-dialog',
+ });
+
+ dialogRef.afterClosed().subscribe(async (result: AddRelayDialogData) => {
+ if (!result) {
+ return;
+ }
+
+ // Append the Web Socket prefix if missing.
+ if (result.url.indexOf('://') === -1) {
+ result.url = 'wss://' + result.url;
+ }
+
+ await this.relayService.appendRelay(result.url, result.read, result.write);
+ });
+ }
+
+ hasPrivateKey = false;
+ verifiedWalletPassword?: boolean;
+ privateKey?: string;
+ qrCodePrivateKey?: string;
+
+ resetPrivateKey() {
+ this.privateKey = undefined;
+ this.qrCodePrivateKey = undefined;
+ this.verifiedWalletPassword = undefined;
+ }
+
+ ngOnDestroy() {
+ this.resetPrivateKey();
+ }
+
+ onLanguageChanged(event: any) {
+ this.appState.setLanguage(this.optionsService.values.language);
+
+ const rtlLanguages: string[] = ['ar', 'fa', 'he'];
+
+ if (rtlLanguages.includes(this.optionsService.values.language)) {
+ this.appState.documentDirection = 'rtl';
+ this.optionsService.values.dir = 'rtl';
+ } else {
+ this.appState.documentDirection = 'ltr';
+ this.optionsService.values.dir = 'ltr';
+ }
+
+ this.optionsService.save();
+ }
+
+ async exportPrivateKey() {
+ const dialogRef = this.dialog.open(PasswordDialog, {
+ data: { action: 'Unlock Private Key', password: '' },
+ maxWidth: '100vw',
+ panelClass: 'full-width-dialog',
+ });
+
+ dialogRef.afterClosed().subscribe(async (result: PasswordDialogData) => {
+ if (!result) {
+ return;
+ }
+
+ let prvkeyEncrypted = localStorage.getItem('blockcore:notes:nostr:prvkey');
+
+ const prvkey = await this.security.decryptData(prvkeyEncrypted!, result.password);
+
+ if (!prvkey) {
+ this.verifiedWalletPassword = false;
+
+ this.snackBar.open(`Unable to decrypt data. Probably wrong password. Try again.`, 'Hide', {
+ duration: 3000,
+ horizontalPosition: 'center',
+ verticalPosition: 'bottom',
+ });
+ return;
+ }
+
+ this.verifiedWalletPassword = true;
+
+ const privateKey = nip19.nsecEncode(prvkey);
+ this.privateKey = privateKey;
+
+ this.qrCodePrivateKey = await QRCode.toDataURL('nostr:' + this.privateKey, {
+ errorCorrectionLevel: 'L',
+ margin: 2,
+ scale: 5,
+ });
+ });
+
+ // this.verifiedWalletPassword = null;
+ // this.privateKey = null;
+ // const dialogRef = this.dialog.open(PasswordDialog, {
+ // data: { password: null },
+ // });
+
+ // dialogRef.afterClosed().subscribe(async (result) => {
+ // if (result === null || result === undefined || result === '') {
+ // return;
+ // }
+
+ // this.verifiedWalletPassword = await this.walletManager.verifyWalletPassword(this.walletManager.activeWalletId, result);
+
+ // if (this.verifiedWalletPassword === true) {
+ // const network = this.identityService.getNetwork(this.walletManager.activeAccount.networkType);
+ // const identityNode = this.identityService.getIdentityNode(this.walletManager.activeWallet, this.walletManager.activeAccount);
+
+ // this.privateKey = this.cryptoUtility.convertToBech32(identityNode.privateKey, 'nsec');
+ // console.log(secp.utils.bytesToHex(identityNode.privateKey));
+ // //this.privateKey = secp.utils.bytesToHex(identityNode.privateKey);
+
+ // this.qrCodePrivateKey = await QRCode.toDataURL('nostr:' + this.privateKey, {
+ // errorCorrectionLevel: 'L',
+ // margin: 2,
+ // scale: 5,
+ // });
+ // }
+ }
+}
diff --git a/app/src/app/app-routing.module.ts b/app/src/app/app-routing.module.ts
index bef0fd4..22945ad 100644
--- a/app/src/app/app-routing.module.ts
+++ b/app/src/app/app-routing.module.ts
@@ -6,7 +6,7 @@ import { HomeComponent } from './home/home';
import { LogoutComponent } from './logout/logout';
import { NotesComponent } from './notes/notes';
import { ProfileComponent } from './profile/profile';
-import { AuthGuardService as AuthGuard } from './services/auth-guard';
+import { AuthGuardService as AuthGuard, AuthGuardAdminService as AdminGuard } from './services/auth-guard';
import { SettingsComponent } from './settings/settings';
import { UserComponent } from './user/user';
import { CirclesComponent } from './circles/circles';
@@ -37,6 +37,7 @@ import { ExampleComponent } from './example/example';
import { ProjectsComponent } from './projects/projects.component';
import { FilesComponent } from './files/files';
import { ProjectComponent } from './projects/project/project.component';
+import { AdminComponent } from './admin/admin';
const routes: Routes = [
{
@@ -167,6 +168,14 @@ const routes: Routes = [
data: LoadingResolverService,
},
},
+ {
+ path: 'admin',
+ component: AdminComponent,
+ canActivate: [AdminGuard],
+ resolve: {
+ data: LoadingResolverService,
+ },
+ },
{
path: 'badges/:id',
component: BadgesComponent,
diff --git a/app/src/app/app.html b/app/src/app/app.html
index 6090d8d..ff25843 100644
--- a/app/src/app/app.html
+++ b/app/src/app/app.html
@@ -117,11 +117,19 @@
search
-