diff --git a/frontend/src/app/models/forms/updateuser.control.ts b/frontend/src/app/models/forms/updateuser.control.ts index d172bd3..dc0b82f 100644 --- a/frontend/src/app/models/forms/updateuser.control.ts +++ b/frontend/src/app/models/forms/updateuser.control.ts @@ -1,5 +1,6 @@ import { FormControl } from '@angular/forms'; import Fuse from 'fuse.js'; +import { Permissions } from 'picsur-shared/dist/dto/permissions'; import { PermanentRolesList } from 'picsur-shared/dist/dto/roles.dto'; import { ERole } from 'picsur-shared/dist/entities/role.entity'; import { BehaviorSubject, Subscription } from 'rxjs'; @@ -71,6 +72,23 @@ export class UpdateUserControl { return true; } + public getEffectivePermissions(): Permissions { + const permissions: Permissions = []; + for (const role of this.selectedRoles) { + const fullRole = this.fullRoles.find((r) => r.name === role); + if (!fullRole) { + console.warn(`Role ${role} not found`); + continue; + } + + permissions.push( + ...fullRole.permissions.filter((p) => !permissions.includes(p)) + ); + } + + return permissions; + } + // Data interaction public putAllRoles(roles: ERole[]) { diff --git a/frontend/src/app/routes/settings/users/delete-confirm-dialog/delete-confirm-dialog.component.html b/frontend/src/app/routes/settings/users/delete-confirm-dialog/delete-confirm-dialog.component.html new file mode 100644 index 0000000..1a36fa3 --- /dev/null +++ b/frontend/src/app/routes/settings/users/delete-confirm-dialog/delete-confirm-dialog.component.html @@ -0,0 +1,15 @@ +
+
+

Are you sure you want to delete {{ data.username }}?

+
+
+
+
+ + +
+
diff --git a/frontend/src/app/routes/settings/users/delete-confirm-dialog/delete-confirm-dialog.component.scss b/frontend/src/app/routes/settings/users/delete-confirm-dialog/delete-confirm-dialog.component.scss new file mode 100644 index 0000000..b4f5f86 --- /dev/null +++ b/frontend/src/app/routes/settings/users/delete-confirm-dialog/delete-confirm-dialog.component.scss @@ -0,0 +1,3 @@ +.button-red { + background-color: #C62828; +} diff --git a/frontend/src/app/routes/settings/users/delete-confirm-dialog/delete-confirm-dialog.component.ts b/frontend/src/app/routes/settings/users/delete-confirm-dialog/delete-confirm-dialog.component.ts new file mode 100644 index 0000000..9a1020c --- /dev/null +++ b/frontend/src/app/routes/settings/users/delete-confirm-dialog/delete-confirm-dialog.component.ts @@ -0,0 +1,43 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { EUser } from 'picsur-shared/dist/entities/user.entity'; +import { HasFailed } from 'picsur-shared/dist/types'; +import { SnackBarType } from 'src/app/models/snack-bar-type'; +import { UserManageService } from 'src/app/services/api/usermanage.service'; +import { UtilService } from 'src/app/util/util.service'; + +@Component({ + selector: 'app-delete-confirm-dialog', + templateUrl: './delete-confirm-dialog.component.html', + styleUrls: ['./delete-confirm-dialog.component.scss'], +}) +export class DeleteConfirmDialogComponent implements OnInit { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: EUser, + private userManageService: UserManageService, + private utilService: UtilService + ) {} + + ngOnInit(): void { + console.log('Dialog', this.data); + } + + async onDelete() { + const result = await this.userManageService.deleteUser(this.data.username); + if (HasFailed(result)) { + this.utilService.showSnackBar( + 'Failed to delete user', + SnackBarType.Error + ); + } else { + this.utilService.showSnackBar('User deleted', SnackBarType.Success); + } + + this.dialogRef.close(); + } + + onCancel() { + this.dialogRef.close(); + } +} diff --git a/frontend/src/app/routes/settings/users/settings-users-edit/settings-users-edit.component.html b/frontend/src/app/routes/settings/users/settings-users-edit/settings-users-edit.component.html index 4e46833..3362dfb 100644 --- a/frontend/src/app/routes/settings/users/settings-users-edit/settings-users-edit.component.html +++ b/frontend/src/app/routes/settings/users/settings-users-edit/settings-users-edit.component.html @@ -24,9 +24,9 @@ name="username" required /> - {{ - model.usernameError - }} + + {{ model.usernameError }} + @@ -42,9 +42,9 @@ name="password" [required]="adding" /> - {{ - model.passwordError - }} + + {{ model.passwordError }} + @@ -57,6 +57,7 @@ {{ role }} @@ -91,7 +92,23 @@
-
+
+

Effective Permissions

+
+
+ + + {{ permission }} + + +
+
+ +
+
diff --git a/frontend/src/app/routes/settings/users/settings-users-edit/settings-users-edit.component.ts b/frontend/src/app/routes/settings/users/settings-users-edit/settings-users-edit.component.ts index 290d1a7..9a57aee 100644 --- a/frontend/src/app/routes/settings/users/settings-users-edit/settings-users-edit.component.ts +++ b/frontend/src/app/routes/settings/users/settings-users-edit/settings-users-edit.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit } from '@angular/core'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatChipInputEvent } from '@angular/material/chips'; import { ActivatedRoute, Router } from '@angular/router'; +import { UIFriendlyPermissions } from 'picsur-shared/dist/dto/permissions'; import { HasFailed } from 'picsur-shared/dist/types'; import { UpdateUserControl } from 'src/app/models/forms/updateuser.control'; import { SnackBarType } from 'src/app/models/snack-bar-type'; @@ -77,6 +78,12 @@ export class SettingsUsersEditComponent implements OnInit { this.model.putAllRoles(roles); } + getEffectivePermissions() { + return this.model + .getEffectivePermissions() + .map((permission) => UIFriendlyPermissions[permission]); + } + removeRole(role: string) { this.model.removeRole(role); } diff --git a/frontend/src/app/routes/settings/users/settings-users.component.html b/frontend/src/app/routes/settings/users/settings-users.component.html index b19b086..5f6b7bc 100644 --- a/frontend/src/app/routes/settings/users/settings-users.component.html +++ b/frontend/src/app/routes/settings/users/settings-users.component.html @@ -1,22 +1,31 @@

Users

- - Username {{ user.username }} + + Roles + + + + {{ role }} + + + + + Actions + diff --git a/frontend/src/app/routes/settings/users/settings-users.component.scss b/frontend/src/app/routes/settings/users/settings-users.component.scss index e187b16..0a95fb8 100644 --- a/frontend/src/app/routes/settings/users/settings-users.component.scss +++ b/frontend/src/app/routes/settings/users/settings-users.component.scss @@ -6,3 +6,6 @@ mat-table { justify-content: end; } +.icon-red { + color: #F44336; +} diff --git a/frontend/src/app/routes/settings/users/settings-users.component.ts b/frontend/src/app/routes/settings/users/settings-users.component.ts index e3ee7a5..c3efc60 100644 --- a/frontend/src/app/routes/settings/users/settings-users.component.ts +++ b/frontend/src/app/routes/settings/users/settings-users.component.ts @@ -1,4 +1,5 @@ import { Component, OnInit, ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; import { MatPaginator, PageEvent } from '@angular/material/paginator'; import { Router } from '@angular/router'; import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; @@ -8,13 +9,18 @@ import { BehaviorSubject, Subject, throttleTime } from 'rxjs'; import { SnackBarType } from 'src/app/models/snack-bar-type'; import { UserManageService } from 'src/app/services/api/usermanage.service'; import { UtilService } from 'src/app/util/util.service'; +import { DeleteConfirmDialogComponent } from './delete-confirm-dialog/delete-confirm-dialog.component'; @Component({ templateUrl: './settings-users.component.html', styleUrls: ['./settings-users.component.scss'], }) export class SettingsUsersComponent implements OnInit { - public readonly displayedColumns: string[] = [/*'id',*/ 'username', 'actions']; + public readonly displayedColumns: string[] = [ + 'username', + 'roles', + 'actions', + ]; public readonly pageSizeOptions: number[] = [5, 10, 25, 100]; public readonly startingPageSize = this.pageSizeOptions[2]; @@ -26,7 +32,8 @@ export class SettingsUsersComponent implements OnInit { constructor( private userManageService: UserManageService, private utilService: UtilService, - private router: Router + private router: Router, + private dialog: MatDialog ) {} async ngOnInit() { @@ -34,12 +41,28 @@ export class SettingsUsersComponent implements OnInit { this.fetchUsers(this.startingPageSize, 0); } + public addUser() { + this.router.navigate(['/settings/users/add']); + } + public editUser(user: EUser) { this.router.navigate(['/settings/users/edit', user.username]); } - public addUser() { - this.router.navigate(['/settings/users/add']); + public deleteUser(user: EUser) { + const dialogRef = this.dialog.open(DeleteConfirmDialogComponent, { + data: user, + }); + + dialogRef.afterClosed().subscribe(async () => { + const page = this.paginator.pageIndex; + const pageSize = this.paginator.pageSize; + + const success = await this.fetchUsers(pageSize, page); + if (!success) { + this.paginator.firstPage(); + } + }); } @AutoUnsubscribe() diff --git a/frontend/src/app/routes/settings/users/settings-users.module.ts b/frontend/src/app/routes/settings/users/settings-users.module.ts index 63f2ad0..f90e95c 100644 --- a/frontend/src/app/routes/settings/users/settings-users.module.ts +++ b/frontend/src/app/routes/settings/users/settings-users.module.ts @@ -4,17 +4,23 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; import { MatChipsModule } from '@angular/material/chips'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatPaginatorModule } from '@angular/material/paginator'; import { MatTableModule } from '@angular/material/table'; +import { DeleteConfirmDialogComponent } from './delete-confirm-dialog/delete-confirm-dialog.component'; import { SettingsUsersEditComponent } from './settings-users-edit/settings-users-edit.component'; import { SettingsUsersComponent } from './settings-users.component'; import { SettingsUsersRoutingModule } from './settings-users.routing.module'; @NgModule({ - declarations: [SettingsUsersComponent, SettingsUsersEditComponent], + declarations: [ + SettingsUsersComponent, + SettingsUsersEditComponent, + DeleteConfirmDialogComponent, + ], imports: [ CommonModule, SettingsUsersRoutingModule, @@ -26,6 +32,7 @@ import { SettingsUsersRoutingModule } from './settings-users.routing.module'; MatInputModule, MatChipsModule, MatAutocompleteModule, + MatDialogModule, FormsModule, ReactiveFormsModule, ], diff --git a/frontend/src/app/routes/user/login/login.component.html b/frontend/src/app/routes/user/login/login.component.html index bbc3583..fa1eae2 100644 --- a/frontend/src/app/routes/user/login/login.component.html +++ b/frontend/src/app/routes/user/login/login.component.html @@ -19,9 +19,9 @@ name="username" required /> - {{ - model.usernameError - }} + + {{ model.usernameError }} +
@@ -34,9 +34,9 @@ name="password" required /> - {{ - model.passwordError - }} + + {{ model.passwordError }} +
diff --git a/frontend/src/app/routes/user/register/register.component.html b/frontend/src/app/routes/user/register/register.component.html index eccda54..df13d6e 100644 --- a/frontend/src/app/routes/user/register/register.component.html +++ b/frontend/src/app/routes/user/register/register.component.html @@ -17,9 +17,9 @@ name="username" required /> - {{ - model.usernameError - }} + + {{ model.usernameError }} +
@@ -32,9 +32,9 @@ name="password" required /> - {{ - model.passwordError - }} + + {{ model.passwordError }} +
@@ -47,9 +47,9 @@ name="confirmpassword" required /> - {{ - model.passwordConfirmError - }} + + {{ model.passwordConfirmError }} +
diff --git a/frontend/src/app/services/api/usermanage.service.ts b/frontend/src/app/services/api/usermanage.service.ts index 22fc10b..691570b 100644 --- a/frontend/src/app/services/api/usermanage.service.ts +++ b/frontend/src/app/services/api/usermanage.service.ts @@ -2,6 +2,8 @@ import { Injectable } from '@angular/core'; import { UserCreateRequest, UserCreateResponse, + UserDeleteRequest, + UserDeleteResponse, UserInfoRequest, UserInfoResponse, UserListRequest, @@ -76,4 +78,19 @@ export class UserManageService { return result; } + + public async deleteUser(username: string): AsyncFailable { + const body = { + username, + }; + + const result = await this.apiService.post( + UserDeleteRequest, + UserDeleteResponse, + '/api/user/delete', + body + ); + + return result; + } } diff --git a/shared/src/dto/permissions.ts b/shared/src/dto/permissions.ts index 0d78e73..12fa406 100644 --- a/shared/src/dto/permissions.ts +++ b/shared/src/dto/permissions.ts @@ -27,3 +27,20 @@ export const AdminDashboardPermissions: Permissions = [ Permission.RoleManage, Permission.SysPrefManage, ]; + +export const UIFriendlyPermissions: { + [key in Permission]: string; +} = { + [Permission.ImageView]: 'View images', + [Permission.ImageUpload]: 'Upload images', + + [Permission.UserLogin]: 'Login', + [Permission.UserMe]: 'View own user details', + [Permission.UserRegister]: 'Register', + + [Permission.Settings]: 'View available settings', + + [Permission.UserManage]: 'Manage users', + [Permission.RoleManage]: 'Manage roles', + [Permission.SysPrefManage]: 'Manage system preferences', +}; diff --git a/yarn.lock b/yarn.lock index 89ff422..d2c0b87 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5499,17 +5499,7 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -minimist@^1.2.0, minimist@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== - -"minimist@npm:minimist-lite": +minimist@1.2.5, minimist@^1.2.0, minimist@^1.2.6, "minimist@npm:minimist-lite": version "2.2.1" resolved "https://registry.yarnpkg.com/minimist-lite/-/minimist-lite-2.2.1.tgz#abb71db2c9b454d7cf4496868c03e9802de9934d" integrity sha512-RSrWIRWGYoM2TDe102s7aIyeSipXMIXKb1fSHYx1tAbxAV0z4g2xR6ra3oPzkTqFb0EIUz1H3A/qvYYeDd+/qQ==