Add client side preference verification

Add support for hostname override
This commit is contained in:
rubikscraft 2022-12-25 23:24:16 +01:00
parent 145ff6973f
commit dac43896ce
No known key found for this signature in database
GPG Key ID: 1463EBE9200A5CD4
25 changed files with 254 additions and 138 deletions

View File

@ -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) {

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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,

View File

@ -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 {}

View File

@ -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>

View File

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

View File

@ -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,

View File

@ -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]: {

View File

@ -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;

View File

@ -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,

View File

@ -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>

View File

@ -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,
),
}));
}),
)
);
}
}

View File

@ -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({

View File

@ -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() {

View File

@ -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}${

View File

@ -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';
}
}

View File

@ -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,

View File

@ -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(),

View File

@ -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(),
};

View File

@ -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(),
};

View File

@ -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])?)*\.?$/;

View File

@ -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),
);