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 }}
+
+
+
+
+
+
+
+
+
+
+
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);
}