add better role indications
This commit is contained in:
parent
860476ecd4
commit
6b1b973a4e
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<EUserBackend> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<string>('DEFAULT_ADMIN_PASSWORD', 'admin');
|
||||
}
|
||||
|
||||
public getDefaultAdminUsername(): string {
|
||||
return this.configService.get<string>('DEFAULT_ADMIN_USERNAME', 'admin');
|
||||
return this.configService.get<string>(`${EnvPrefix}ADMIN_PASSWORD`, 'admin');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<string>('JWT_SECRET');
|
||||
return this.configService.get<string>(`${EnvPrefix}JWT_SECRET`);
|
||||
}
|
||||
|
||||
public getJwtExpiresIn(): string | undefined {
|
||||
return this.configService.get<string>('JWT_EXPIRES_IN');
|
||||
return this.configService.get<string>(`${EnvPrefix}JWT_EXPIRY`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,6 @@ export class GuestStrategy extends PassportStrategy(
|
|||
}
|
||||
|
||||
override async validate(payload: any) {
|
||||
return this.guestService.createGuest();
|
||||
return await this.guestService.getGuestUser();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<EUserBackend> {
|
||||
const user = await this.usersService.findOne('guest');
|
||||
if (HasFailed(user)) {
|
||||
return this.fallBackUser;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row" *ngIf="!isLockedPerms()">
|
||||
<div class="col-lg-6 col-12">
|
||||
<mat-form-field appearance="outline" color="accent">
|
||||
<mat-label>Roles</mat-label>
|
||||
|
@ -112,6 +112,10 @@
|
|||
<button mat-raised-button color="accent" type="submit">
|
||||
{{ editing ? "Update" : "Add" }}
|
||||
</button>
|
||||
|
||||
<button mat-raised-button class="ms-2" color="primary" type="button" (click)="cancel()">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
<mat-header-cell *matHeaderCellDef>Roles</mat-header-cell>
|
||||
<mat-cell *matCellDef="let user">
|
||||
<mat-chip-list aria-label="User Roles">
|
||||
<mat-chip [disableRipple]="true" [disabled]="true" *ngIf="isSystem(user)">
|
||||
System
|
||||
</mat-chip>
|
||||
<mat-chip [disableRipple]="true" *ngFor="let role of user.roles">
|
||||
{{ role }}
|
||||
</mat-chip>
|
||||
|
@ -23,7 +26,11 @@
|
|||
<button mat-icon-button (click)="editUser(user)">
|
||||
<mat-icon aria-label="Edit">edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="deleteUser(user)">
|
||||
<button
|
||||
*ngIf="!isSystem(user)"
|
||||
mat-icon-button
|
||||
(click)="deleteUser(user)"
|
||||
>
|
||||
<mat-icon class="icon-red" aria-label="Delete">delete</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
|
|
|
@ -7,5 +7,10 @@ mat-table {
|
|||
}
|
||||
|
||||
.icon-red {
|
||||
color: #F44336;
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
mat-chip-list {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
|
||||
|
|
8
shared/src/dto/specialusers.dto.ts
Normal file
8
shared/src/dto/specialusers.dto.ts
Normal file
|
@ -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'];
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue