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 - - - @@ -234,15 +242,15 @@ {{ 'App.Circles' | translate }} - folder - {{ 'App.Files' | translate }} - + [routerLink]="['/files']" + mat-menu-item + (click)="toggleMenu()" + [routerLinkActiveOptions]="{ exact: true }" + routerLinkActive="active-nav-link" + > + folder + {{ 'App.Files' | translate }} + bookmarks {{ 'App.Bookmarks' | translate }} + + manage_accounts + {{ 'App.Admin' | translate }} + +