diff --git a/README.md b/README.md index 86edcc0..e89691a 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,6 @@ services: # PICSUR_DB_PASSWORD: picsur # PICSUR_DB_NAME: picsur - # PICSUR_ADMIN_USERNAME: picsur # PICSUR_ADMIN_PASSWORD: picsur # PICSUR_JWT_SECRET: CHANGE_ME diff --git a/backend/src/collections/userdb/userdb.module.ts b/backend/src/collections/userdb/userdb.module.ts index 911ab38..f66af90 100644 --- a/backend/src/collections/userdb/userdb.module.ts +++ b/backend/src/collections/userdb/userdb.module.ts @@ -1,6 +1,7 @@ import { Logger, Module, OnModuleInit } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { HasFailed } from 'picsur-shared/dist/types'; +import { generateRandomString } from 'picsur-shared/dist/util/random'; import { AuthConfigService } from '../../config/auth.config.service'; import { PicsurConfigModule } from '../../config/config.module'; import { EUserBackend } from '../../models/entities/user.entity'; @@ -27,29 +28,49 @@ export class UsersModule implements OnModuleInit { ) {} async onModuleInit() { + await this.ensureGuestExists(); await this.ensureAdminExists(); } - private async ensureAdminExists() { - const username = this.authConfigService.getDefaultAdminUsername(); - const password = this.authConfigService.getDefaultAdminPassword(); - this.logger.debug(`Ensuring admin user "${username}" exists`); + private async ensureGuestExists() { + const username = 'guest'; + const password = generateRandomString(128); + this.logger.debug(`Ensuring guest user exists`); const exists = await this.usersService.exists(username); if (exists) return; - const newUser = await this.usersService.create(username, password); + const newUser = await this.usersService.create( + username, + password, + ['guest'], + true, + ); if (HasFailed(newUser)) { this.logger.error( - `Failed to create admin user "${username}" because: ${newUser.getReason()}`, + `Failed to create guest user because: ${newUser.getReason()}`, ); return; } + } - const result = await this.userRolesService.addRoles(newUser, ['admin']); - if (HasFailed(result)) { + private async ensureAdminExists() { + const username = 'admin'; + const password = this.authConfigService.getDefaultAdminPassword(); + this.logger.debug(`Ensuring admin user exists`); + + const exists = await this.usersService.exists(username); + if (exists) return; + + const newUser = await this.usersService.create( + username, + password, + ['user', 'admin'], + true, + ); + if (HasFailed(newUser)) { this.logger.error( - `Failed to make admin user "${username}" because: ${result.getReason()}`, + `Failed to create admin user because: ${newUser.getReason()}`, ); return; } diff --git a/backend/src/collections/userdb/userdb.service.ts b/backend/src/collections/userdb/userdb.service.ts index 33ff0e1..63fdcc7 100644 --- a/backend/src/collections/userdb/userdb.service.ts +++ b/backend/src/collections/userdb/userdb.service.ts @@ -2,7 +2,16 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import * as bcrypt from 'bcrypt'; import { plainToClass } from 'class-transformer'; -import { PermanentRolesList, Roles } from 'picsur-shared/dist/dto/roles.dto'; +import { + DefaultRolesList, + PermanentRolesList, + Roles +} from 'picsur-shared/dist/dto/roles.dto'; +import { + LockedLoginUsersList, + LockedPermsUsersList, + SystemUsersList +} from 'picsur-shared/dist/dto/specialusers.dto'; import { AsyncFailable, Fail, @@ -33,6 +42,7 @@ export class UsersService { username: string, password: string, roles?: Roles, + byPassRoleCheck?: boolean, ): AsyncFailable { if (await this.exists(username)) return Fail('User already exists'); @@ -41,7 +51,13 @@ export class UsersService { let user = new EUserBackend(); user.username = username; user.password = hashedPassword; - user.roles = ['user', ...(roles || [])]; + if (byPassRoleCheck) { + const rolesToAdd = roles ?? []; + user.roles = [...new Set([...rolesToAdd])]; + } else { + const rolesToAdd = this.filterAddedRoles(roles ?? []); + user.roles = [...new Set([...DefaultRolesList, ...rolesToAdd])]; + } try { user = await this.usersRepository.save(user, { reload: true }); @@ -59,6 +75,10 @@ export class UsersService { const userToModify = await this.resolve(user); if (HasFailed(userToModify)) return userToModify; + if (SystemUsersList.includes(userToModify.username)) { + return Fail('Cannot delete system user'); + } + try { return await this.usersRepository.remove(userToModify); } catch (e: any) { @@ -75,12 +95,15 @@ export class UsersService { const userToModify = await this.resolve(user); if (HasFailed(userToModify)) return userToModify; + if (LockedPermsUsersList.includes(userToModify.username)) { + // Just fail silently + return userToModify; + } + const rolesToKeep = userToModify.roles.filter((role) => PermanentRolesList.includes(role), ); - const rolesToAdd = roles.filter( - (role) => !PermanentRolesList.includes(role), - ); + const rolesToAdd = this.filterAddedRoles(roles); const newRoles = [...new Set([...rolesToKeep, ...rolesToAdd])]; @@ -121,6 +144,10 @@ export class UsersService { const user = await this.findOne(username, true); if (HasFailed(user)) return user; + if (LockedLoginUsersList.includes(user.username)) { + return Fail('Wrong password'); + } + if (!(await bcrypt.compare(password, user.password))) return Fail('Wrong password'); @@ -188,4 +215,12 @@ export class UsersService { return user; } } + + private filterAddedRoles(roles: Roles): Roles { + const filteredRoles = roles.filter( + (role) => !PermanentRolesList.includes(role), + ); + + return filteredRoles; + } } diff --git a/backend/src/config/auth.config.service.ts b/backend/src/config/auth.config.service.ts index 705826e..d56e53a 100644 --- a/backend/src/config/auth.config.service.ts +++ b/backend/src/config/auth.config.service.ts @@ -1,15 +1,12 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { EnvPrefix } from './config.static'; @Injectable() export class AuthConfigService { constructor(private configService: ConfigService) {} public getDefaultAdminPassword(): string { - return this.configService.get('DEFAULT_ADMIN_PASSWORD', 'admin'); - } - - public getDefaultAdminUsername(): string { - return this.configService.get('DEFAULT_ADMIN_USERNAME', 'admin'); + return this.configService.get(`${EnvPrefix}ADMIN_PASSWORD`, 'admin'); } } diff --git a/backend/src/config/jwt.config.service.ts b/backend/src/config/jwt.config.service.ts index 10baf5c..b8a0019 100644 --- a/backend/src/config/jwt.config.service.ts +++ b/backend/src/config/jwt.config.service.ts @@ -1,15 +1,16 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { EnvPrefix } from './config.static'; @Injectable() export class EnvJwtConfigService { constructor(private configService: ConfigService) {} public getJwtSecret(): string | undefined { - return this.configService.get('JWT_SECRET'); + return this.configService.get(`${EnvPrefix}JWT_SECRET`); } public getJwtExpiresIn(): string | undefined { - return this.configService.get('JWT_EXPIRES_IN'); + return this.configService.get(`${EnvPrefix}JWT_EXPIRY`); } } diff --git a/backend/src/managers/auth/guards/guest.strategy.ts b/backend/src/managers/auth/guards/guest.strategy.ts index a4b1ce0..212cb9a 100644 --- a/backend/src/managers/auth/guards/guest.strategy.ts +++ b/backend/src/managers/auth/guards/guest.strategy.ts @@ -37,6 +37,6 @@ export class GuestStrategy extends PassportStrategy( } override async validate(payload: any) { - return this.guestService.createGuest(); + return await this.guestService.getGuestUser(); } } diff --git a/backend/src/managers/auth/guest.service.ts b/backend/src/managers/auth/guest.service.ts index 12ca7f1..73fac9e 100644 --- a/backend/src/managers/auth/guest.service.ts +++ b/backend/src/managers/auth/guest.service.ts @@ -1,13 +1,24 @@ import { Injectable } from '@nestjs/common'; +import { HasFailed } from 'picsur-shared/dist/types'; +import { UsersService } from '../../collections/userdb/userdb.service'; import { EUserBackend } from '../../models/entities/user.entity'; @Injectable() export class GuestService { - public createGuest(): EUserBackend { - const guest = new EUserBackend(); - guest.roles = ['guest']; - guest.username = 'guest'; + private fallBackUser: EUserBackend; - return guest; + constructor(private usersService: UsersService) { + this.fallBackUser = new EUserBackend(); + this.fallBackUser.roles = ['guest']; + this.fallBackUser.username = 'guest'; + } + + public async getGuestUser(): Promise { + const user = await this.usersService.findOne('guest'); + if (HasFailed(user)) { + return this.fallBackUser; + } + + return user; } } 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 3362dfb..0f5ee46 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 @@ -49,7 +49,7 @@ -
+
Roles @@ -112,6 +112,10 @@ + +
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 9a57aee..f252f81 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,6 +4,8 @@ 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 { DefaultRolesList } from 'picsur-shared/dist/dto/roles.dto'; +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'; @@ -52,6 +54,7 @@ export class SettingsUsersEditComponent implements OnInit { const username = this.route.snapshot.paramMap.get('username'); if (!username) { this.mode = EditMode.add; + this.model.putRoles(DefaultRolesList); return; } @@ -97,6 +100,10 @@ export class SettingsUsersEditComponent implements OnInit { this.model.addRole(event.option.viewValue); } + cancel() { + this.router.navigate(['/settings/users']); + } + async updateUser() { const data = this.model.getData(); @@ -132,4 +139,12 @@ export class SettingsUsersEditComponent implements OnInit { this.router.navigate(['/settings/users']); } + + isLockedPerms(): boolean { + if (this.adding) { + return false; + } else { + return LockedPermsUsersList.includes(this.model.getData().username); + } + } } 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 5f6b7bc..e7bd286 100644 --- a/frontend/src/app/routes/settings/users/settings-users.component.html +++ b/frontend/src/app/routes/settings/users/settings-users.component.html @@ -10,6 +10,9 @@ Roles + + System + {{ role }} @@ -23,7 +26,11 @@ - 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 0a95fb8..a70f43f 100644 --- a/frontend/src/app/routes/settings/users/settings-users.component.scss +++ b/frontend/src/app/routes/settings/users/settings-users.component.scss @@ -7,5 +7,10 @@ mat-table { } .icon-red { - color: #F44336; + color: #f44336; +} + +mat-chip-list { + margin-top: 8px; + margin-bottom: 8px; } 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 c3efc60..afb0ce0 100644 --- a/frontend/src/app/routes/settings/users/settings-users.component.ts +++ b/frontend/src/app/routes/settings/users/settings-users.component.ts @@ -3,6 +3,7 @@ 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'; +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'; @@ -16,11 +17,7 @@ import { DeleteConfirmDialogComponent } from './delete-confirm-dialog/delete-con styleUrls: ['./settings-users.component.scss'], }) export class SettingsUsersComponent implements OnInit { - public readonly displayedColumns: string[] = [ - 'username', - 'roles', - 'actions', - ]; + public readonly displayedColumns: string[] = ['username', 'roles', 'actions']; public readonly pageSizeOptions: number[] = [5, 10, 25, 100]; public readonly startingPageSize = this.pageSizeOptions[2]; @@ -104,4 +101,8 @@ export class SettingsUsersComponent implements OnInit { return false; } + + isSystem(user: EUser): boolean { + return SystemUsersList.includes(user.username); + } } diff --git a/shared/src/dto/roles.dto.ts b/shared/src/dto/roles.dto.ts index 920f7fd..1611c2d 100644 --- a/shared/src/dto/roles.dto.ts +++ b/shared/src/dto/roles.dto.ts @@ -9,6 +9,8 @@ const PermanentRolesTuple = tuple('guest', 'user'); const ImmuteableRolesTuple = tuple('admin'); // These roles can never be removed from the server const SystemRolesTuple = tuple(...PermanentRolesTuple, ...ImmuteableRolesTuple); +// These roles will be applied by default to new users +export const DefaultRolesList: string[] = ['user']; // Derivatives @@ -16,6 +18,7 @@ export const PermanentRolesList: string[] = PermanentRolesTuple; export const ImmuteableRolesList: string[] = ImmuteableRolesTuple; export const SystemRolesList: string[] = SystemRolesTuple; + export type SystemRole = typeof SystemRolesTuple[number]; export type SystemRoles = SystemRole[]; diff --git a/shared/src/dto/specialusers.dto.ts b/shared/src/dto/specialusers.dto.ts new file mode 100644 index 0000000..49b1e31 --- /dev/null +++ b/shared/src/dto/specialusers.dto.ts @@ -0,0 +1,8 @@ +// Cannot be deleted +export const SystemUsersList = ['guest', 'admin']; + +// Cannot have different permissions +export const LockedPermsUsersList = ['admin']; + +// Cannot login +export const LockedLoginUsersList = ['guest']; diff --git a/shared/src/entities/user.entity.ts b/shared/src/entities/user.entity.ts index dcd146a..a4075f8 100644 --- a/shared/src/entities/user.entity.ts +++ b/shared/src/entities/user.entity.ts @@ -1,8 +1,5 @@ import { Exclude } from 'class-transformer'; -import { - IsArray, IsOptional, - IsString -} from 'class-validator'; +import { IsArray, IsOptional, IsString } from 'class-validator'; import { Roles } from '../dto/roles.dto'; import { EntityID } from '../validators/entity-id.validator'; import { IsPlainTextPwd, IsUsername } from '../validators/user.validators'; diff --git a/support/picsur.docker-compose.yml b/support/picsur.docker-compose.yml index a207484..9fd66ec 100644 --- a/support/picsur.docker-compose.yml +++ b/support/picsur.docker-compose.yml @@ -15,7 +15,6 @@ services: # PICSUR_DB_PASSWORD: picsur # PICSUR_DB_NAME: picsur - # PICSUR_ADMIN_USERNAME: picsur # PICSUR_ADMIN_PASSWORD: picsur # PICSUR_JWT_SECRET: CHANGE_ME