diff --git a/backend/src/collections/syspreferencesdb/syspreferencedefaults.service.ts b/backend/src/collections/syspreferencesdb/syspreferencedefaults.service.ts index 231401b..6811c12 100644 --- a/backend/src/collections/syspreferencesdb/syspreferencedefaults.service.ts +++ b/backend/src/collections/syspreferencesdb/syspreferencedefaults.service.ts @@ -27,7 +27,6 @@ export class SysPreferenceDefaultsService { } }, jwt_expires_in: () => this.jwtConfigService.getJwtExpiresIn() ?? '7d', - upload_require_auth: () => true, test_string: () => 'test_string', test_number: () => 123, diff --git a/frontend/src/app/routes/settings/settings-sidebar/settings-sidebar.component.ts b/frontend/src/app/routes/settings/settings-sidebar/settings-sidebar.component.ts index c297df5..c6915c6 100644 --- a/frontend/src/app/routes/settings/settings-sidebar/settings-sidebar.component.ts +++ b/frontend/src/app/routes/settings/settings-sidebar/settings-sidebar.component.ts @@ -1,5 +1,6 @@ import { Component, Inject, OnInit } from '@angular/core'; import { Router } from '@angular/router'; +import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; import { PRoutes } from 'src/app/models/picsur-routes'; import { PermissionService } from 'src/app/services/api/permission.service'; @@ -23,7 +24,7 @@ export class SettingsSidebarComponent implements OnInit { this.subscribePermissions(); } - // @AutoUnsubscribe() + @AutoUnsubscribe() private subscribePermissions() { return this.permissionService.live.subscribe((permissions) => { this.accessibleRoutes = this.settingsRoutes diff --git a/frontend/src/app/routes/settings/settings-syspref/settings-syspref-option/settings-syspref-option.component.html b/frontend/src/app/routes/settings/settings-syspref/settings-syspref-option/settings-syspref-option.component.html new file mode 100644 index 0000000..8c48d1a --- /dev/null +++ b/frontend/src/app/routes/settings/settings-syspref/settings-syspref-option/settings-syspref-option.component.html @@ -0,0 +1,47 @@ + +
+
+

{{ name }}

+
+
+ + + +
+
+
+ +
+
+

{{ name }}

+
+
+ + + +
+
+
+ +
+
+

{{ name }}

+
+
+ +
+
+
diff --git a/frontend/src/app/routes/settings/settings-syspref/settings-syspref-option/settings-syspref-option.component.scss b/frontend/src/app/routes/settings/settings-syspref/settings-syspref-option/settings-syspref-option.component.scss new file mode 100644 index 0000000..1b54109 --- /dev/null +++ b/frontend/src/app/routes/settings/settings-syspref/settings-syspref-option/settings-syspref-option.component.scss @@ -0,0 +1,9 @@ +mat-form-field { + min-width: 50%; +} + +.y-center { + justify-content: center; + display: flex; + flex-direction: column; +} diff --git a/frontend/src/app/routes/settings/settings-syspref/settings-syspref-option/settings-syspref-option.component.ts b/frontend/src/app/routes/settings/settings-syspref/settings-syspref-option/settings-syspref-option.component.ts new file mode 100644 index 0000000..77bd117 --- /dev/null +++ b/frontend/src/app/routes/settings/settings-syspref/settings-syspref-option/settings-syspref-option.component.ts @@ -0,0 +1,92 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; +import { SysPreferenceResponse } from 'picsur-shared/dist/dto/api/pref.dto'; +import { + SysPreferenceFriendlyNames, + SysPrefValueType +} from 'picsur-shared/dist/dto/syspreferences.dto'; +import { HasFailed } from 'picsur-shared/dist/types'; +import { debounceTime, Subject } from 'rxjs'; +import { SnackBarType } from 'src/app/models/snack-bar-type'; +import { SysprefService } from 'src/app/services/api/syspref.service'; +import { UtilService } from 'src/app/util/util.service'; + +@Component({ + selector: 'syspref-option', + templateUrl: './settings-syspref-option.component.html', + styleUrls: ['./settings-syspref-option.component.scss'], +}) +export class SettingsSysprefOptionComponent implements OnInit { + @Input() pref: SysPreferenceResponse; + + private updateSubject = new Subject(); + + constructor( + private sysprefService: SysprefService, + private utilService: UtilService + ) {} + + ngOnInit(): void { + this.subscribeUpdate(); + } + + get name(): string { + return SysPreferenceFriendlyNames[this.pref.key]; + } + + get valString(): string { + if (this.pref.type !== 'string') { + throw new Error('Not a string preference'); + } + return this.pref.value as string; + } + + get valNumber(): number { + if (this.pref.type !== 'number') { + throw new Error('Not an int preference'); + } + return this.pref.value as number; + } + + get valBool(): boolean { + if (this.pref.type !== 'boolean') { + throw new Error('Not a boolean preference'); + } + return this.pref.value as boolean; + } + + update(value: any) { + this.updateSubject.next(value); + } + + stringUpdateWrapper(e: Event) { + this.update((e.target as HTMLInputElement).value); + } + + numberUpdateWrapper(e: Event) { + this.update((e.target as HTMLInputElement).valueAsNumber); + } + + @AutoUnsubscribe() + subscribeUpdate() { + return this.updateSubject + .pipe(debounceTime(300)) + .subscribe(async (value) => { + const result = await this.sysprefService.setPreference( + this.pref.key, + value + ); + if (!HasFailed(result)) { + this.utilService.showSnackBar( + `Updated ${this.name}`, + SnackBarType.Success + ); + } else { + this.utilService.showSnackBar( + `Failed to update ${this.name}`, + SnackBarType.Error + ); + } + }); + } +} diff --git a/frontend/src/app/routes/settings/settings-syspref/settings-syspref.component.html b/frontend/src/app/routes/settings/settings-syspref/settings-syspref.component.html index 86c5a80..a11a6e0 100644 --- a/frontend/src/app/routes/settings/settings-syspref/settings-syspref.component.html +++ b/frontend/src/app/routes/settings/settings-syspref/settings-syspref.component.html @@ -1,2 +1,7 @@

Settings Syspref

+ + + + + diff --git a/frontend/src/app/routes/settings/settings-syspref/settings-syspref.component.ts b/frontend/src/app/routes/settings/settings-syspref/settings-syspref.component.ts index 2c18349..63555e8 100644 --- a/frontend/src/app/routes/settings/settings-syspref/settings-syspref.component.ts +++ b/frontend/src/app/routes/settings/settings-syspref/settings-syspref.component.ts @@ -1,10 +1,59 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; +import { SysPreferenceResponse } from 'picsur-shared/dist/dto/api/pref.dto'; +import { SysprefService as SysPrefService } from 'src/app/services/api/syspref.service'; @Component({ templateUrl: './settings-syspref.component.html', }) -export class SettingsSysprefComponent implements OnInit { - constructor() {} +export class SettingsSysprefComponent implements OnInit, OnChanges { + render = true; + preferences: SysPreferenceResponse[] = []; - ngOnInit(): void {} + constructor(private sysprefService: SysPrefService) {} + + async ngOnInit() { + this.subscribePreferences(); + await this.sysprefService.getPreferences(); + } + + @AutoUnsubscribe() + private subscribePreferences() { + return this.sysprefService.live.subscribe((preferences) => { + // If the preferences are the same, something probably went wrong, so reset + if (this.compareFlatObjectArray(this.preferences, preferences)) { + this.render = false; + setTimeout(() => { + this.render = true; + }); + } + + this.preferences = preferences; + }); + } + + ngOnChanges(changes: SimpleChanges): void { + console.log('cahnges', changes); + } + + private compareFlatObjectArray(a: any[], b: any[]): boolean { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!this.compareFlatObject(a[i], b[i])) { + return false; + } + } + return true; + } + + private compareFlatObject(a: any, b: any): boolean { + for (const key in a) { + if (a.hasOwnProperty(key) && a[key] !== b[key]) { + return false; + } + } + return true; + } } diff --git a/frontend/src/app/routes/settings/settings-syspref/settings-syspref.module.ts b/frontend/src/app/routes/settings/settings-syspref/settings-syspref.module.ts index d58e916..a74f901 100644 --- a/frontend/src/app/routes/settings/settings-syspref/settings-syspref.module.ts +++ b/frontend/src/app/routes/settings/settings-syspref/settings-syspref.module.ts @@ -1,13 +1,20 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatInputModule } from '@angular/material/input'; +import { MatListModule } from '@angular/material/list'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { SettingsSysprefOptionComponent } from './settings-syspref-option/settings-syspref-option.component'; import { SettingsSysprefComponent } from './settings-syspref.component'; import { SettingsSysprefRoutingModule } from './settings-syspref.routing.module'; @NgModule({ - declarations: [SettingsSysprefComponent], + declarations: [SettingsSysprefComponent, SettingsSysprefOptionComponent], imports: [ CommonModule, SettingsSysprefRoutingModule, + MatListModule, + MatSlideToggleModule, + MatInputModule, ], }) export class SettingsSysprefRouteModule {} diff --git a/frontend/src/app/services/api/permission.service.ts b/frontend/src/app/services/api/permission.service.ts index d3d3cd9..42f6d97 100644 --- a/frontend/src/app/services/api/permission.service.ts +++ b/frontend/src/app/services/api/permission.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Optional, SkipSelf } from '@angular/core'; +import { Injectable } from '@angular/core'; import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; import { UserMePermissionsResponse } from 'picsur-shared/dist/dto/api/user.dto'; import { @@ -14,11 +14,7 @@ import { UserService } from './user.service'; export class PermissionService { private readonly logger = console; - constructor( - private userService: UserService, - private api: ApiService, - @Optional() @SkipSelf() parent?: PermissionService - ) { + constructor(private userService: UserService, private api: ApiService) { this.onUser(); } diff --git a/frontend/src/app/services/api/syspref.service.ts b/frontend/src/app/services/api/syspref.service.ts new file mode 100644 index 0000000..6fbec89 --- /dev/null +++ b/frontend/src/app/services/api/syspref.service.ts @@ -0,0 +1,133 @@ +import { Injectable } from '@angular/core'; +import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; +import { + MultipleSysPreferencesResponse, + SysPreferenceResponse, + UpdateSysPreferenceRequest +} from 'picsur-shared/dist/dto/api/pref.dto'; +import { Permission } from 'picsur-shared/dist/dto/permissions'; +import { + SysPreferences, + SysPrefValueType +} from 'picsur-shared/dist/dto/syspreferences.dto'; +import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types'; +import { BehaviorSubject } from 'rxjs'; +import { ApiService } from './api.service'; +import { PermissionService } from './permission.service'; + +@Injectable({ + providedIn: 'root', +}) +export class SysprefService { + private hasPermission = false; + + public get snapshot() { + return this.sysprefObservable.getValue(); + } + + public get live() { + return this.sysprefObservable; + } + + private sysprefObservable = new BehaviorSubject([]); + + constructor( + private api: ApiService, + private permissionsService: PermissionService + ) { + this.onPermissions(); + } + + public async getPreferences(): AsyncFailable { + if (!this.hasPermission) + return Fail('You do not have permission to edit system preferences'); + + const response = await this.api.get( + MultipleSysPreferencesResponse, + '/api/pref/sys' + ); + if (HasFailed(response)) { + this.sync(); + return response; + } + + this.sysprefObservable.next(response.preferences); + return response.preferences; + } + + public async getPreference( + key: SysPreferences + ): AsyncFailable { + if (!this.hasPermission) + return Fail('You do not have permission to edit system preferences'); + + const response = await this.api.get( + SysPreferenceResponse, + `/api/pref/sys/${key}` + ); + if (HasFailed(response)) { + this.sync(); + return response; + } + + this.updatePrefArray(response); + return response; + } + + public async setPreference( + key: SysPreferences, + value: SysPrefValueType + ): AsyncFailable { + if (!this.hasPermission) + return Fail('You do not have permission to edit system preferences'); + + const response = await this.api.post( + UpdateSysPreferenceRequest, + SysPreferenceResponse, + `/api/pref/sys/${key}`, + { value } + ); + if (HasFailed(response)) { + this.sync(); + return response; + } + + this.updatePrefArray(response); + return response; + } + + private updatePrefArray(pref: SysPreferenceResponse) { + const prefArray = this.snapshot; + // Replace the old pref with the new one + const index = prefArray.findIndex((i) => pref.key === i.key); + if (index === -1) { + const newArray = [...prefArray, pref]; + this.sysprefObservable.next(newArray); + } else { + const newArray = [...prefArray]; + newArray[index] = pref; + this.sysprefObservable.next(newArray); + } + } + + private sync() { + console.warn('System preferences have been flushed'); + this.sysprefObservable.next( + ([] as SysPreferenceResponse[]).concat(this.snapshot) + ); + } + + private flush() { + this.sysprefObservable.next([]); + } + + @AutoUnsubscribe() + private onPermissions() { + return this.permissionsService.live.subscribe((permissions) => { + this.hasPermission = permissions.includes(Permission.SysPrefManage); + if (!this.hasPermission) { + this.flush(); + } + }); + } +} diff --git a/frontend/src/scss/fixes.scss b/frontend/src/scss/fixes.scss index 6b13c7a..738ca7b 100644 --- a/frontend/src/scss/fixes.scss +++ b/frontend/src/scss/fixes.scss @@ -12,7 +12,9 @@ app-root { html { box-sizing: border-box; + color-scheme: dark; } + *, *:before, *:after { @@ -44,3 +46,7 @@ form mat-form-field { width: inherit; max-width: 40rem; } + +input::placeholder { + color: white; +} diff --git a/shared/src/dto/api/pref.dto.ts b/shared/src/dto/api/pref.dto.ts index 0d6299c..75d657a 100644 --- a/shared/src/dto/api/pref.dto.ts +++ b/shared/src/dto/api/pref.dto.ts @@ -14,7 +14,8 @@ import { export class UpdateSysPreferenceRequest { @IsNotEmpty() - value: string; + @IsSysPrefValue() + value: SysPrefValueType; } export class SysPreferenceResponse { diff --git a/shared/src/dto/syspreferences.dto.ts b/shared/src/dto/syspreferences.dto.ts index cb66106..9f83f4f 100644 --- a/shared/src/dto/syspreferences.dto.ts +++ b/shared/src/dto/syspreferences.dto.ts @@ -10,7 +10,6 @@ import tuple from '../types/tuple'; const SysPreferencesTuple = tuple( 'jwt_secret', 'jwt_expires_in', - 'upload_require_auth', 'test_string', 'test_number', 'test_boolean', @@ -19,6 +18,16 @@ const SysPreferencesTuple = tuple( export const SysPreferences: string[] = SysPreferencesTuple; export type SysPreferences = typeof SysPreferencesTuple[number]; +export const SysPreferenceFriendlyNames: { + [key in SysPreferences]: string; +} = { + jwt_secret: 'JWT Secret', + jwt_expires_in: 'JWT Expiry Time', + test_string: 'Test String', + test_number: 'Test Number', + test_boolean: 'Test Boolean', +}; + // Syspref Values export type SysPrefValueType = string | number | boolean; @@ -30,7 +39,6 @@ export const SysPreferenceValueTypes: { } = { jwt_secret: 'string', jwt_expires_in: 'string', - upload_require_auth: 'boolean', test_string: 'string', test_number: 'number', test_boolean: 'boolean', diff --git a/shared/src/types/failable.ts b/shared/src/types/failable.ts index 1dcb8da..43291ce 100644 --- a/shared/src/types/failable.ts +++ b/shared/src/types/failable.ts @@ -17,9 +17,11 @@ export type AsyncFailable = Promise>; // TODO: prevent promise from being allowed in these 2 functions export function HasFailed(failable: Failable): failable is Failure { + if (failable instanceof Promise) throw new Error('Invalid use of HasFailed'); return failable instanceof Failure; } export function HasSuccess(failable: Failable): failable is T { + if (failable instanceof Promise) throw new Error('Invalid use of HasSuccess'); return !(failable instanceof Failure); }