1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-12-21 01:22:08 +02:00

Add users panel to users #569

This commit is contained in:
Patrik J. Braun 2023-01-06 19:01:24 +01:00
parent 82bc7ab280
commit a6e60c7c19
9 changed files with 287 additions and 64 deletions

View File

@ -3,7 +3,7 @@ import * as path from 'path';
import { ConfigClass, ConfigClassBuilder } from 'typeconfig/node';
import { ConfigProperty, SubConfigClass } from 'typeconfig/common';
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class BenchmarksConfig {
@ConfigProperty()
bmScanDirectory: boolean = true;

View File

@ -91,7 +91,7 @@ export type videoResolutionType =
| 4320;
export type videoFormatType = 'mp4' | 'webm';
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class MySQLConfig {
@ConfigProperty({
envAlias: 'MYSQL_HOST',
@ -140,7 +140,7 @@ export class MySQLConfig {
password: string = '';
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class SQLiteConfig {
@ConfigProperty({
tags:
@ -153,7 +153,7 @@ export class SQLiteConfig {
DBFileName: string = 'sqlite.db';
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class UserConfig {
@ConfigProperty({
tags:
@ -216,7 +216,7 @@ export class UserConfig {
}
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerDataBaseConfig {
@ConfigProperty<DatabaseType, ServerConfig>({
type: DatabaseType,
@ -262,7 +262,7 @@ export class ServerDataBaseConfig {
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerUserConfig extends ClientUserConfig {
@ConfigProperty({
arrayType: UserConfig,
@ -273,13 +273,13 @@ export class ServerUserConfig extends ClientUserConfig {
uiOptional: true,
githubIssue: 575
} 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.`,
description: $localize`Creates these users in the DB during startup 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[] = [];
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerThumbnailConfig extends ClientThumbnailConfig {
@ConfigProperty({
tags:
@ -312,7 +312,7 @@ export class ServerThumbnailConfig extends ClientThumbnailConfig {
personFaceMargin: number = 0.6; // in ration [0-1]
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerGPXCompressingConfig extends ClientGPXCompressingConfig {
@ConfigProperty({
tags:
@ -350,7 +350,7 @@ export class ServerGPXCompressingConfig extends ClientGPXCompressingConfig {
minTimeDistance: number = 5000;
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerMetaFileConfig extends ClientMetaFileConfig {
@ConfigProperty({
tags:
@ -367,7 +367,7 @@ export class ServerMetaFileConfig extends ClientMetaFileConfig {
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerSharingConfig extends ClientSharingConfig {
@ConfigProperty({
type: 'unsignedInt',
@ -382,7 +382,7 @@ export class ServerSharingConfig extends ClientSharingConfig {
updateTimeout: number = 1000 * 60 * 5;
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerIndexingConfig {
@ConfigProperty({
type: 'unsignedInt',
@ -431,7 +431,7 @@ export class ServerIndexingConfig {
excludeFileList: string[] = [];
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerThreadingConfig {
@ConfigProperty({
tags:
@ -453,7 +453,7 @@ export class ServerThreadingConfig {
thumbnailThreads: number = 0; // if zero-> CPU count -1
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerDuplicatesConfig {
@ConfigProperty({
type: 'unsignedInt',
@ -467,7 +467,7 @@ export class ServerDuplicatesConfig {
listingLimit: number = 1000;
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerLogConfig {
@ConfigProperty({
type: LogLevel,
@ -497,13 +497,13 @@ export class ServerLogConfig {
logServerTiming: boolean = false;
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class NeverJobTriggerConfig implements NeverJobTrigger {
@ConfigProperty({type: JobTriggerType})
readonly type = JobTriggerType.never;
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ScheduledJobTriggerConfig implements ScheduledJobTrigger {
@ConfigProperty({type: JobTriggerType})
readonly type = JobTriggerType.scheduled;
@ -512,7 +512,7 @@ export class ScheduledJobTriggerConfig implements ScheduledJobTrigger {
time: number; // data time
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class PeriodicJobTriggerConfig implements PeriodicJobTrigger {
@ConfigProperty({type: JobTriggerType})
readonly type = JobTriggerType.periodic;
@ -522,7 +522,7 @@ export class PeriodicJobTriggerConfig implements PeriodicJobTrigger {
atTime: number | undefined = 0; // day time
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class AfterJobTriggerConfig implements AfterJobTrigger {
@ConfigProperty({type: JobTriggerType})
readonly type = JobTriggerType.after;
@ -534,7 +534,7 @@ export class AfterJobTriggerConfig implements AfterJobTrigger {
}
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class JobScheduleConfig implements JobScheduleDTO {
@ConfigProperty()
name: string;
@ -586,7 +586,7 @@ export class JobScheduleConfig implements JobScheduleDTO {
}
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerJobConfig {
@ConfigProperty({
type: 'unsignedInt',
@ -661,7 +661,7 @@ export class ServerJobConfig {
];
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class VideoTranscodingConfig {
@ConfigProperty({
type: 'unsignedInt',
@ -761,7 +761,7 @@ export class VideoTranscodingConfig {
customOptions: string[] = [];
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerVideoConfig extends ClientVideoConfig {
@ConfigProperty({
tags: {
@ -774,7 +774,7 @@ export class ServerVideoConfig extends ClientVideoConfig {
transcoding: VideoTranscodingConfig = new VideoTranscodingConfig();
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class PhotoConvertingConfig extends ClientPhotoConvertingConfig {
@ConfigProperty({
tags: {
@ -802,7 +802,7 @@ export class PhotoConvertingConfig extends ClientPhotoConvertingConfig {
resolution: videoResolutionType = 1080;
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerPhotoConfig extends ClientPhotoConfig {
@ConfigProperty({
tags: {
@ -813,7 +813,7 @@ export class ServerPhotoConfig extends ClientPhotoConfig {
Converting: PhotoConvertingConfig = new PhotoConvertingConfig();
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerPreviewConfig {
@ConfigProperty({
type: 'object',
@ -842,7 +842,7 @@ export class ServerPreviewConfig {
];
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerMediaConfig extends ClientMediaConfig {
@ConfigProperty({
tags: {
@ -914,7 +914,7 @@ export class ServerMediaConfig extends ClientMediaConfig {
Thumbnail: ServerThumbnailConfig = new ServerThumbnailConfig();
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerServiceConfig extends ClientServiceConfig {
@ConfigProperty({
arrayType: 'string',
@ -974,7 +974,7 @@ export class ServerServiceConfig extends ClientServiceConfig {
Log: ServerLogConfig = new ServerLogConfig();
}
@SubConfigClass()
@SubConfigClass({softReadonly: true})
export class ServerEnvironmentConfig {
@ConfigProperty({volatile: true})
upTime: string | undefined;
@ -988,7 +988,7 @@ export class ServerEnvironmentConfig {
isDocker: boolean | undefined;
}
@SubConfigClass<TAGS>()
@SubConfigClass<TAGS>({softReadonly: true})
export class ServerConfig extends ClientConfig {
@ConfigProperty({volatile: true})

View File

@ -13,7 +13,6 @@ if (typeof $localize === 'undefined') {
}
export enum MapProviders {
OpenStreetMap = 1,
Mapbox = 2,
@ -49,7 +48,7 @@ export type TAGS = {
}[]
};
@SubConfigClass<TAGS>({tags: {client: true}})
@SubConfigClass<TAGS>({tags: {client: true}, softReadonly: true})
export class AutoCompleteConfig {
@ConfigProperty({
tags:
@ -93,7 +92,7 @@ export class AutoCompleteConfig {
cacheTimeout: number = 1000 * 60 * 60;
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientSearchConfig {
@ConfigProperty({
tags:
@ -162,7 +161,7 @@ export class ClientSearchConfig {
listMetafiles: boolean = true;
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientAlbumConfig {
@ConfigProperty({
tags:
@ -174,7 +173,7 @@ export class ClientAlbumConfig {
enabled: boolean = true;
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientSharingConfig {
@ConfigProperty({
tags:
@ -196,7 +195,7 @@ export class ClientSharingConfig {
passwordProtected: boolean = true;
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientRandomPhotoConfig {
@ConfigProperty({
tags:
@ -209,7 +208,7 @@ export class ClientRandomPhotoConfig {
enabled: boolean = true;
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class MapLayers {
@ConfigProperty({
tags:
@ -230,7 +229,7 @@ export class MapLayers {
url: string = '';
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientMapConfig {
@ConfigProperty<boolean, ClientConfig, TAGS>({
onNewValue: (value, config) => {
@ -293,7 +292,7 @@ export class ClientMapConfig {
maxPreviewMarkers: number = 50;
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientThumbnailConfig {
@ConfigProperty({
type: 'unsignedInt', max: 100,
@ -351,7 +350,7 @@ export enum NavigationLinkTypes {
gallery = 1, faces, albums, search, url
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class NavigationLinkConfig {
@ConfigProperty({
type: NavigationLinkTypes,
@ -400,7 +399,7 @@ export class NavigationLinkConfig {
}
}
@SubConfigClass<TAGS>({tags: {client: true}})
@SubConfigClass<TAGS>({tags: {client: true}, softReadonly: true})
export class NavBarConfig {
@ConfigProperty({
tags: {
@ -427,7 +426,7 @@ export class NavBarConfig {
];
}
@SubConfigClass<TAGS>({tags: {client: true, priority: ConfigPriority.advanced}})
@SubConfigClass<TAGS>({tags: {client: true, priority: ConfigPriority.advanced}, softReadonly: true})
export class ClientGalleryConfig {
@ConfigProperty({
tags: {
@ -528,7 +527,7 @@ export class ClientGalleryConfig {
defaultSlideshowSpeed: number = 5;
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientVideoConfig {
@ConfigProperty({
tags: {
@ -562,7 +561,7 @@ export class ClientVideoConfig {
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientPhotoConvertingConfig {
@ConfigProperty({
tags: {
@ -584,7 +583,7 @@ export class ClientPhotoConvertingConfig {
loadFullImageOnZoom: boolean = true;
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientPhotoConfig {
@ConfigProperty({
tags: {
@ -605,7 +604,7 @@ export class ClientPhotoConfig {
supportedFormats: string[] = ['gif', 'jpeg', 'jpg', 'jpe', 'png', 'webp', 'svg'];
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientGPXCompressingConfig {
@ConfigProperty({
tags: {
@ -619,7 +618,7 @@ export class ClientGPXCompressingConfig {
enabled: boolean = true;
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientMediaConfig {
@ConfigProperty({
tags: {
@ -644,7 +643,7 @@ export class ClientMediaConfig {
Photo: ClientPhotoConfig = new ClientPhotoConfig();
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientMetaFileConfig {
@ConfigProperty({
tags: {
@ -693,7 +692,7 @@ export class ClientMetaFileConfig {
supportedFormats: string[] = ['gpx', 'pg2conf', 'md'];
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientFacesConfig {
@ConfigProperty({
tags: {
@ -728,7 +727,7 @@ export class ClientFacesConfig {
readAccessMinRole: UserRoles = UserRoles.User;
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientServiceConfig {
@ConfigProperty({
@ -783,7 +782,7 @@ export class ClientServiceConfig {
customHTMLHead: string = '';
}
@SubConfigClass({tags: {client: true}})
@SubConfigClass({tags: {client: true}, softReadonly: true})
export class ClientUserConfig {
@ConfigProperty<boolean, ClientConfig>({
@ -794,7 +793,6 @@ export class ClientUserConfig {
},
tags: {
name: $localize`Password protection`,
priority: ConfigPriority.advanced,
},
description: $localize`Enables user management with login to password protect the gallery.`,
})
@ -812,7 +810,7 @@ export class ClientUserConfig {
}
@SubConfigClass<TAGS>({tags: {client: true}})
@SubConfigClass<TAGS>({tags: {client: true}, softReadonly: true})
export class ClientConfig {
@ConfigProperty()

View File

@ -103,6 +103,7 @@ import {GalleryStatisticComponent} from './ui/settings/gallery-statistic/gallery
import { JobButtonComponent } from './ui/settings/workflow/button/job-button.settings.component';
import { JobProgressComponent } from './ui/settings/workflow/progress/job-progress.settings.component';
import {SettingsEntryComponent} from './ui/settings/template/settings-entry/settings-entry.component';
import { UsersComponent } from './ui/settings/users/users.component';
@Injectable()
export class MyHammerConfig extends HammerGestureConfig {
@ -236,6 +237,7 @@ Marker.prototype.options.icon = iconDefault;
StringifySearchQuery,
FileDTOToPathPipe,
PhotoFilterPipe,
UsersComponent,
],
providers: [
{provide: HTTP_INTERCEPTORS, useClass: CSRFInterceptor, multi: true},

View File

@ -76,17 +76,17 @@
<div class="py-md-1 px-md-0"
*ngFor="let s of contents;"
[hidden]="!s.HasAvailableSettings">
<button class="btn btn-link nav-link text-start p-0"
(click)="viewportScroller.scrollToAnchor(s.ConfigPath)"
>
<span class="oi oi-{{s.icon}}"></span> {{s.Name}}
</button>
<button class="btn btn-link nav-link text-start ms-3 p-0"
*ngFor="let n of s.nestedConfigs;"
[hidden]="!n.visible()"
(click)="viewportScroller.scrollToAnchor(n.id)">
<span class="oi oi-{{n.icon}}"></span> {{n.name}}
</button>
<button class="btn btn-link nav-link text-start p-0"
(click)="viewportScroller.scrollToAnchor(s.ConfigPath)"
>
<span class="oi oi-{{s.icon}}"></span> {{s.Name}}
</button>
<button class="btn btn-link nav-link text-start ms-3 p-0"
*ngFor="let n of s.nestedConfigs;"
[hidden]="!n.visible()"
(click)="viewportScroller.scrollToAnchor(n.id)">
<span class="oi oi-{{n.icon}}"></span> {{n.name}}
</button>
</div>
</div>
</div>
@ -107,6 +107,7 @@
<hr class="mt-2"/>
<app-settings-gallery-statistic></app-settings-gallery-statistic>
</ng-container>
<app-settings-users *ngIf="cp=='Users'"></app-settings-users>
</app-settings-template>
</div>
</div>

View File

@ -0,0 +1,81 @@
<ng-container *ngIf="Enabled">
<div class="row mt-2">
<div class="col-auto">
<h5 i18n>User list</h5>
</div>
<div class="col">
<hr/>
</div>
</div>
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<table class="table table-hover">
<thead>
<tr>
<th i18n>Name</th>
<th i18n>Role</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of users">
<td>{{user.name}}</td>
<td *ngIf="canModifyUser(user)">
<select class="form-select" [(ngModel)]="user.role" (change)="updateRole(user)" required>
<option *ngFor="let repository of userRoles" [value]="repository.key">
{{repository.value}}
</option>
</select>
</td>
<td *ngIf="!canModifyUser(user)">
{{user.role | stringifyRole}}
</td>
<td>
<button [disabled]="!canModifyUser(user)" (click)="deleteUser(user)"
[ngClass]="canModifyUser(user)? 'btn-danger':'btn-secondary'"
class="btn float-end">
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
</button>
</td>
</tr>
</tbody>
</table>
<button class="btn btn-primary float-end"
(click)="initNewUser()" i18n>+ Add user
</button>
</ng-container>
<!-- Modal -->
<div bsModal #userModal="bs-modal" class="modal fade" id="userModal" tabindex="-1" role="dialog"
aria-labelledby="userModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="userModalLabel" i18n>Add new User</h5>
<button type="button" class="btn-close" (click)="userModal.hide()" data-dismiss="modal" aria-label="Close">
</button>
</div>
<form #NewUserForm="ngForm">
<div class="modal-body">
<input type="text" class="form-control" i18n-placeholder placeholder="Username"
[(ngModel)]="newUser.name" name="name" required>
<input type="password" class="form-control" i18n-placeholder placeholder="Password"
[(ngModel)]="newUser.password" name="password" autocomplete="off" required>
<select class="form-select" [(ngModel)]="newUser.role" name="role" required>
<option *ngFor="let repository of userRoles" [value]="repository.key">{{repository.value}}
</option>
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" (click)="userModal.hide()" i18n>Close</button>
<button type="button" class="btn btn-primary" data-dismiss="modal"
(click)="addNewUser()"
[disabled]="!NewUserForm.form.valid" i18n>Add User
</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,110 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {ModalDirective} from 'ngx-bootstrap/modal';
import {UserDTO, UserRoles} from '../../../../../common/entities/UserDTO';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {Utils} from '../../../../../common/Utils';
import {ErrorCodes, ErrorDTO} from '../../../../../common/entities/Error';
import {UsersSettingsService} from './users.service';
import {SettingsService} from '../settings.service';
@Component({
selector: 'app-settings-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.css']
})
export class UsersComponent implements OnInit {
@ViewChild('userModal', {static: false}) public childModal: ModalDirective;
public newUser = {} as UserDTO;
public userRoles: { key: number; value: string }[] = [];
public users: UserDTO[] = [];
public error: string = null;
public inProgress = false;
Changed = false;
constructor(
private authService: AuthenticationService,
private navigation: NavigationService,
private userSettings: UsersSettingsService,
private settingsService: SettingsService,
private notification: NotificationService
) {
}
ngOnInit(): void {
if (
!this.authService.isAuthenticated() ||
this.authService.user.value.role < UserRoles.Admin
) {
this.navigation.toLogin();
return;
}
this.userRoles = Utils.enumToArray(UserRoles)
.filter((r) => r.key !== UserRoles.LimitedGuest)
.filter((r) => r.key <= this.authService.user.value.role)
.sort((a, b) => a.key - b.key);
this.getUsersList();
}
canModifyUser(user: UserDTO): boolean {
const currentUser = this.authService.user.value;
if (!currentUser) {
return false;
}
return currentUser.name !== user.name && currentUser.role >= user.role;
}
get Enabled():boolean{
return this.settingsService.settings.value.Users.authenticationRequired;
}
initNewUser(): void {
this.newUser = {role: UserRoles.User} as UserDTO;
this.childModal.show();
}
async addNewUser(): Promise<void> {
try {
await this.userSettings.createUser(this.newUser);
await this.getUsersList();
this.childModal.hide();
} catch (e) {
const err: ErrorDTO = e;
this.notification.error(
err.message + ', ' + err.details,
$localize`User creation error!`
);
}
}
async updateRole(user: UserDTO): Promise<void> {
await this.userSettings.updateRole(user);
await this.getUsersList();
this.childModal.hide();
}
async deleteUser(user: UserDTO): Promise<void> {
await this.userSettings.deleteUser(user);
await this.getUsersList();
this.childModal.hide();
}
private async getUsersList(): Promise<void> {
try {
this.users = await this.userSettings.getUsers();
} catch (err) {
this.users = [];
if ((err as ErrorDTO).code !== ErrorCodes.USER_MANAGEMENT_DISABLED) {
throw err;
}
}
}
}

View File

@ -0,0 +1,31 @@
import {Injectable} from '@angular/core';
import {UserDTO} from '../../../../../common/entities/UserDTO';
import {NetworkService} from '../../../model/network/network.service';
@Injectable({
providedIn: 'root'
})
export class UsersSettingsService {
constructor(private networkService: NetworkService) {
}
public createUser(user: UserDTO): Promise<string> {
return this.networkService.putJson('/user', {newUser: user});
}
public getUsers(): Promise<Array<UserDTO>> {
return this.networkService.getJson('/user/list');
}
public deleteUser(user: UserDTO): Promise<void> {
return this.networkService.deleteJson('/user/' + user.id);
}
public updateRole(user: UserDTO): Promise<void> {
return this.networkService.postJson('/user/' + user.id + '/role', {
newRole: user.role,
});
}
}