1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-12-03 23:00:25 +02:00

Implement settings for user allow/block query changes #1015

This commit is contained in:
Patrik J. Braun
2025-08-20 23:37:29 +02:00
parent 9386c51c3b
commit 2f1716a196
15 changed files with 515 additions and 220 deletions

View File

@@ -8,7 +8,7 @@
</div>
</div>
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{ error }}</div>
<table class="table table-hover">
<thead>
@@ -20,23 +20,21 @@
</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>{{ user.name }}</td>
<td>
{{ user.role | stringifyRole }}
</td>
<td>
<button [disabled]="!canModifyUser(user)" (click)="deleteUser(user)"
[ngClass]="canModifyUser(user)? 'btn-danger':'btn-secondary'"
class="btn float-end">
<button [disabled]="!canDeleteUser(user)" (click)="canDeleteUser(user)"
[ngClass]="canDeleteUser(user)? 'btn-danger':'btn-secondary'"
class="btn float-end ms-2">
<ng-icon name="ionTrashOutline" title="Delete" i18n-title></ng-icon>
</button>
<button [disabled]="!canModifyUser(user)" (click)="openEditUser(user)"
[ngClass]="canModifyUser(user)? 'btn-primary':'btn-secondary'"
class="btn float-end ms-2">
<ng-icon name="ionPencil" title="Edit" i18n-title></ng-icon>
</button>
</td>
</tr>
</tbody>
@@ -66,7 +64,7 @@
<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 *ngFor="let repository of userRoles" [value]="repository.key">{{ repository.value }}
</option>
</select>
</div>
@@ -81,3 +79,79 @@
</div>
</div>
</div>
<!-- Edit User Modal -->
<div bsModal #editUserModal="bs-modal" class="modal fade" id="editUserModal" tabindex="-1" role="dialog"
aria-labelledby="editUserModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editUserModalLabel">
<ng-container i18n>Edit user</ng-container>
<span class="ms-1">{{ editUser?.name }}</span>
</h5>
<button type="button" class="btn-close" (click)="editUserModal.hide()" data-dismiss="modal" aria-label="Close">
</button>
</div>
<form #EditUserForm="ngForm">
<div class="modal-body" *ngIf="editUser">
<div class="mb-3">
<label class="form-label" for="editRole" i18n>Role</label>
<select id="editRole" [disabled]="!canModifyRole(editOriginalUser)" class="form-select"
[(ngModel)]="editUser.role" name="editRole" required>
<option *ngFor="let repository of userRoles" [value]="repository.key">{{ repository.value }}</option>
</select>
<small class="text-muted" i18n>Controls the permissions of the user.</small>
</div>
<div class="mb-2">
<label class="form-label" for="newPassword" i18n>New password</label>
<input id="newPassword" type="password" class="form-control" i18n-placeholder
placeholder="Leave blank to keep password"
[(ngModel)]="newPassword" name="newPassword" autocomplete="off">
<small class="text-muted" i18n>Leave empty to keep the current password.</small>
</div>
<div class="form-check form-switch my-3">
<input class="form-check-input" type="checkbox" id="overrideAllowBlockList"
[(ngModel)]="editSettings.overrideAllowBlockList" name="overrideAllowBlockList">
<label class="form-check-label" for="overrideAllowBlockList" i18n>Override allow/block list</label>
<div><small class="text-muted" i18n>Enable to apply the queries below to limit what this user can
see.</small></div>
</div>
<div *ngIf="editSettings.overrideAllowBlockList">
<div class="mb-3">
<label class="form-label" i18n>Allowed query</label>
<app-gallery-search-field
name="allowQuery"
[(ngModel)]="editSettings.allowQuery"
i18n-placeholder
placeholder="Allowed query">
</app-gallery-search-field>
<small class="text-muted" i18n>Only items matching this query will be visible.</small>
</div>
<div class="mb-3">
<label class="form-label" i18n>Blocked query</label>
<app-gallery-search-field
name="blockQuery"
[(ngModel)]="editSettings.blockQuery"
placeholder="Blocked query"
i18n-placeholder>
</app-gallery-search-field>
<small class="text-muted" i18n>Items matching this query will be hidden.</small>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" (click)="editUserModal.hide()" i18n>Close</button>
<button type="button" class="btn btn-primary" data-dismiss="modal"
(click)="saveEditUser()">
<ng-container i18n>Save changes</ng-container>
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -8,49 +8,56 @@ import {Utils} from '../../../../../common/Utils';
import {ErrorCodes, ErrorDTO} from '../../../../../common/entities/Error';
import {UsersSettingsService} from './users.service';
import {SettingsService} from '../settings.service';
import { NgIf, NgFor, NgClass } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { NgIconComponent } from '@ng-icons/core';
import { StringifyRole } from '../../../pipes/StringifyRolePipe';
import {NgClass, NgFor, NgIf} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {NgIconComponent} from '@ng-icons/core';
import {StringifyRole} from '../../../pipes/StringifyRolePipe';
import {UserSettingsDTO} from '../../../../../common/entities/UserSettingsDTO';
import {SearchQueryDTO, SearchQueryTypes, TextSearch} from '../../../../../common/entities/SearchQueryDTO';
import {GallerySearchFieldComponent} from '../../gallery/search/search-field/search-field.gallery.component';
@Component({
selector: 'app-settings-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.css'],
imports: [NgIf, NgFor, FormsModule, NgClass, NgIconComponent, ModalDirective, StringifyRole]
selector: 'app-settings-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.css'],
imports: [NgIf, NgFor, FormsModule, NgClass, NgIconComponent, ModalDirective, StringifyRole, GallerySearchFieldComponent]
})
export class UsersComponent implements OnInit {
@ViewChild('userModal', {static: false}) public childModal: ModalDirective;
@ViewChild('editUserModal', {static: false}) public editModal: ModalDirective;
public newUser = {} as UserDTO;
public userRoles: { key: number; value: string }[] = [];
public users: UserDTO[] = [];
public error: string = null;
public inProgress = false;
Changed = false;
public editUser: UserDTO = null;
public editSettings: UserSettingsDTO = {};
public newPassword = '';
public editOriginalUser: UserDTO = null;
constructor(
private authService: AuthenticationService,
private navigation: NavigationService,
private userSettings: UsersSettingsService,
private settingsService: SettingsService,
private notification: NotificationService
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.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);
.filter((r) => r.key !== UserRoles.LimitedGuest)
.filter((r) => r.key <= this.authService.user.value.role)
.sort((a, b) => a.key - b.key);
this.getUsersList();
}
@@ -61,6 +68,24 @@ export class UsersComponent implements OnInit {
return false;
}
return currentUser.role >= user.role;
}
canDeleteUser(user: UserDTO): boolean {
const currentUser = this.authService.user.value;
if (!currentUser) {
return false;
}
return currentUser.name !== user.name && currentUser.role >= user.role;
}
canModifyRole(user: UserDTO): boolean {
const currentUser = this.authService.user.value;
if (!currentUser) {
return false;
}
return currentUser.name !== user.name && currentUser.role >= user.role;
}
@@ -82,8 +107,8 @@ export class UsersComponent implements OnInit {
} catch (e) {
const err: ErrorDTO = e;
this.notification.error(
err.message + ', ' + err.details,
$localize`User creation error!`
err.message + ', ' + err.details,
$localize`User creation error!`
);
}
}
@@ -100,6 +125,69 @@ export class UsersComponent implements OnInit {
this.childModal.hide();
}
async openEditUser(user: UserDTO): Promise<void> {
await this.getUsersList();
const fresh = this.users.find(u => u.id === user.id) || user;
this.editUser = {...fresh};
this.editOriginalUser = Utils.clone(this.editUser);
const defaultQuery: SearchQueryDTO = {type: SearchQueryTypes.any_text, text: ''} as TextSearch;
this.editSettings = {
overrideAllowBlockList: fresh.overrideAllowBlockList ?? false,
allowQuery: fresh.allowQuery ?? defaultQuery,
blockQuery: fresh.blockQuery ?? defaultQuery
} as UserSettingsDTO;
this.newPassword = '';
this.editModal.show();
}
private isEmptyQuery(q: SearchQueryDTO | null | undefined): boolean {
if (!q) {
return true;
}
return q.type === SearchQueryTypes.any_text && (q as TextSearch).text === '';
}
async saveEditUser(): Promise<void> {
try {
if (this.editSettings.overrideAllowBlockList) {
const allowEmpty = this.isEmptyQuery(this.editSettings.allowQuery);
const blockEmpty = this.isEmptyQuery(this.editSettings.blockQuery);
if (allowEmpty && blockEmpty) {
this.notification.error($localize`Please set at least one of Allowed or Blocked query`, $localize`Validation error`);
return;
}
}
// Save role if changed
if (this.editUser.role !== this.editOriginalUser?.role) {
if(!this.canModifyRole(this.editOriginalUser)){
this.notification.error($localize`Can't modify user role`);
return;
}
await this.userSettings.updateRole(this.editUser);
}
const settings: UserSettingsDTO = {
overrideAllowBlockList: this.editSettings.overrideAllowBlockList,
allowQuery: this.editSettings.overrideAllowBlockList ? this.editSettings.allowQuery : null,
blockQuery: this.editSettings.overrideAllowBlockList ? this.editSettings.blockQuery : null,
} as UserSettingsDTO;
if (this.newPassword && this.newPassword.length > 0) {
settings.newPassword = this.newPassword;
}
await this.userSettings.updateSettings(this.editUser.id, settings);
this.notification.success($localize`User settings saved successfully`);
await this.getUsersList();
this.editModal.hide();
} catch (e) {
const err: ErrorDTO = e;
this.notification.error(
(err?.message || '') + (err?.details ? ', ' + err.details : ''),
$localize`Could not save user settings`
);
}
}
private async getUsersList(): Promise<void> {
try {
this.users = await this.userSettings.getUsers();

View File

@@ -1,6 +1,7 @@
import {Injectable} from '@angular/core';
import {UserDTO} from '../../../../../common/entities/UserDTO';
import {NetworkService} from '../../../model/network/network.service';
import {UserSettingsDTO} from '../../../../../common/entities/UserSettingsDTO';
@Injectable({
providedIn: 'root'
@@ -28,4 +29,10 @@ export class UsersSettingsService {
newRole: user.role,
});
}
public updateSettings(userId: number, settings: UserSettingsDTO): Promise<void> {
return this.networkService.postJson('/user/' + userId + '/settings', {
settings
});
}
}

View File

@@ -44,7 +44,7 @@ import { FormsModule } from '@angular/forms';
import { provideAnimations } from '@angular/platform-browser/animations';
import { AppRoutingModule } from './app/app.routing';
import { NgIconsModule } from '@ng-icons/core';
import { ionDownloadOutline, ionFunnelOutline, ionGitBranchOutline, ionArrowDownOutline, ionArrowUpOutline, ionStarOutline, ionStar, ionCalendarOutline, ionPersonOutline, ionShuffleOutline, ionPeopleOutline, ionMenuOutline, ionShareSocialOutline, ionImagesOutline, ionLinkOutline, ionSearchOutline, ionHammerOutline, ionCopyOutline, ionAlbumsOutline, ionSettingsOutline, ionLogOutOutline, ionChevronForwardOutline, ionChevronDownOutline, ionChevronBackOutline, ionTrashOutline, ionSaveOutline, ionAddOutline, ionRemoveOutline, ionTextOutline, ionFolderOutline, ionDocumentOutline, ionDocumentTextOutline, ionImageOutline, ionPricetagOutline, ionLocationOutline, ionSunnyOutline, ionMoonOutline, ionVideocamOutline, ionInformationCircleOutline, ionInformationOutline, ionContractOutline, ionExpandOutline, ionCloseOutline, ionTimerOutline, ionPlayOutline, ionPauseOutline, ionVolumeMediumOutline, ionVolumeMuteOutline, ionCameraOutline, ionWarningOutline, ionLockClosedOutline, ionChevronUpOutline, ionFlagOutline, ionGlobeOutline, ionPieChartOutline, ionStopOutline, ionTimeOutline, ionCheckmarkOutline, ionPulseOutline, ionResizeOutline, ionCloudOutline, ionChatboxOutline, ionServerOutline, ionFileTrayFullOutline, ionBrushOutline, ionBrowsersOutline, ionUnlinkOutline, ionSquareOutline, ionGridOutline, ionAppsOutline, ionOpenOutline, ionRefresh, ionExtensionPuzzleOutline, ionList } from '@ng-icons/ionicons';
import { ionDownloadOutline, ionFunnelOutline, ionGitBranchOutline, ionArrowDownOutline, ionArrowUpOutline, ionStarOutline, ionStar, ionCalendarOutline, ionPersonOutline, ionShuffleOutline, ionPeopleOutline, ionMenuOutline, ionShareSocialOutline, ionImagesOutline, ionLinkOutline, ionSearchOutline, ionHammerOutline, ionCopyOutline, ionAlbumsOutline, ionSettingsOutline, ionLogOutOutline, ionChevronForwardOutline, ionChevronDownOutline, ionChevronBackOutline, ionTrashOutline, ionSaveOutline, ionAddOutline, ionRemoveOutline, ionTextOutline, ionFolderOutline, ionDocumentOutline, ionDocumentTextOutline, ionImageOutline, ionPricetagOutline, ionLocationOutline, ionSunnyOutline, ionMoonOutline, ionVideocamOutline, ionInformationCircleOutline, ionInformationOutline, ionContractOutline, ionExpandOutline, ionCloseOutline, ionTimerOutline, ionPlayOutline, ionPauseOutline, ionVolumeMediumOutline, ionVolumeMuteOutline, ionCameraOutline, ionWarningOutline, ionLockClosedOutline, ionChevronUpOutline, ionFlagOutline, ionGlobeOutline, ionPieChartOutline, ionStopOutline, ionTimeOutline, ionCheckmarkOutline, ionPulseOutline, ionResizeOutline, ionCloudOutline, ionChatboxOutline, ionServerOutline, ionFileTrayFullOutline, ionBrushOutline, ionBrowsersOutline, ionUnlinkOutline, ionSquareOutline, ionGridOutline, ionAppsOutline, ionOpenOutline, ionRefresh, ionExtensionPuzzleOutline, ionList, ionPencil } from '@ng-icons/ionicons';
import { ClipboardModule } from 'ngx-clipboard';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { ToastrModule } from 'ngx-toastr';
@@ -123,7 +123,7 @@ bootstrapApplication(AppComponent, {
ionTimeOutline, ionCheckmarkOutline, ionPulseOutline, ionResizeOutline,
ionCloudOutline, ionChatboxOutline, ionServerOutline, ionFileTrayFullOutline, ionBrushOutline,
ionBrowsersOutline, ionUnlinkOutline, ionSquareOutline, ionGridOutline,
ionAppsOutline, ionOpenOutline, ionRefresh, ionExtensionPuzzleOutline, ionList
ionAppsOutline, ionOpenOutline, ionRefresh, ionExtensionPuzzleOutline, ionList, ionPencil
}), ClipboardModule, TooltipModule.forRoot(), ToastrModule.forRoot(), ModalModule.forRoot(), CollapseModule.forRoot(), PopoverModule.forRoot(), BsDropdownModule.forRoot(), BsDatepickerModule.forRoot(), TimepickerModule.forRoot(), LoadingBarModule, LeafletModule, LeafletMarkerClusterModule, MarkdownModule.forRoot({ loader: HttpClient })),
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
{ provide: UrlSerializer, useClass: CustomUrlSerializer },