mirror of
https://github.com/CaramelFur/Picsur.git
synced 2025-11-17 16:50:38 +01:00
add permissions to roles
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { SysPreferenceModule } from '../../collections/syspreferencesdb/syspreferencedb.module';
|
||||
import { UsersModule } from '../../collections/userdb/userdb.module';
|
||||
import { AuthConfigService } from '../../config/auth.config.service';
|
||||
import {
|
||||
JwtConfigService,
|
||||
JwtSecretProvider
|
||||
@@ -35,42 +33,6 @@ import { GuestService } from './guest.service';
|
||||
JwtSecretProvider,
|
||||
GuestService,
|
||||
],
|
||||
exports: [AuthManagerService],
|
||||
exports: [UsersModule, AuthManagerService],
|
||||
})
|
||||
export class AuthManagerModule implements OnModuleInit {
|
||||
private readonly logger = new Logger('AuthModule');
|
||||
|
||||
constructor(
|
||||
private authService: AuthManagerService,
|
||||
private authConfigService: AuthConfigService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.ensureAdminExists();
|
||||
}
|
||||
|
||||
private async ensureAdminExists() {
|
||||
const username = this.authConfigService.getDefaultAdminUsername();
|
||||
const password = this.authConfigService.getDefaultAdminPassword();
|
||||
this.logger.debug(`Ensuring admin user "${username}" exists`);
|
||||
|
||||
const exists = await this.authService.userExists(username);
|
||||
if (exists) return;
|
||||
|
||||
const newUser = await this.authService.createUser(username, password);
|
||||
if (HasFailed(newUser)) {
|
||||
this.logger.error(
|
||||
`Failed to create admin user "${username}" because: ${newUser.getReason()}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.authService.makeAdmin(newUser);
|
||||
if (HasFailed(result)) {
|
||||
this.logger.error(
|
||||
`Failed to make admin user "${username}" because: ${result.getReason()}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
export class AuthManagerModule {}
|
||||
|
||||
@@ -1,48 +1,15 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { instanceToPlain, plainToClass } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import { JwtDataDto } from 'picsur-shared/dist/dto/auth.dto';
|
||||
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { UsersService } from '../../collections/userdb/userdb.service';
|
||||
import { EUserBackend } from '../../models/entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class AuthManagerService {
|
||||
private readonly logger = new Logger('AuthService');
|
||||
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async createUser(username: string, password: string): AsyncFailable<EUserBackend> {
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
return this.usersService.create(username, hashedPassword);
|
||||
}
|
||||
|
||||
async deleteUser(user: string | EUserBackend): AsyncFailable<EUserBackend> {
|
||||
return this.usersService.delete(user);
|
||||
}
|
||||
|
||||
async listUsers(): AsyncFailable<EUserBackend[]> {
|
||||
return this.usersService.findAll();
|
||||
}
|
||||
|
||||
async userExists(username: string): Promise<boolean> {
|
||||
return this.usersService.exists(username);
|
||||
}
|
||||
|
||||
async authenticate(username: string, password: string): AsyncFailable<EUserBackend> {
|
||||
const user = await this.usersService.findOne(username, true);
|
||||
if (HasFailed(user)) return user;
|
||||
|
||||
if (!(await bcrypt.compare(password, user.password)))
|
||||
return Fail('Wrong password');
|
||||
|
||||
return await this.usersService.findOne(username);
|
||||
}
|
||||
constructor(private jwtService: JwtService) {}
|
||||
|
||||
async createToken(user: EUserBackend): Promise<string> {
|
||||
const jwtData: JwtDataDto = plainToClass(JwtDataDto, {
|
||||
@@ -57,12 +24,4 @@ export class AuthManagerService {
|
||||
|
||||
return this.jwtService.signAsync(instanceToPlain(jwtData));
|
||||
}
|
||||
|
||||
async makeAdmin(user: string | EUserBackend): AsyncFailable<true> {
|
||||
return this.usersService.addRoles(user, ['admin']);
|
||||
}
|
||||
|
||||
async revokeAdmin(user: string | EUserBackend): AsyncFailable<true> {
|
||||
return this.usersService.removeRoles(user, ['admin']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,17 @@ import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy } from 'passport-local';
|
||||
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { UsersService } from '../../../collections/userdb/userdb.service';
|
||||
import { EUserBackend } from '../../../models/entities/user.entity';
|
||||
import { AuthManagerService } from '../auth.service';
|
||||
|
||||
@Injectable()
|
||||
export class LocalAuthStrategy extends PassportStrategy(Strategy, 'local') {
|
||||
constructor(private authService: AuthManagerService) {
|
||||
constructor(private usersService: UsersService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async validate(username: string, password: string): AsyncFailable<EUserBackend> {
|
||||
const user = await this.authService.authenticate(username, password);
|
||||
const user = await this.usersService.authenticate(username, password);
|
||||
if (HasFailed(user)) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
@@ -8,15 +8,23 @@ import { Reflector } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { isArray, isEnum, isString, validate } from 'class-validator';
|
||||
import { Roles, RolesList } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import {
|
||||
Permissions,
|
||||
PermissionsList
|
||||
} from 'picsur-shared/dist/dto/permissions';
|
||||
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { Fail, Failable, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { UsersService } from '../../../collections/userdb/userdb.service';
|
||||
import { EUserBackend } from '../../../models/entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
|
||||
private readonly logger = new Logger('MainAuthGuard');
|
||||
|
||||
constructor(private reflector: Reflector) {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private usersService: UsersService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -31,39 +39,47 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
|
||||
context.switchToHttp().getRequest().user,
|
||||
);
|
||||
|
||||
const roles = this.extractRoles(context);
|
||||
if (HasFailed(roles)) {
|
||||
this.logger.warn(roles.getReason());
|
||||
const permissions = this.extractPermissions(context);
|
||||
if (HasFailed(permissions)) {
|
||||
this.logger.warn(permissions.getReason());
|
||||
return false;
|
||||
}
|
||||
|
||||
// User must have all roles
|
||||
return roles.every((role) => user.roles.includes(role));
|
||||
const userPermissions = await this.usersService.getPermissions(user);
|
||||
if (HasFailed(userPermissions)) {
|
||||
this.logger.warn(userPermissions.getReason());
|
||||
return false;
|
||||
}
|
||||
|
||||
return permissions.every((permission) =>
|
||||
userPermissions.includes(permission),
|
||||
);
|
||||
}
|
||||
|
||||
private extractRoles(context: ExecutionContext): Failable<Roles> {
|
||||
private extractPermissions(context: ExecutionContext): Failable<Permissions> {
|
||||
const handlerName = context.getHandler().name;
|
||||
const roles =
|
||||
this.reflector.get<Roles>('roles', context.getHandler()) ??
|
||||
this.reflector.get<Roles>('roles', context.getClass());
|
||||
const permissions =
|
||||
this.reflector.get<Permissions>('permissions', context.getHandler()) ??
|
||||
this.reflector.get<Permissions>('permissions', context.getClass());
|
||||
|
||||
if (roles === undefined) {
|
||||
if (permissions === undefined) {
|
||||
return Fail(
|
||||
`${handlerName} does not have any roles defined, denying access`,
|
||||
`${handlerName} does not have any permissions defined, denying access`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.isRolesArray(roles)) {
|
||||
return Fail(`Roles for ${handlerName} is not a string array`);
|
||||
if (!this.isPermissionsArray(permissions)) {
|
||||
return Fail(`Permissions for ${handlerName} is not a string array`);
|
||||
}
|
||||
|
||||
return roles;
|
||||
return permissions;
|
||||
}
|
||||
|
||||
private isRolesArray(value: any): value is Roles {
|
||||
private isPermissionsArray(value: any): value is Roles {
|
||||
if (!isArray(value)) return false;
|
||||
if (!value.every((item: unknown) => isString(item))) return false;
|
||||
if (!value.every((item: string) => isEnum(item, RolesList))) return false;
|
||||
if (!value.every((item: string) => isEnum(item, PermissionsList)))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { EUserBackend } from '../../models/entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
@@ -7,13 +6,9 @@ export class GuestService {
|
||||
public createGuest(): EUserBackend {
|
||||
const guest = new EUserBackend();
|
||||
guest.id = -1;
|
||||
guest.roles = this.createGuestRoles();
|
||||
guest.roles = ['guest'];
|
||||
guest.username = 'guest';
|
||||
|
||||
return guest;
|
||||
}
|
||||
|
||||
private createGuestRoles(): Roles {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user