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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { Logger, Module, OnModuleInit } from '@nestjs/common'; import { Logger, Module, OnModuleInit } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport'; import { PassportModule } from '@nestjs/passport';
import { HasFailed } from 'picsur-shared/dist/types';
import { SysPreferenceModule } from '../../collections/syspreferencesdb/syspreferencedb.module'; import { SysPreferenceModule } from '../../collections/syspreferencesdb/syspreferencedb.module';
import { UsersModule } from '../../collections/userdb/userdb.module'; import { UsersModule } from '../../collections/userdb/userdb.module';
import { AuthConfigService } from '../../config/auth.config.service'; import { AuthConfigService } from '../../config/auth.config.service';
@@ -51,7 +52,23 @@ export class AuthManagerModule implements OnModuleInit {
const password = this.authConfigService.getDefaultAdminPassword(); const password = this.authConfigService.getDefaultAdminPassword();
this.logger.debug(`Ensuring admin user "${username}" exists`); this.logger.debug(`Ensuring admin user "${username}" exists`);
await this.authService.createUser(username, password); const exists = await this.authService.userExists(username);
await this.authService.makeAdmin(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(); return this.usersService.findAll();
} }
async userExists(username: string): Promise<boolean> {
return this.usersService.exists(username);
}
async authenticate(username: string, password: string): AsyncFailable<EUserBackend> { async authenticate(username: string, password: string): AsyncFailable<EUserBackend> {
const user = await this.usersService.findOne(username, true); const user = await this.usersService.findOne(username, true);
if (HasFailed(user)) return user; if (HasFailed(user)) return user;
@@ -55,10 +59,10 @@ export class AuthManagerService {
} }
async makeAdmin(user: string | EUserBackend): AsyncFailable<true> { 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> { 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 false;
} }
return user.isAdmin; return user.roles.includes('admin');
} }
} }

View File

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

View File

@@ -7,9 +7,9 @@ export class ESysPreferenceBackend extends ESysPreference {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
override id?: number; override id?: number;
@Column({ unique: true }) @Column({ nullable: false, unique: true })
override key: SysPreferences; override key: SysPreferences;
@Column() @Column({ nullable: false })
override value: string; 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 { EUser } from 'picsur-shared/dist/entities/user.entity';
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
@@ -9,12 +10,12 @@ export class EUserBackend extends EUser {
override id?: number; override id?: number;
@Index() @Index()
@Column({ unique: true }) @Column({ nullable: false, unique: true })
override username: string; override username: string;
@Column({ default: false }) @Column('text', { nullable: false, array: true })
override isAdmin: boolean; override roles: Roles;
@Column({ select: false }) @Column({ nullable: false, select: false })
override password?: string; 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 { Exclude } from 'class-transformer';
import { import { IsArray, IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
IsDefined, import { Roles, RolesList } from '../dto/roles.dto';
IsNotEmpty,
IsOptional,
} from 'class-validator';
export class EUser { export class EUser {
@IsOptional() @IsOptional()
@@ -12,8 +9,9 @@ export class EUser {
@IsNotEmpty() @IsNotEmpty()
username: string; username: string;
@IsDefined() @IsArray()
isAdmin: boolean; @IsEnum(RolesList, { each: true })
roles: Roles;
@IsOptional() @IsOptional()
@Exclude() @Exclude()