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 { 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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ export class AdminGuard implements CanActivate {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return user.isAdmin;
|
return user.roles.includes('admin');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
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 { 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()
|
||||||
|
|||||||
Reference in New Issue
Block a user