Add client side preference verification
Add support for hostname override
This commit is contained in:
parent
145ff6973f
commit
dac43896ce
|
@ -24,6 +24,8 @@ export class PreferenceDefaultsService {
|
|||
private readonly sysDefaults: {
|
||||
[key in SysPreference]: (() => PrefValueType) | PrefValueType;
|
||||
} = {
|
||||
[SysPreference.HostOverride]: '',
|
||||
|
||||
[SysPreference.JwtSecret]: () => {
|
||||
const envSecret = this.jwtConfigService.getJwtSecret();
|
||||
if (envSecret) {
|
||||
|
|
|
@ -3,19 +3,19 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||
import {
|
||||
DecodedSysPref,
|
||||
PrefValueType,
|
||||
PrefValueTypeStrings,
|
||||
PrefValueTypeStrings
|
||||
} from 'picsur-shared/dist/dto/preferences.dto';
|
||||
import {
|
||||
SysPreference,
|
||||
SysPreferenceList,
|
||||
SysPreferenceValidators,
|
||||
SysPreferenceValueTypes,
|
||||
SysPreferenceValueTypes
|
||||
} from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { Repository } from 'typeorm';
|
||||
import {
|
||||
ESysPreferenceBackend,
|
||||
ESysPreferenceSchema,
|
||||
ESysPreferenceSchema
|
||||
} from '../../database/entities/sys-preference.entity';
|
||||
import { MutexFallBack } from '../../util/mutex-fallback';
|
||||
import { PreferenceCommonService } from './preference-common.service';
|
||||
|
@ -155,6 +155,9 @@ export class SysPreferenceDbService {
|
|||
const valueValidated = SysPreferenceValidators[key as SysPreference].safeParse(
|
||||
value,
|
||||
);
|
||||
if (!valueValidated.success) {
|
||||
return Fail(FT.UsrValidation, undefined, valueValidated.error);
|
||||
}
|
||||
|
||||
let verifySysPreference = new ESysPreferenceBackend();
|
||||
verifySysPreference.key = validated.key;
|
||||
|
|
|
@ -3,19 +3,19 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||
import {
|
||||
DecodedUsrPref,
|
||||
PrefValueType,
|
||||
PrefValueTypeStrings,
|
||||
PrefValueTypeStrings
|
||||
} from 'picsur-shared/dist/dto/preferences.dto';
|
||||
import {
|
||||
UsrPreference,
|
||||
UsrPreferenceList,
|
||||
UsrPreferenceValidators,
|
||||
UsrPreferenceValueTypes,
|
||||
UsrPreferenceValueTypes
|
||||
} from 'picsur-shared/dist/dto/usr-preferences.enum';
|
||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { Repository } from 'typeorm';
|
||||
import {
|
||||
EUsrPreferenceBackend,
|
||||
EUsrPreferenceSchema,
|
||||
EUsrPreferenceSchema
|
||||
} from '../../database/entities/usr-preference.entity';
|
||||
import { MutexFallBack } from '../../util/mutex-fallback';
|
||||
import { PreferenceCommonService } from './preference-common.service';
|
||||
|
@ -193,15 +193,11 @@ export class UsrPreferenceDbService {
|
|||
);
|
||||
if (HasFailed(validated)) return validated;
|
||||
|
||||
if (!UsrPreferenceValidators[validated.key](validated.value))
|
||||
throw Fail(
|
||||
FT.UsrValidation,
|
||||
undefined,
|
||||
'Preference validator failed for ' +
|
||||
validated.key +
|
||||
' with value ' +
|
||||
validated.value,
|
||||
);
|
||||
const valueValidated =
|
||||
UsrPreferenceValidators[key as UsrPreference].safeParse(value);
|
||||
if (!valueValidated.success) {
|
||||
return Fail(FT.UsrValidation, undefined, valueValidated.error);
|
||||
}
|
||||
|
||||
let verifySysPreference = new EUsrPreferenceBackend();
|
||||
verifySysPreference.key = validated.key;
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { SysPreferenceDbService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
|
||||
@Injectable()
|
||||
export class InfoConfigService {
|
||||
private readonly logger = new Logger(InfoConfigService.name);
|
||||
|
||||
constructor(private readonly prefService: SysPreferenceDbService) {}
|
||||
|
||||
public async getHostnameOverride(): Promise<string | undefined> {
|
||||
const hostname = await this.prefService.getStringPreference(
|
||||
SysPreference.HostOverride,
|
||||
);
|
||||
if (HasFailed(hostname)) {
|
||||
this.logger.warn(hostname.print());
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return hostname;
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ import { PreferenceDbModule } from '../../collections/preference-db/preference-d
|
|||
import { SysPreferenceDbService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
import { EarlyConfigModule } from '../early/early-config.module';
|
||||
import { EarlyJwtConfigService } from '../early/early-jwt.config.service';
|
||||
import { InfoConfigService } from './info.config.service';
|
||||
import { JwtConfigService } from './jwt.config.service';
|
||||
|
||||
// This module contains all configservices that depend on the syspref module
|
||||
|
@ -11,9 +12,9 @@ import { JwtConfigService } from './jwt.config.service';
|
|||
// Otherwise we will create a circular depedency
|
||||
|
||||
@Module({
|
||||
imports: [PreferenceDbModule, EarlyConfigModule],
|
||||
providers: [JwtConfigService],
|
||||
exports: [JwtConfigService, EarlyConfigModule],
|
||||
imports: [EarlyConfigModule, PreferenceDbModule],
|
||||
providers: [JwtConfigService, InfoConfigService],
|
||||
exports: [EarlyConfigModule, JwtConfigService, InfoConfigService],
|
||||
})
|
||||
export class LateConfigModule implements OnModuleInit {
|
||||
private readonly logger = new Logger(LateConfigModule.name);
|
||||
|
|
|
@ -2,17 +2,18 @@ import { Controller, Get } from '@nestjs/common';
|
|||
import {
|
||||
AllFormatsResponse,
|
||||
AllPermissionsResponse,
|
||||
InfoResponse,
|
||||
InfoResponse
|
||||
} from 'picsur-shared/dist/dto/api/info.dto';
|
||||
import {
|
||||
FileType2Ext,
|
||||
FileType2Mime,
|
||||
SupportedAnimFileTypes,
|
||||
SupportedImageFileTypes,
|
||||
SupportedImageFileTypes
|
||||
} from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { TrackingState } from 'picsur-shared/dist/dto/tracking-state.enum';
|
||||
import { FallbackIfFailed } from 'picsur-shared/dist/types';
|
||||
import { HostConfigService } from '../../../config/early/host.config.service';
|
||||
import { InfoConfigService } from '../../../config/late/info.config.service';
|
||||
import { NoPermissions } from '../../../decorators/permissions.decorator';
|
||||
import { Returns } from '../../../decorators/returns.decorator';
|
||||
import { UsageService } from '../../../managers/usage/usage.service';
|
||||
|
@ -23,21 +24,23 @@ import { PermissionsList } from '../../../models/constants/permissions.const';
|
|||
export class InfoController {
|
||||
constructor(
|
||||
private readonly hostConfig: HostConfigService,
|
||||
private readonly infoConfig: InfoConfigService,
|
||||
private readonly usageService: UsageService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Returns(InfoResponse)
|
||||
async getInfo(): Promise<InfoResponse> {
|
||||
const trackingID = FallbackIfFailed(
|
||||
await this.usageService.getTrackingID(),
|
||||
null,
|
||||
) ?? undefined;
|
||||
const trackingID =
|
||||
FallbackIfFailed(await this.usageService.getTrackingID(), null) ??
|
||||
undefined;
|
||||
const hostOverride = await this.infoConfig.getHostnameOverride();
|
||||
|
||||
return {
|
||||
demo: this.hostConfig.isDemo(),
|
||||
production: this.hostConfig.isProduction(),
|
||||
version: this.hostConfig.getVersion(),
|
||||
host_override: hostOverride,
|
||||
tracking: {
|
||||
id: trackingID,
|
||||
state: TrackingState.Detailed,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { EarlyConfigModule } from '../../../config/early/early-config.module';
|
||||
import { LateConfigModule } from '../../../config/late/late-config.module';
|
||||
import { UsageManagerModule } from '../../../managers/usage/usage.module';
|
||||
import { InfoController } from './info.controller';
|
||||
|
||||
@Module({
|
||||
imports: [EarlyConfigModule, UsageManagerModule],
|
||||
imports: [LateConfigModule, UsageManagerModule],
|
||||
controllers: [InfoController],
|
||||
})
|
||||
export class InfoModule {}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
<ng-container *ngIf="pref.type === 'string'">
|
||||
<ng-container *ngIf="type === 'string'">
|
||||
<div class="y-center">
|
||||
<mat-form-field appearance="outline" color="accent">
|
||||
<mat-label>{{ name }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[value]="pref.value"
|
||||
(change)="stringUpdateWrapper($event)"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
placeholder="Empty"
|
||||
[formControl]="formControl"
|
||||
/>
|
||||
<!-- show tooltip on press -->
|
||||
<button
|
||||
|
@ -28,20 +27,22 @@
|
|||
help_outline
|
||||
</mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="formControl.invalid">
|
||||
{{ getErrorMessage() }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="pref.type === 'number'">
|
||||
<ng-container *ngIf="type === 'number'">
|
||||
<div class="y-center">
|
||||
<mat-form-field appearance="outline" color="accent">
|
||||
<mat-label>{{ name }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[value]="pref.value"
|
||||
(change)="numberUpdateWrapper($event)"
|
||||
placeholder="Empty"
|
||||
[formControl]="formControl"
|
||||
/>
|
||||
<button
|
||||
mat-icon-button
|
||||
|
@ -60,18 +61,18 @@
|
|||
help_outline
|
||||
</mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="formControl.invalid">
|
||||
{{ getErrorMessage() }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="pref.type === 'boolean'">
|
||||
<ng-container *ngIf="type === 'boolean'">
|
||||
<div class="y-center">
|
||||
<mat-form-field appearance="outline" color="accent">
|
||||
<mat-label>{{ name }}</mat-label>
|
||||
<mat-select
|
||||
[value]="pref.value"
|
||||
(valueChange)="booleanUpdateWrapper($event)"
|
||||
>
|
||||
<mat-select [formControl]="formControl" placeholder="Empty">
|
||||
<mat-option [value]="false">No</mat-option>
|
||||
<mat-option [value]="true">Yes</mat-option>
|
||||
</mat-select>
|
||||
|
@ -91,6 +92,9 @@
|
|||
help_outline
|
||||
</mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="formControl.invalid">
|
||||
{{ getErrorMessage() }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { AbstractControl, FormControl, ValidationErrors } from '@angular/forms';
|
||||
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
|
||||
import {
|
||||
DecodedPref,
|
||||
PrefValueType
|
||||
} from 'picsur-shared/dist/dto/preferences.dto';
|
||||
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { Subject } from 'rxjs';
|
||||
import { filter } from 'rxjs';
|
||||
import { Required } from 'src/app/models/decorators/required.decorator';
|
||||
import { Logger } from 'src/app/services/logger/logger.service';
|
||||
import { ErrorService } from 'src/app/util/error-manager/error.service';
|
||||
import { Throttle } from 'src/app/util/throttle';
|
||||
import { ZodTypeAny } from 'zod';
|
||||
|
||||
@Component({
|
||||
selector: 'pref-option',
|
||||
|
@ -19,16 +21,28 @@ import { Throttle } from 'src/app/util/throttle';
|
|||
export class PrefOptionComponent implements OnInit {
|
||||
private readonly logger = new Logger(PrefOptionComponent.name);
|
||||
|
||||
@Input() @Required pref: DecodedPref;
|
||||
public formControl = new FormControl<any>(undefined, {
|
||||
updateOn: 'blur',
|
||||
validators: this.syncValidator.bind(this),
|
||||
});
|
||||
|
||||
private pref: DecodedPref;
|
||||
@Input('pref') set prefSet(pref: DecodedPref) {
|
||||
this.pref = pref;
|
||||
this.formControl.setValue(pref.value);
|
||||
}
|
||||
get type() {
|
||||
return this.pref.type;
|
||||
}
|
||||
|
||||
@Input('update') @Required updateFunction: (
|
||||
key: string,
|
||||
pref: PrefValueType,
|
||||
) => AsyncFailable<any>;
|
||||
|
||||
@Input() @Required name: string = '';
|
||||
|
||||
@Input() helpText: string = '';
|
||||
|
||||
private updateSubject = new Subject<PrefValueType>();
|
||||
@Input() validator?: ZodTypeAny = undefined;
|
||||
|
||||
constructor(private readonly errorService: ErrorService) {}
|
||||
|
||||
|
@ -36,44 +50,27 @@ export class PrefOptionComponent implements OnInit {
|
|||
this.subscribeUpdate();
|
||||
}
|
||||
|
||||
get valString(): string {
|
||||
if (this.pref.type !== 'string') {
|
||||
throw new Error('Not a string preference');
|
||||
getErrorMessage() {
|
||||
if (this.formControl.errors) {
|
||||
const errors = this.formControl.errors;
|
||||
if (errors['error']) {
|
||||
return errors['error'];
|
||||
}
|
||||
return 'Invalid value';
|
||||
}
|
||||
return this.pref.value as string;
|
||||
return '';
|
||||
}
|
||||
|
||||
get valNumber(): number {
|
||||
if (this.pref.type !== 'number') {
|
||||
throw new Error('Not an int preference');
|
||||
private syncValidator(control: AbstractControl): ValidationErrors | null {
|
||||
if (!this.validator) return null;
|
||||
|
||||
const result = this.validator.safeParse(control.value);
|
||||
|
||||
if (!result.success) {
|
||||
return { error: result.error.issues[0]?.message ?? 'Invalid value' };
|
||||
}
|
||||
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) {
|
||||
const value = (e.target as HTMLInputElement).valueAsNumber;
|
||||
if (isNaN(value)) return;
|
||||
|
||||
this.update(value);
|
||||
}
|
||||
|
||||
booleanUpdateWrapper(e: boolean) {
|
||||
this.update(e);
|
||||
return null;
|
||||
}
|
||||
|
||||
private async updatePreference(value: PrefValueType) {
|
||||
|
@ -97,8 +94,11 @@ export class PrefOptionComponent implements OnInit {
|
|||
|
||||
@AutoUnsubscribe()
|
||||
subscribeUpdate() {
|
||||
return this.updateSubject
|
||||
.pipe(Throttle(300))
|
||||
return this.formControl.valueChanges
|
||||
.pipe(
|
||||
filter((value) => this.formControl.errors === null),
|
||||
Throttle(300),
|
||||
)
|
||||
.subscribe(this.updatePreference.bind(this));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
@ -17,6 +18,7 @@ import { PrefOptionComponent } from './pref-option.component';
|
|||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
MatButtonModule,
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
|
|
|
@ -7,21 +7,10 @@ export const SysPreferenceUI: {
|
|||
category: string;
|
||||
};
|
||||
} = {
|
||||
[SysPreference.JwtSecret]: {
|
||||
name: 'JWT Secret',
|
||||
helpText: 'Secret used to sign JWT authentication tokens.',
|
||||
category: 'Authentication',
|
||||
},
|
||||
[SysPreference.JwtExpiresIn]: {
|
||||
name: 'JWT Expiry Time',
|
||||
helpText: 'Time before JWT authentication tokens expire.',
|
||||
category: 'Authentication',
|
||||
},
|
||||
[SysPreference.BCryptStrength]: {
|
||||
name: 'BCrypt Strength',
|
||||
helpText:
|
||||
'Strength of BCrypt hashing algorithm, 10 is recommended. Reduce this if running on a low powered device.',
|
||||
category: 'Authentication',
|
||||
[SysPreference.HostOverride]: {
|
||||
name: 'Host Override',
|
||||
helpText: 'Override the hostname for the server, useful for when you are accessing the server from a different domain.',
|
||||
category: 'General',
|
||||
},
|
||||
|
||||
[SysPreference.RemoveDerivativesAfter]: {
|
||||
|
@ -43,7 +32,6 @@ export const SysPreferenceUI: {
|
|||
|
||||
category: 'Image Processing',
|
||||
},
|
||||
|
||||
[SysPreference.ConversionTimeLimit]: {
|
||||
name: 'Convert/Edit Time Limit',
|
||||
helpText:
|
||||
|
@ -57,6 +45,23 @@ export const SysPreferenceUI: {
|
|||
category: 'Image Processing',
|
||||
},
|
||||
|
||||
[SysPreference.JwtSecret]: {
|
||||
name: 'JWT Secret',
|
||||
helpText: 'Secret used to sign JWT authentication tokens.',
|
||||
category: 'Authentication',
|
||||
},
|
||||
[SysPreference.JwtExpiresIn]: {
|
||||
name: 'JWT Expiry Time',
|
||||
helpText: 'Time before JWT authentication tokens expire.',
|
||||
category: 'Authentication',
|
||||
},
|
||||
[SysPreference.BCryptStrength]: {
|
||||
name: 'BCrypt Strength',
|
||||
helpText:
|
||||
'Strength of BCrypt hashing algorithm, 10 is recommended. Reduce this if running on a low powered device.',
|
||||
category: 'Authentication',
|
||||
},
|
||||
|
||||
[SysPreference.EnableTracking]: {
|
||||
name: 'Enable Ackee Web Tracking',
|
||||
helpText:
|
||||
|
@ -65,7 +70,7 @@ export const SysPreferenceUI: {
|
|||
},
|
||||
[SysPreference.TrackingUrl]: {
|
||||
name: 'Ackee tracking URL',
|
||||
helpText: 'URL of the Ackee tracking server.',
|
||||
helpText: 'URL of the Ackee tracking server. Requests are proxied, so ensure the X-Forwarded-For header is handled.',
|
||||
category: 'Usage',
|
||||
},
|
||||
[SysPreference.TrackingId]: {
|
||||
|
|
|
@ -4,6 +4,7 @@ export class ServerInfo {
|
|||
production: boolean = false;
|
||||
demo: boolean = false;
|
||||
version: string = '0.0.0';
|
||||
host_override?: string;
|
||||
tracking: {
|
||||
state: TrackingState;
|
||||
id?: string;
|
||||
|
|
|
@ -7,6 +7,7 @@ import { HasFailed } from 'picsur-shared/dist/types';
|
|||
import { BehaviorSubject } from 'rxjs';
|
||||
import { scan } from 'rxjs/operators';
|
||||
import { ApiKeysService } from 'src/app/services/api/apikeys.service';
|
||||
import { InfoService } from 'src/app/services/api/info.service';
|
||||
import { PermissionService } from 'src/app/services/api/permission.service';
|
||||
import { Logger } from 'src/app/services/logger/logger.service';
|
||||
import { ErrorService } from 'src/app/util/error-manager/error.service';
|
||||
|
@ -41,6 +42,7 @@ export class SettingsShareXComponent implements OnInit {
|
|||
constructor(
|
||||
private readonly apikeysService: ApiKeysService,
|
||||
private readonly permissionService: PermissionService,
|
||||
private readonly infoService: InfoService,
|
||||
private readonly utilService: UtilService,
|
||||
private readonly errorService: ErrorService,
|
||||
) {}
|
||||
|
@ -66,7 +68,7 @@ export class SettingsShareXComponent implements OnInit {
|
|||
}
|
||||
|
||||
const sharexConfig = BuildShareX(
|
||||
this.utilService.getHost(),
|
||||
this.infoService.getHostname(),
|
||||
this.key,
|
||||
'.' + ext,
|
||||
canUseDelete,
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
[update]="sysPrefService.setPreference.bind(sysPrefService)"
|
||||
[name]="getName(pref.key)"
|
||||
[helpText]="getHelpText(pref.key)"
|
||||
[validator]="getValidator(pref.key)"
|
||||
></pref-option>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
|
|
@ -1,42 +1,54 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { DecodedPref } from 'picsur-shared/dist/dto/preferences.dto';
|
||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import {
|
||||
SysPreference,
|
||||
SysPreferenceValidators
|
||||
} from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { SysPreferenceUI } from 'src/app/i18n/sys-pref.i18n';
|
||||
|
||||
import { makeUnique } from 'picsur-shared/dist/util/unique';
|
||||
import { SysPrefService } from 'src/app/services/api/sys-pref.service';
|
||||
import { z, ZodTypeAny } from 'zod';
|
||||
|
||||
@Component({
|
||||
templateUrl: './settings-sys-pref.component.html',
|
||||
styleUrls: ['./settings-sys-pref.component.scss'],
|
||||
})
|
||||
export class SettingsSysprefComponent {
|
||||
private readonly syspreferenceUI = SysPreferenceUI;
|
||||
|
||||
public getName(key: string) {
|
||||
return this.syspreferenceUI[key as SysPreference]?.name ?? key;
|
||||
return SysPreferenceUI[key as SysPreference]?.name ?? key;
|
||||
}
|
||||
|
||||
public getHelpText(key: string) {
|
||||
return this.syspreferenceUI[key as SysPreference]?.helpText ?? '';
|
||||
return SysPreferenceUI[key as SysPreference]?.helpText ?? '';
|
||||
}
|
||||
|
||||
public getCategory(key: string): null | string {
|
||||
return this.syspreferenceUI[key as SysPreference]?.category ?? null;
|
||||
return SysPreferenceUI[key as SysPreference]?.category ?? null;
|
||||
}
|
||||
|
||||
preferences: Observable<Array<{ category: string | null; prefs: DecodedPref[] }>>;
|
||||
public getValidator(key: string): ZodTypeAny {
|
||||
return SysPreferenceValidators[key as SysPreference] ?? z.any();
|
||||
}
|
||||
|
||||
preferences: Observable<
|
||||
Array<{ category: string | null; prefs: DecodedPref[] }>
|
||||
>;
|
||||
|
||||
constructor(public readonly sysPrefService: SysPrefService) {
|
||||
this.preferences = sysPrefService.live.pipe(
|
||||
map((prefs) => {
|
||||
const categories = makeUnique(prefs.map((pref) => this.getCategory(pref.key)));
|
||||
const categories = makeUnique(
|
||||
prefs.map((pref) => this.getCategory(pref.key)),
|
||||
);
|
||||
return categories.map((category) => ({
|
||||
category,
|
||||
prefs: prefs.filter((pref) => this.getCategory(pref.key) === category),
|
||||
prefs: prefs.filter(
|
||||
(pref) => this.getCategory(pref.key) === category,
|
||||
),
|
||||
}));
|
||||
}),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,11 +17,11 @@ import { ErrorService } from 'src/app/util/error-manager/error.service';
|
|||
import { UtilService } from 'src/app/util/util.service';
|
||||
import {
|
||||
CustomizeDialogComponent,
|
||||
CustomizeDialogData,
|
||||
CustomizeDialogData
|
||||
} from '../customize-dialog/customize-dialog.component';
|
||||
import {
|
||||
EditDialogComponent,
|
||||
EditDialogData,
|
||||
EditDialogData
|
||||
} from '../edit-dialog/edit-dialog.component';
|
||||
|
||||
@Component({
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
ChangeDetectorRef,
|
||||
Component,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto';
|
||||
|
@ -11,7 +11,7 @@ import { ImageLinks } from 'picsur-shared/dist/dto/image-links.class';
|
|||
import {
|
||||
AnimFileType,
|
||||
ImageFileType,
|
||||
SupportedFileTypeCategory,
|
||||
SupportedFileTypeCategory
|
||||
} from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { EImage } from 'picsur-shared/dist/entities/image.entity';
|
||||
import { EUser } from 'picsur-shared/dist/entities/user.entity';
|
||||
|
@ -85,12 +85,19 @@ export class ViewComponent implements OnInit, OnDestroy {
|
|||
);
|
||||
}
|
||||
|
||||
private imageLinksCache: Record<string, ImageLinks> = {};
|
||||
public get imageLinks(): ImageLinks {
|
||||
if (this.imageLinksCache[this.selectedFormat] !== undefined)
|
||||
return this.imageLinksCache[this.selectedFormat];
|
||||
|
||||
const format = this.selectedFormat;
|
||||
return this.imageService.CreateImageLinksFromID(
|
||||
const links = this.imageService.CreateImageLinksFromID(
|
||||
this.id,
|
||||
format === 'original' ? null : format,
|
||||
);
|
||||
|
||||
this.imageLinksCache[format] = links;
|
||||
return links;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
|
|
|
@ -6,11 +6,11 @@ import {
|
|||
ImageListResponse,
|
||||
ImageUpdateRequest,
|
||||
ImageUpdateResponse,
|
||||
ImageUploadResponse,
|
||||
ImageUploadResponse
|
||||
} from 'picsur-shared/dist/dto/api/image-manage.dto';
|
||||
import {
|
||||
ImageMetaResponse,
|
||||
ImageRequestParams,
|
||||
ImageRequestParams
|
||||
} from 'picsur-shared/dist/dto/api/image.dto';
|
||||
import { ImageLinks } from 'picsur-shared/dist/dto/image-links.class';
|
||||
import { FileType2Ext } from 'picsur-shared/dist/dto/mimes.dto';
|
||||
|
@ -21,11 +21,11 @@ import {
|
|||
FT,
|
||||
HasFailed,
|
||||
HasSuccess,
|
||||
Open,
|
||||
Open
|
||||
} from 'picsur-shared/dist/types/failable';
|
||||
import { UtilService } from 'src/app/util/util.service';
|
||||
import { ImageUploadRequest } from '../../models/dto/image-upload-request.dto';
|
||||
import { ApiService } from './api.service';
|
||||
import { InfoService } from './info.service';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
@Injectable({
|
||||
|
@ -34,7 +34,7 @@ import { UserService } from './user.service';
|
|||
export class ImageService {
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
private readonly util: UtilService,
|
||||
private readonly infoService: InfoService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
|
@ -126,7 +126,7 @@ export class ImageService {
|
|||
// Non api calls
|
||||
|
||||
public GetImageURL(image: string, filetype: string | null): string {
|
||||
const baseURL = this.util.getHost();
|
||||
const baseURL = this.infoService.getHostname();
|
||||
const extension = FileType2Ext(filetype ?? '');
|
||||
|
||||
return `${baseURL}/i/${image}${
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { LOCATION } from '@ng-web-apis/common';
|
||||
import { InfoResponse } from 'picsur-shared/dist/dto/api/info.dto';
|
||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { SemVerRegex } from 'picsur-shared/dist/util/common-regex';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, filter, Observable, take } from 'rxjs';
|
||||
import pkg from '../../../../package.json';
|
||||
import { ServerInfo } from '../../models/dto/server-info.dto';
|
||||
import { Logger } from '../logger/logger.service';
|
||||
|
@ -24,7 +25,12 @@ export class InfoService {
|
|||
|
||||
private infoSubject = new BehaviorSubject<ServerInfo>(new ServerInfo());
|
||||
|
||||
constructor(private readonly api: ApiService) {}
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
@Inject(LOCATION) private readonly location: Location,
|
||||
) {
|
||||
this.pollInfo().catch((e) => this.logger.warn(e));
|
||||
}
|
||||
|
||||
public async pollInfo(): AsyncFailable<ServerInfo> {
|
||||
const response = await this.api.get(InfoResponse, '/api/info');
|
||||
|
@ -34,10 +40,34 @@ export class InfoService {
|
|||
return response;
|
||||
}
|
||||
|
||||
public async getLoadedSnapshot(): Promise<ServerInfo> {
|
||||
if (this.isLoaded()) {
|
||||
return this.snapshot;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const filtered = this.live.pipe(
|
||||
filter((info) => info.version !== '0.0.0'),
|
||||
take(1),
|
||||
);
|
||||
(filtered as Observable<ServerInfo>).subscribe(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
public getFrontendVersion(): string {
|
||||
return pkg.version;
|
||||
}
|
||||
|
||||
public getHostname(): string {
|
||||
// const info = await this.getLoadedSnapshot();
|
||||
|
||||
// if (info.host_override !== undefined) {
|
||||
// return info.host_override;
|
||||
// }
|
||||
|
||||
return this.location.protocol + '//' + this.location.host;
|
||||
}
|
||||
|
||||
// If either version starts with 0. it has to be exactly the same
|
||||
// If both versions start with something else, they have to match the first part
|
||||
public async isCompatibleWithServer(): AsyncFailable<boolean> {
|
||||
|
@ -67,4 +97,8 @@ export class InfoService {
|
|||
return serverDecoded[0] === clientDecoded[0];
|
||||
}
|
||||
}
|
||||
|
||||
public isLoaded(): boolean {
|
||||
return this.snapshot.version !== '0.0.0';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { Inject, Injectable } from '@angular/core';
|
||||
import { LOCATION } from '@ng-web-apis/common';
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
FileType2Ext,
|
||||
SupportedFileTypes,
|
||||
SupportedFileTypes
|
||||
} from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { Logger } from '../services/logger/logger.service';
|
||||
|
@ -13,12 +12,6 @@ import { Logger } from '../services/logger/logger.service';
|
|||
export class UtilService {
|
||||
private readonly logger = new Logger(UtilService.name);
|
||||
|
||||
constructor(@Inject(LOCATION) private readonly location: Location) {}
|
||||
|
||||
public getHost(): string {
|
||||
return this.location.protocol + '//' + this.location.host;
|
||||
}
|
||||
|
||||
public downloadBuffer(
|
||||
buffer: ArrayBuffer | string,
|
||||
filename: string,
|
||||
|
|
|
@ -9,6 +9,7 @@ export const InfoResponseSchema = z.object({
|
|||
production: z.boolean(),
|
||||
demo: z.boolean(),
|
||||
version: string().regex(SemVerRegex),
|
||||
host_override: z.string().optional(),
|
||||
tracking: z.object({
|
||||
state: TrackingStateSchema,
|
||||
id: IsEntityID().optional(),
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { PrefValueTypeStrings } from './preferences.dto';
|
||||
import ms from 'ms';
|
||||
import { IsValidMS } from '../validators/ms.validator';
|
||||
import { URLRegex, UUIDRegex } from '../util/common-regex';
|
||||
import { z } from 'zod';
|
||||
import { IsPosInt } from '../validators/positive-int.validator';
|
||||
import { HostNameRegex, URLRegex } from '../util/common-regex';
|
||||
import { IsEntityID } from '../validators/entity-id.validator';
|
||||
import { IsValidMS } from '../validators/ms.validator';
|
||||
import { IsPosInt } from '../validators/positive-int.validator';
|
||||
import { PrefValueTypeStrings } from './preferences.dto';
|
||||
|
||||
// This enum is only here to make accessing the values easier, and type checking in the backend
|
||||
export enum SysPreference {
|
||||
HostOverride = 'host_override',
|
||||
|
||||
JwtSecret = 'jwt_secret',
|
||||
JwtExpiresIn = 'jwt_expires_in',
|
||||
BCryptStrength = 'bcrypt_strength',
|
||||
|
@ -33,6 +34,8 @@ export const SysPreferenceList: string[] = Object.values(SysPreference);
|
|||
export const SysPreferenceValueTypes: {
|
||||
[key in SysPreference]: PrefValueTypeStrings;
|
||||
} = {
|
||||
[SysPreference.HostOverride]: 'string',
|
||||
|
||||
[SysPreference.JwtSecret]: 'string',
|
||||
[SysPreference.JwtExpiresIn]: 'string',
|
||||
[SysPreference.BCryptStrength]: 'number',
|
||||
|
@ -54,6 +57,11 @@ export const SysPreferenceValueTypes: {
|
|||
export const SysPreferenceValidators: {
|
||||
[key in SysPreference]: z.ZodTypeAny;
|
||||
} = {
|
||||
[SysPreference.HostOverride]: z
|
||||
.string()
|
||||
.regex(HostNameRegex)
|
||||
.or(z.literal('')),
|
||||
|
||||
[SysPreference.JwtSecret]: z.boolean(),
|
||||
[SysPreference.JwtExpiresIn]: IsValidMS(),
|
||||
|
||||
|
@ -66,8 +74,8 @@ export const SysPreferenceValidators: {
|
|||
[SysPreference.ConversionMemoryLimit]: IsPosInt(),
|
||||
|
||||
[SysPreference.EnableTracking]: z.boolean(),
|
||||
[SysPreference.TrackingUrl]: z.string().regex(URLRegex),
|
||||
[SysPreference.TrackingId]: IsEntityID(),
|
||||
[SysPreference.TrackingUrl]: z.string().regex(URLRegex).or(z.literal('')),
|
||||
[SysPreference.TrackingId]: IsEntityID().or(z.literal('')),
|
||||
|
||||
[SysPreference.EnableTelemetry]: z.boolean(),
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import { PrefValueTypeStrings } from './preferences.dto';
|
||||
|
||||
// This enum is only here to make accessing the values easier, and type checking in the backend
|
||||
|
@ -16,7 +17,7 @@ export const UsrPreferenceValueTypes: {
|
|||
};
|
||||
|
||||
export const UsrPreferenceValidators: {
|
||||
[key in UsrPreference]: (value: any) => boolean;
|
||||
[key in UsrPreference]: z.ZodTypeAny;
|
||||
} = {
|
||||
[UsrPreference.KeepOriginal]: (value: any) => typeof value === 'boolean',
|
||||
[UsrPreference.KeepOriginal]: z.boolean(),
|
||||
};
|
||||
|
|
|
@ -5,3 +5,4 @@ export const URLRegex =
|
|||
/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/;
|
||||
export const UUIDRegex =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
export const HostNameRegex = /^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\.?$/;
|
||||
|
|
|
@ -2,4 +2,20 @@ import ms from 'ms';
|
|||
import { z } from 'zod';
|
||||
|
||||
export const IsValidMS = () =>
|
||||
z.preprocess((v) => ms(v as any), z.number().int().min(0));
|
||||
z.preprocess(
|
||||
(v: any) => {
|
||||
try {
|
||||
return ms(v);
|
||||
} catch (e) {
|
||||
return NaN;
|
||||
}
|
||||
},
|
||||
z
|
||||
.number({
|
||||
errorMap: () => ({
|
||||
message: 'Invalid duration value',
|
||||
}),
|
||||
})
|
||||
.int()
|
||||
.min(0),
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue