diff --git a/backend/src/collections/userdb/userdb.service.ts b/backend/src/collections/userdb/userdb.service.ts index 588af20..5d92f9c 100644 --- a/backend/src/collections/userdb/userdb.service.ts +++ b/backend/src/collections/userdb/userdb.service.ts @@ -2,11 +2,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import * as bcrypt from 'bcrypt'; import { plainToClass } from 'class-transformer'; -import { - LockedLoginUsersList, - LockedPermsUsersList, - SystemUsersList -} from 'picsur-shared/dist/dto/specialusers.dto'; import { AsyncFailable, Fail, @@ -19,6 +14,7 @@ import { DefaultRolesList, SoulBoundRolesList } from '../../models/dto/roles.dto'; +import { ImmutableUsersList, LockedLoginUsersList, UndeletableUsersList } from '../../models/dto/specialusers.dto'; import { EUserBackend } from '../../models/entities/user.entity'; import { GetCols } from '../collectionutils'; import { RolesService } from '../roledb/roledb.service'; @@ -74,7 +70,7 @@ export class UsersService { const userToModify = await this.resolve(user); if (HasFailed(userToModify)) return userToModify; - if (SystemUsersList.includes(userToModify.username)) { + if (UndeletableUsersList.includes(userToModify.username)) { return Fail('Cannot delete system user'); } @@ -94,7 +90,7 @@ export class UsersService { const userToModify = await this.resolve(user); if (HasFailed(userToModify)) return userToModify; - if (LockedPermsUsersList.includes(userToModify.username)) { + if (ImmutableUsersList.includes(userToModify.username)) { // Just fail silently return userToModify; } diff --git a/shared/src/dto/specialusers.dto.ts b/backend/src/models/dto/specialusers.dto.ts similarity index 54% rename from shared/src/dto/specialusers.dto.ts rename to backend/src/models/dto/specialusers.dto.ts index 49b1e31..87cdac5 100644 --- a/shared/src/dto/specialusers.dto.ts +++ b/backend/src/models/dto/specialusers.dto.ts @@ -1,8 +1,8 @@ // Cannot be deleted -export const SystemUsersList = ['guest', 'admin']; +export const UndeletableUsersList = ['guest', 'admin']; // Cannot have different permissions -export const LockedPermsUsersList = ['admin']; +export const ImmutableUsersList = ['admin']; // Cannot login export const LockedLoginUsersList = ['guest']; diff --git a/backend/src/routes/api/user/usermanage.controller.ts b/backend/src/routes/api/user/usermanage.controller.ts index 9e1adf7..54d2b99 100644 --- a/backend/src/routes/api/user/usermanage.controller.ts +++ b/backend/src/routes/api/user/usermanage.controller.ts @@ -6,7 +6,9 @@ import { Logger, Post } from '@nestjs/common'; +import { plainToClass } from 'class-transformer'; import { + GetSpecialUsersResponse, UserCreateRequest, UserCreateResponse, UserDeleteRequest, @@ -22,6 +24,7 @@ import { Permission } from 'picsur-shared/dist/dto/permissions'; import { HasFailed } from 'picsur-shared/dist/types'; import { UsersService } from '../../../collections/userdb/userdb.service'; import { RequiredPermissions } from '../../../decorators/permissions.decorator'; +import { ImmutableUsersList, LockedLoginUsersList, UndeletableUsersList } from '../../../models/dto/specialusers.dto'; @Controller('api/user') @RequiredPermissions(Permission.UserManage) @@ -125,4 +128,15 @@ export class UserManageController { return user; } + + @Get('special') + async getSpecial(): Promise { + const result: GetSpecialUsersResponse = { + ImmutableUsersList, + LockedLoginUsersList, + UndeletableUsersList, + }; + + return plainToClass(GetSpecialUsersResponse, result); + } } 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 eec92f6..7ffd934 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 @@ -4,7 +4,6 @@ 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 { LockedPermsUsersList } from 'picsur-shared/dist/dto/specialusers.dto'; 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'; @@ -23,6 +22,8 @@ enum EditMode { styleUrls: ['./settings-users-edit.component.scss'], }) export class SettingsUsersEditComponent implements OnInit { + private ImmutableUsersList: string[] = []; + readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE]; private mode: EditMode = EditMode.edit; @@ -73,6 +74,10 @@ export class SettingsUsersEditComponent implements OnInit { this.model.putUsername(user.username); this.model.putRoles(user.roles); + + const { ImmutableUsersList } = + await this.userManageService.getSpecialRolesOptimistic(); + this.ImmutableUsersList = ImmutableUsersList; } private async initRoles() { @@ -148,7 +153,7 @@ export class SettingsUsersEditComponent implements OnInit { if (this.adding) { return false; } else { - return LockedPermsUsersList.includes(this.model.getData().username); + return this.ImmutableUsersList.includes(this.model.getData().username); } } } 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 9795a2d..d2c0094 100644 --- a/frontend/src/app/routes/settings/users/settings-users.component.ts +++ b/frontend/src/app/routes/settings/users/settings-users.component.ts @@ -2,7 +2,6 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { MatPaginator, PageEvent } from '@angular/material/paginator'; import { Router } from '@angular/router'; import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; -import { SystemUsersList } from 'picsur-shared/dist/dto/specialusers.dto'; import { EUser } from 'picsur-shared/dist/entities/user.entity'; import { HasFailed } from 'picsur-shared/dist/types'; import { BehaviorSubject, Subject, throttleTime } from 'rxjs'; @@ -15,6 +14,8 @@ import { UtilService } from 'src/app/util/util.service'; styleUrls: ['./settings-users.component.scss'], }) export class SettingsUsersComponent implements OnInit { + private UndeletableUsersList: string[] = []; + public readonly displayedColumns: string[] = ['username', 'roles', 'actions']; public readonly pageSizeOptions: number[] = [5, 10, 25, 100]; public readonly startingPageSize = this.pageSizeOptions[2]; @@ -31,9 +32,13 @@ export class SettingsUsersComponent implements OnInit { private router: Router ) {} - async ngOnInit() { + ngOnInit() { this.subscribeToUpdate(); - this.fetchUsers(this.startingPageSize, 0); + + Promise.all([ + this.fetchUsers(this.startingPageSize, 0), + this.initSpecialUsers(), + ]).catch(console.error); } public addUser() { @@ -123,7 +128,20 @@ export class SettingsUsersComponent implements OnInit { return false; } + private async initSpecialUsers() { + const specialUsers = await this.userManageService.getSpecialUsers(); + if (HasFailed(specialUsers)) { + this.utilService.showSnackBar( + 'Failed to fetch special users', + SnackBarType.Error + ); + return; + } + + this.UndeletableUsersList = specialUsers.UndeletableUsersList; + } + isSystem(user: EUser): boolean { - return SystemUsersList.includes(user.username); + return this.UndeletableUsersList.includes(user.username); } } diff --git a/frontend/src/app/services/api/usermanage.service.ts b/frontend/src/app/services/api/usermanage.service.ts index 691570b..0ac335e 100644 --- a/frontend/src/app/services/api/usermanage.service.ts +++ b/frontend/src/app/services/api/usermanage.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { + GetSpecialUsersResponse, UserCreateRequest, UserCreateResponse, UserDeleteRequest, @@ -15,12 +16,16 @@ import { EUser } from 'picsur-shared/dist/entities/user.entity'; import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types'; import { FullUserModel } from 'src/app/models/forms/fulluser.model'; import { ApiService } from './api.service'; +import { CacheService } from './cache.service'; @Injectable({ providedIn: 'root', }) export class UserManageService { - constructor(private apiService: ApiService) {} + constructor( + private apiService: ApiService, + private cacheService: CacheService + ) {} public async getUser(username: string): AsyncFailable { const body = { @@ -93,4 +98,38 @@ export class UserManageService { return result; } + + public async getSpecialUsers(): AsyncFailable { + const cached = + this.cacheService.get('specialUsers'); + if (cached !== null) { + return cached; + } + + const result = await this.apiService.get( + GetSpecialUsersResponse, + '/api/user/special' + ); + + if (HasFailed(result)) { + return result; + } + + this.cacheService.set('specialRoles', result); + + return result; + } + + public async getSpecialRolesOptimistic(): Promise { + const result = await this.getSpecialUsers(); + if (HasFailed(result)) { + return { + ImmutableUsersList: [], + LockedLoginUsersList: [], + UndeletableUsersList: [], + }; + } + + return result; + } } diff --git a/shared/src/dto/api/usermanage.dto.ts b/shared/src/dto/api/usermanage.dto.ts index a020198..29e7b31 100644 --- a/shared/src/dto/api/usermanage.dto.ts +++ b/shared/src/dto/api/usermanage.dto.ts @@ -2,7 +2,8 @@ import { Type } from 'class-transformer'; import { IsArray, IsDefined, - IsOptional, ValidateNested + IsOptional, + ValidateNested } from 'class-validator'; import { EUser, NamePassUser, UsernameUser } from '../../entities/user.entity'; import { IsPosInt } from '../../validators/positive-int.validator'; @@ -60,3 +61,18 @@ export class UserUpdateRequest extends UsernameUser { } export class UserUpdateResponse extends EUser {} + +// GetSpecialUsers +export class GetSpecialUsersResponse { + @IsDefined() + @IsStringList() + UndeletableUsersList: string[]; + + @IsDefined() + @IsStringList() + ImmutableUsersList: string[]; + + @IsDefined() + @IsStringList() + LockedLoginUsersList: string[]; +}