1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-12-23 01:27:14 +02:00

Implementing template config savings #569

This commit is contained in:
Patrik J. Braun 2022-12-28 22:12:53 +01:00
parent 9c4178e508
commit 9ac67ead63
15 changed files with 150 additions and 132 deletions

View File

@ -1,13 +1,14 @@
import { NextFunction, Request, Response } from 'express';
import { ErrorCodes, ErrorDTO } from '../../common/entities/Error';
import { Message } from '../../common/entities/Message';
import { Config, PrivateConfigClass } from '../../common/config/private/Config';
import { UserDTO, UserRoles } from '../../common/entities/UserDTO';
import { NotificationManager } from '../model/NotifocationManager';
import { Logger } from '../Logger';
import { SharingDTO } from '../../common/entities/SharingDTO';
import { Utils } from '../../common/Utils';
import { LoggerRouter } from '../routes/LoggerRouter';
import {NextFunction, Request, Response} from 'express';
import {ErrorCodes, ErrorDTO} from '../../common/entities/Error';
import {Message} from '../../common/entities/Message';
import {Config, PrivateConfigClass} from '../../common/config/private/Config';
import {UserDTO, UserRoles} from '../../common/entities/UserDTO';
import {NotificationManager} from '../model/NotifocationManager';
import {Logger} from '../Logger';
import {SharingDTO} from '../../common/entities/SharingDTO';
import {Utils} from '../../common/Utils';
import {LoggerRouter} from '../routes/LoggerRouter';
import {TAGS} from '../../common/config/public/ClientConfig';
export class RenderingMWs {
public static renderResult(
@ -57,7 +58,7 @@ export class RenderingMWs {
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password, creator, ...sharing } = req.resultPipe as SharingDTO;
const {password, creator, ...sharing} = req.resultPipe as SharingDTO;
RenderingMWs.renderMessage(res, sharing);
}
@ -107,12 +108,12 @@ export class RenderingMWs {
const originalConf = await Config.original();
// These are sensitive information, do not send to the client side
originalConf.Server.sessionSecret = null;
originalConf.Users.enforcedUsers = null;
const message = new Message<PrivateConfigClass>(
null,
originalConf.toJSON({
attachState: true,
attachVolatile: true,
skipTags: {secret: true} as TAGS
}) as PrivateConfigClass
);
res.json(message);

View File

@ -3,6 +3,8 @@ import {ErrorCodes, ErrorDTO} from '../../../common/entities/Error';
import {Logger} from '../../Logger';
import {Config} from '../../../common/config/private/Config';
import {ConfigDiagnostics} from '../../model/diagnostics/ConfigDiagnostics';
import {ConfigClassBuilder} from '../../../../node_modules/typeconfig/node';
import {TAGS} from '../../../common/config/public/ClientConfig';
const LOG_TAG = '[SettingsMWs]';
@ -23,9 +25,18 @@ export class SettingsMWs {
}
try {
const settings = req.body.settings; // Top level settings JSON
let settings = req.body.settings; // Top level settings JSON
const settingsPath: string = req.body.settingsPath; // Name of the top level settings
const transformer = await Config.original();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
transformer[settingsPath] = settings;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
settings = ConfigClassBuilder.attachPrivateInterface(transformer[settingsPath]).toJSON({
skipTags: {secret: true} as TAGS
});
const original = await Config.original();
// only updating explicitly set config (not saving config set by the diagnostics)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -38,7 +49,7 @@ export class SettingsMWs {
original.save();
await ConfigDiagnostics.runDiagnostics();
Logger.info(LOG_TAG, 'new config:');
Logger.info(LOG_TAG, JSON.stringify(Config, null, '\t'));
Logger.info(LOG_TAG, JSON.stringify(Config.toJSON({attachDescription: false}), null, '\t'));
return next();
} catch (err) {
if (err instanceof Error) {

View File

@ -119,16 +119,21 @@ export class SQLConnection {
) {
for (const uc of Config.Users.enforcedUsers) {
const user = await userRepository.findOneBy({ name: uc.name });
// encrypt password and save back to the config
if(uc.password) {
console.log(uc.password);
if (!uc.encryptedPassword) {
uc.encryptedPassword = PasswordHelper.cryptPassword(uc.password);
}
uc.encrypted = !!uc.encryptedPassword;
uc.password = '';
await Config.save();
}
if (!user) {
Logger.info(LOG_TAG, 'Saving enforced user: ' + uc.name);
const a = new UserEntity();
a.name = uc.name;
// encrypt password and save back to the db
if (!uc.encryptedPassword) {
uc.encryptedPassword = PasswordHelper.cryptPassword(uc.password);
uc.password = '';
await Config.save();
}
a.password = uc.encryptedPassword;
a.role = uc.role;
await userRepository.save(a);

View File

@ -9,7 +9,7 @@ import {CookieNames} from '../../common/CookieNames';
import {ErrorCodes, ErrorDTO} from '../../common/entities/Error';
import {UserDTO} from '../../common/entities/UserDTO';
import {ServerTimeEntry} from '../middlewares/ServerTimingMWs';
import {ClientConfig} from '../../common/config/public/ClientConfig';
import {ClientConfig, TAGS} from '../../common/config/public/ClientConfig';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
@ -83,7 +83,11 @@ export class PublicRouter {
res.tpl.user.csrfToken = req.csrfToken();
}
}
const confCopy = Config.toJSON({attachVolatile: true, keepTags: {client: true}}) as unknown as ClientConfig;
const confCopy = Config.toJSON({
attachVolatile: true,
skipTags: {secret: true} as TAGS,
keepTags: {client: true}
}) as unknown as ClientConfig;
// Escaping html tags, like <script></script>
confCopy.Server.customHTMLHead =
confCopy.Server.customHTMLHead

View File

@ -62,7 +62,7 @@ export class Server {
).configPath +
':'
);
Logger.verbose(LOG_TAG, JSON.stringify(Config, null, '\t'));
Logger.verbose(LOG_TAG, JSON.stringify(Config.toJSON({attachDescription: false}), null, '\t'));
this.app = express();

View File

@ -1,26 +1,25 @@
/* eslint-disable @typescript-eslint/no-inferrable-types */
import 'reflect-metadata';
import {
JobScheduleDTO,
JobTrigger,
JobTriggerType,
} from '../../entities/job/JobScheduleDTO';
import {JobScheduleDTO, JobTrigger, JobTriggerType,} from '../../entities/job/JobScheduleDTO';
import {
ClientConfig,
ClientGPXCompressingConfig, ClientMediaConfig,
ClientMetaFileConfig, ClientPhotoConfig, ClientPhotoConvertingConfig,
ClientGPXCompressingConfig,
ClientMediaConfig,
ClientMetaFileConfig,
ClientPhotoConfig,
ClientPhotoConvertingConfig,
ClientServiceConfig,
ClientSharingConfig, ClientThumbnailConfig,
ClientUserConfig, ClientVideoConfig, ConfigPriority, MapProviders, TAGS
ClientSharingConfig,
ClientThumbnailConfig,
ClientUserConfig,
ClientVideoConfig,
ConfigPriority,
TAGS
} from '../public/ClientConfig';
import {SubConfigClass} from 'typeconfig/src/decorators/class/SubConfigClass';
import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty';
import {DefaultsJobs} from '../../entities/job/JobDTO';
import {
SearchQueryDTO,
SearchQueryTypes,
TextSearch,
} from '../../entities/SearchQueryDTO';
import {SearchQueryDTO, SearchQueryTypes, TextSearch,} from '../../entities/SearchQueryDTO';
import {SortingMethods} from '../../entities/SortingMethods';
import {UserRoles} from '../../entities/UserDTO';
@ -163,17 +162,18 @@ export class UserConfig {
priority: ConfigPriority.underTheHood
},
})
role: UserRoles;
role: UserRoles = UserRoles.User;
@ConfigProperty({
tags:
{
name: $localize`Password`,
priority: ConfigPriority.underTheHood
priority: ConfigPriority.underTheHood,
relevant: (c: UserConfig) => !c.encrypted
},
description: $localize`Unencrypted, temporary password. App will encrypt it and delete this.`
})
password: string = '';
password: string;
@ConfigProperty({
tags:
{
@ -184,10 +184,25 @@ export class UserConfig {
})
encryptedPassword: string | undefined;
constructor(name: string, password: string, role: UserRoles) {
this.name = name;
this.role = role;
this.password = password;
@ConfigProperty({
tags:
{
priority: ConfigPriority.underTheHood,
relevant: () => false // never render this on UI. Only used to indicate that encryption is done.
} as TAGS,
})
encrypted: boolean;
constructor(name?: string, password?: string, role?: UserRoles) {
if (name) {
this.name = name;
}
if (typeof role !== 'undefined') {
this.role = role;
}
if (password) {
this.password = password;
}
}
}
@ -195,7 +210,7 @@ export class UserConfig {
export class ServerDataBaseConfig {
@ConfigProperty<DatabaseType, ServerConfig>({
type: DatabaseType,
onNewValue: (value: DatabaseType, config: ServerConfig) => {
onNewValue: (value, config) => {
if (config && value === DatabaseType.memory) {
config.Search.enabled = false;
config.Sharing.enabled = false;
@ -248,8 +263,9 @@ export class ServerUserConfig extends ClientUserConfig {
tags:
{
name: $localize`Enforced users`,
priority: ConfigPriority.underTheHood
},
priority: ConfigPriority.underTheHood,
uiOptional: true
} as TAGS,
description: $localize`Creates these users in the DB if they do not exist. If a user with this name exist, it won't be overwritten, even if the role is different.`,
})
enforcedUsers: UserConfig[] = [];

View File

@ -724,7 +724,7 @@ export class ClientServiceConfig {
@ConfigProperty({
tags: {
name: $localize`Page title`
}
} as TAGS
})
applicationTitle: string = 'PiGallery 2';
@ -732,6 +732,7 @@ export class ClientServiceConfig {
description: $localize`If you access the page form local network its good to know the public url for creating sharing link.`,
tags: {
name: $localize`Page public url`,
uiOptional: true
}
})
publicUrl: string = '';
@ -741,7 +742,8 @@ export class ClientServiceConfig {
tags: {
name: $localize`Url Base`,
hint: '/myGallery',
priority: ConfigPriority.advanced
priority: ConfigPriority.advanced,
uiOptional: true
}
})
urlBase: string = '';
@ -763,7 +765,8 @@ export class ClientServiceConfig {
tags: {
name: $localize`Custom HTML Head`,
priority: ConfigPriority.advanced,
githubIssue: 404
githubIssue: 404,
uiOptional: true
}
})
customHTMLHead: string = '';
@ -772,7 +775,12 @@ export class ClientServiceConfig {
@SubConfigClass({tags: {client: true}})
export class ClientUserConfig {
@ConfigProperty({
@ConfigProperty<boolean, ClientConfig>({
onNewValue: (value, config) => {
if (config && value === false) {
config.Sharing.enabled = false;
}
},
tags: {
name: $localize`Password protection`,
priority: ConfigPriority.advanced,

View File

@ -99,6 +99,7 @@ import {GalleryFilterComponent} from './ui/gallery/filter/filter.gallery.compone
import {GallerySortingService} from './ui/gallery/navigator/sorting.service';
import {FilterService} from './ui/gallery/filter/filter.service';
import {TemplateComponent} from './ui/settings/template/template.component';
import {AbstractSettingsService} from './ui/settings/_abstract/abstract.settings.service';
@Injectable()
export class MyHammerConfig extends HammerGestureConfig {
@ -216,24 +217,24 @@ Marker.prototype.options.icon = iconDefault;
// Settings
SettingsEntryComponent,
TemplateComponent,
/* UserMangerSettingsComponent,
DatabaseSettingsComponent,
MapSettingsComponent,
ThumbnailSettingsComponent,
VideoSettingsComponent,
PhotoSettingsComponent,
MetaFileSettingsComponent,
SearchSettingsComponent,
ShareSettingsComponent,
RandomPhotoSettingsComponent,
FacesSettingsComponent,
AlbumsSettingsComponent,
OtherSettingsComponent,
IndexingSettingsComponent,
JobsSettingsComponent,
JobProgressComponent,
JobButtonComponent,
PreviewSettingsComponent,*/
/* UserMangerSettingsComponent,
DatabaseSettingsComponent,
MapSettingsComponent,
ThumbnailSettingsComponent,
VideoSettingsComponent,
PhotoSettingsComponent,
MetaFileSettingsComponent,
SearchSettingsComponent,
ShareSettingsComponent,
RandomPhotoSettingsComponent,
FacesSettingsComponent,
AlbumsSettingsComponent,
OtherSettingsComponent,
IndexingSettingsComponent,
JobsSettingsComponent,
JobProgressComponent,
JobButtonComponent,
PreviewSettingsComponent,*/
// Pipes
StringifyRole,
@ -281,6 +282,7 @@ Marker.prototype.options.icon = iconDefault;
ScheduledJobsService,
BackendtextService,
CookieService,
AbstractSettingsService
],
bootstrap: [AppComponent],
})

View File

@ -25,6 +25,7 @@ import {IConfigClassPrivate} from '../../../../../../node_modules/typeconfig/src
import {IPropertyMetadata} from '../../../../../../node_modules/typeconfig/src/decorators/property/IPropertyState';
import {IWebConfigClass, IWebConfigClassPrivate} from '../../../../../../node_modules/typeconfig/src/decorators/class/IWebConfigClass';
import {WebConfigClassBuilder} from '../../../../../../node_modules/typeconfig/src/decorators/builders/WebConfigClassBuilder';
import {ServerConfig} from '../../../../../common/config/private/PrivateConfig';
interface ConfigState<T = unknown> {
value: T;
@ -59,17 +60,14 @@ export interface RecursiveState extends ConfigState {
@Directive()
export abstract class SettingsComponentDirective<
T extends RecursiveState,
S extends AbstractSettingsService<T> = AbstractSettingsService<T>
> implements OnInit, OnDestroy, OnChanges, ISettingsComponent {
@Input()
public configPriority = ConfigPriority.basic;
T extends RecursiveState> implements OnInit, OnDestroy, ISettingsComponent {
@Input() icon: string;
@Input() ConfigPath: string;
@ViewChild('settingsForm', {static: true})
form: FormControl;
@Output()
hasAvailableSettings = true;
public inProgress = false;
public error: string = null;
@ -82,10 +80,9 @@ export abstract class SettingsComponentDirective<
protected constructor(
protected name: string,
public icon: string,
protected authService: AuthenticationService,
private navigation: NavigationService,
public settingsService: S,
public settingsService: AbstractSettingsService,
protected notification: NotificationService,
protected globalSettingsService: SettingsService,
sliceFN?: (s: IWebConfigClassPrivate<TAGS> & WebConfig) => T
@ -175,8 +172,6 @@ export abstract class SettingsComponentDirective<
}
};
instrument(this.states, null);
this.ngOnChanges();
};
onOptionChange = () => {
@ -224,9 +219,6 @@ export abstract class SettingsComponentDirective<
});
}
ngOnChanges(): void {
}
ngOnDestroy(): void {
if (this.subscription != null) {
this.subscription.unsubscribe();
@ -248,7 +240,7 @@ export abstract class SettingsComponentDirective<
this.inProgress = true;
this.error = '';
try {
await this.settingsService.updateSettings(this.stateToSettings());
await this.settingsService.updateSettings(this.stateToSettings(), this.ConfigPath);
await this.getSettings();
this.notification.success(
this.Name + ' ' + $localize`settings saved`,

View File

@ -1,9 +1,14 @@
import { BehaviorSubject } from 'rxjs';
import { SettingsService } from '../settings.service';
import { WebConfig } from '../../../../../common/config/private/WebConfig';
import {BehaviorSubject} from 'rxjs';
import {SettingsService} from '../settings.service';
import {WebConfig} from '../../../../../common/config/private/WebConfig';
import {NetworkService} from '../../../model/network/network.service';
import {Injectable} from '@angular/core';
export abstract class AbstractSettingsService<T> {
protected constructor(public settingsService: SettingsService) {}
@Injectable()
export class AbstractSettingsService {
constructor(public settingsService: SettingsService,
private networkService: NetworkService) {
}
get Settings(): BehaviorSubject<WebConfig> {
return this.settingsService.settings;
@ -13,6 +18,7 @@ export abstract class AbstractSettingsService<T> {
return this.settingsService.getSettings();
}
abstract updateSettings(settings: T): Promise<void>;
public updateSettings(settings: Record<string, any>, settingsPath: string): Promise<void> {
return this.networkService.putJson('/settings', {settings, settingsPath});
}
}

View File

@ -186,8 +186,8 @@
</ng-container>
<ng-container *ngIf="ArrayType === 'UserConfig'">
<div class="container">
<div class="row mt-1 mb-1 bg-light" *ngFor="let item of state.value; let i = index">
<div class="container ps-0 pe-0">
<div class="row ms-0 me-0 mt-1 mb-1 bg-light" *ngFor="let item of state.value; let i = index">
<div class="col ps-0">
<input type="text" class="form-control"
placeholder="Name"
@ -208,7 +208,9 @@
</select>
</div>
<div class="col">
<input type="password" class="form-control"
<input *ngIf="!item.encrypted"
type="password"
class="form-control"
[(ngModel)]="item.password"
(ngModelChange)="onChange($event)"
[name]="'item_p_'+idName+i"
@ -216,15 +218,16 @@
required>
</div>
<div class="col-1 pe-0">
<button [disabled]="state.value.length == 1" (click)="remove(i)"
[ngClass]="state.value.length > 1? 'btn-danger':'btn-secondary'"
<button [disabled]="(state.value.length == 1 && !state.tags.uiOptional)"
(click)="remove(i)"
[ngClass]="(state.value.length > 1 || state.tags.uiOptional)? 'btn-danger':'btn-secondary'"
class="btn float-end">
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
</button>
</div>
</div>
<div class="row">
<div class="row me-0">
<div class="col pe-0">
<button class="btn btn-primary float-end"
(click)="AddNew()" i18n>+ Add Link

View File

@ -316,6 +316,7 @@ export class SettingsEntryComponent
remove(i: number): void {
(this.state.value as unknown[]).splice(i, 1);
this.onChange(null);
}
/**

View File

@ -37,7 +37,7 @@
{{Name}} <span i18n>config is not supported with these settings.</span>
</div>
<button class="btn btn-success float-end"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
[disabled]="settingsForm.form.invalid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-end"

View File

@ -1,39 +1,31 @@
import {Component, Input, OnInit} from '@angular/core';
import {MapSettingsService} from '../map/map.settings.service';
import {TemplateSettingsService} from './template.settings.service';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
import {ClientMapConfig} from '../../../../../common/config/public/ClientConfig';
import {ServerConfig} from '../../../../../common/config/private/PrivateConfig';
import {SettingsService} from '../settings.service';
import {WebConfig} from '../../../../../common/config/private/WebConfig';
import {AbstractSettingsService} from '../_abstract/abstract.settings.service';
@Component({
selector: 'app-settings-template',
templateUrl: './template.component.html',
styleUrls: ['./template.component.css',
'../_abstract/abstract.settings.component.css'],
providers: [TemplateSettingsService],
'../_abstract/abstract.settings.component.css']
})
export class TemplateComponent extends SettingsComponentDirective<any> implements OnInit {
@Input() ConfigPath: keyof ServerConfig;
@Input() icon: string;
public configKeys: string[] = [];
constructor(
authService: AuthenticationService,
navigation: NavigationService,
settingsService: TemplateSettingsService,
notification: NotificationService,
settingsService: AbstractSettingsService,
globalSettingsService: SettingsService
) {
super(
`Template`,
'',
authService,
navigation,
settingsService,

View File

@ -1,23 +0,0 @@
import { Injectable } from '@angular/core';
import { NetworkService } from '../../../model/network/network.service';
import { SettingsService } from '../settings.service';
import { AbstractSettingsService } from '../_abstract/abstract.settings.service';
import { ClientMapConfig } from '../../../../../common/config/public/ClientConfig';
@Injectable()
export class TemplateSettingsService extends AbstractSettingsService<any> {
constructor(
private networkService: NetworkService,
settingsService: SettingsService
) {
super(settingsService);
}
hasAvailableSettings(): boolean {
return true;
}
public updateSettings(settings: ClientMapConfig): Promise<void> {
return this.networkService.putJson('/settings', { settings });
}
}