You've already forked pigallery2
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:
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user