add roles to users

This commit is contained in:
rubikscraft
2022-03-10 23:02:27 +01:00
parent 749042cdd5
commit 9b98f3c005
11 changed files with 103 additions and 33 deletions

View File

@@ -2,11 +2,12 @@ import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
import {
AsyncFailable,
Fail,
HasFailed,
HasSuccess,
HasSuccess
} from 'picsur-shared/dist/types';
import { Repository } from 'typeorm';
import { EUserBackend } from '../../models/entities/user.entity';
@@ -24,12 +25,14 @@ export class UsersService {
public async create(
username: string,
hashedPassword: string,
roles?: Roles,
): AsyncFailable<EUserBackend> {
if (await this.exists(username)) return Fail('User already exists');
let user = new EUserBackend();
user.username = username;
user.password = hashedPassword;
user.roles = ['user', ...(roles || [])];
try {
user = await this.usersRepository.save(user, { reload: true });
@@ -41,7 +44,9 @@ export class UsersService {
}
// Returns user object without id
public async delete(user: string | EUserBackend): AsyncFailable<EUserBackend> {
public async delete(
user: string | EUserBackend,
): AsyncFailable<EUserBackend> {
const userToModify = await this.resolve(user);
if (HasFailed(userToModify)) return userToModify;
@@ -55,7 +60,9 @@ export class UsersService {
public async findOne<B extends true | undefined = undefined>(
username: string,
getPrivate?: B,
): AsyncFailable<B extends undefined ? EUserBackend : Required<EUserBackend>> {
): AsyncFailable<
B extends undefined ? EUserBackend : Required<EUserBackend>
> {
try {
const found = await this.usersRepository.findOne({
where: { username },
@@ -63,7 +70,9 @@ export class UsersService {
});
if (!found) return Fail('User not found');
return found as B extends undefined ? EUserBackend : Required<EUserBackend>;
return found as B extends undefined
? EUserBackend
: Required<EUserBackend>;
} catch (e: any) {
return Fail(e?.message);
}
@@ -81,20 +90,48 @@ export class UsersService {
return HasSuccess(await this.findOne(username));
}
public async modifyAdmin(
public async addRoles(
user: string | EUserBackend,
admin: boolean,
roles: Roles,
): AsyncFailable<true> {
const userToModify = await this.resolve(user);
if (HasFailed(userToModify)) return userToModify;
userToModify.isAdmin = admin;
await this.usersRepository.save(userToModify);
// This is stupid
userToModify.roles = [...new Set([...userToModify.roles, ...roles])];
try {
await this.usersRepository.save(userToModify);
} catch (e: any) {
return Fail(e?.message);
}
return true;
}
private async resolve(user: string | EUserBackend): AsyncFailable<EUserBackend> {
public async removeRoles(
user: string | EUserBackend,
roles: Roles,
): AsyncFailable<true> {
const userToModify = await this.resolve(user);
if (HasFailed(userToModify)) return userToModify;
userToModify.roles = userToModify.roles.filter(
(role) => !roles.includes(role),
);
try {
await this.usersRepository.save(userToModify);
} catch (e: any) {
return Fail(e?.message);
}
return true;
}
private async resolve(
user: string | EUserBackend,
): AsyncFailable<EUserBackend> {
if (typeof user === 'string') {
return await this.findOne(user);
} else {

View File

@@ -3,12 +3,13 @@ import { ConfigService } from '@nestjs/config';
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';
import { EntityList } from '../models/entities';
import { DefaultName, EnvPrefix } from './config.static';
import { HostConfigService } from './host.config.service';
@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
private readonly logger = new Logger('TypeOrmConfigService');
constructor(private configService: ConfigService) {}
constructor(private configService: ConfigService, private hostService: HostConfigService) {}
public getTypeOrmServerOptions() {
const varOptions = {
@@ -42,7 +43,7 @@ export class TypeOrmConfigService implements TypeOrmOptionsFactory {
const varOptions = this.getTypeOrmServerOptions();
return {
type: 'postgres',
synchronize: true,
synchronize: !this.hostService.isProduction(),
entities: EntityList,

View File

@@ -21,8 +21,8 @@ async function bootstrap() {
AppModule,
fastifyAdapter,
{
bufferLogs: true
}
bufferLogs: true,
},
);
app.useGlobalFilters(new MainExceptionFilter());
app.useGlobalInterceptors(new SuccessInterceptor());

View File

@@ -1,6 +1,7 @@
import { Logger, Module, OnModuleInit } 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';
@@ -51,7 +52,23 @@ export class AuthManagerModule implements OnModuleInit {
const password = this.authConfigService.getDefaultAdminPassword();
this.logger.debug(`Ensuring admin user "${username}" exists`);
await this.authService.createUser(username, password);
await this.authService.makeAdmin(username);
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;
}
}
}

View File

@@ -30,6 +30,10 @@ export class AuthManagerService {
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;
@@ -55,10 +59,10 @@ export class AuthManagerService {
}
async makeAdmin(user: string | EUserBackend): AsyncFailable<true> {
return this.usersService.modifyAdmin(user, true);
return this.usersService.addRoles(user, ['admin']);
}
async revokeAdmin(user: string | EUserBackend): AsyncFailable<true> {
return this.usersService.modifyAdmin(user, false);
return this.usersService.removeRoles(user, ['admin']);
}
}

View File

@@ -26,6 +26,6 @@ export class AdminGuard implements CanActivate {
return false;
}
return user.isAdmin;
return user.roles.includes('admin');
}
}

View File

@@ -11,13 +11,13 @@ export class EImageBackend extends EImage {
override id?: number;
@Index()
@Column({ unique: true })
@Column({ unique: true, nullable: false })
override hash: string;
// Binary data
@Column({ type: 'bytea', nullable: false, select: false })
override data?: Buffer;
@Column({ enum: SupportedMimes })
@Column({ enum: SupportedMimes, nullable: false })
override mime: SupportedMime;
}

View File

@@ -7,9 +7,9 @@ export class ESysPreferenceBackend extends ESysPreference {
@PrimaryGeneratedColumn()
override id?: number;
@Column({ unique: true })
@Column({ nullable: false, unique: true })
override key: SysPreferences;
@Column()
@Column({ nullable: false })
override value: string;
}

View File

@@ -1,3 +1,4 @@
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
import { EUser } from 'picsur-shared/dist/entities/user.entity';
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
@@ -9,12 +10,12 @@ export class EUserBackend extends EUser {
override id?: number;
@Index()
@Column({ unique: true })
@Column({ nullable: false, unique: true })
override username: string;
@Column({ default: false })
override isAdmin: boolean;
@Column('text', { nullable: false, array: true })
override roles: Roles;
@Column({ select: false })
@Column({ nullable: false, select: false })
override password?: string;
}

View File

@@ -0,0 +1,12 @@
import tuple from '../types/tuple';
// Config
const RolesTuple = tuple('user', 'admin');
// Derivatives
export const RolesList: string[] = RolesTuple;
export type Role = typeof RolesTuple[number];
export type Roles = Role[];

View File

@@ -1,9 +1,6 @@
import { Exclude } from 'class-transformer';
import {
IsDefined,
IsNotEmpty,
IsOptional,
} from 'class-validator';
import { IsArray, IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
import { Roles, RolesList } from '../dto/roles.dto';
export class EUser {
@IsOptional()
@@ -12,8 +9,9 @@ export class EUser {
@IsNotEmpty()
username: string;
@IsDefined()
isAdmin: boolean;
@IsArray()
@IsEnum(RolesList, { each: true })
roles: Roles;
@IsOptional()
@Exclude()