mirror of
https://github.com/CaramelFur/Picsur.git
synced 2025-11-13 15:25:39 +01:00
add roles to users
This commit is contained in:
@@ -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;
|
||||
// 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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ async function bootstrap() {
|
||||
AppModule,
|
||||
fastifyAdapter,
|
||||
{
|
||||
bufferLogs: true
|
||||
}
|
||||
bufferLogs: true,
|
||||
},
|
||||
);
|
||||
app.useGlobalFilters(new MainExceptionFilter());
|
||||
app.useGlobalInterceptors(new SuccessInterceptor());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,6 @@ export class AdminGuard implements CanActivate {
|
||||
return false;
|
||||
}
|
||||
|
||||
return user.isAdmin;
|
||||
return user.roles.includes('admin');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
12
shared/src/dto/roles.dto.ts
Normal file
12
shared/src/dto/roles.dto.ts
Normal 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[];
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user