add permissions to roles
This commit is contained in:
parent
0aa897fa8d
commit
b29b88d7b6
|
@ -8,9 +8,9 @@ import { PicsurLoggerModule } from './logger/logger.module';
|
|||
import { AuthManagerModule } from './managers/auth/auth.module';
|
||||
import { DemoManagerModule } from './managers/demo/demomanager.module';
|
||||
import { AuthModule } from './routes/api/auth/auth.module';
|
||||
import { ExperimentModule } from './routes/api/experiment/experiment.module';
|
||||
import { PrefModule } from './routes/api/pref/pref.module';
|
||||
import { ImageModule } from './routes/image/imageroute.module';
|
||||
import { ExperimentModule } from './routes/api/experiment/experiment.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -22,6 +22,7 @@ import { ExperimentModule } from './routes/api/experiment/experiment.module';
|
|||
useExisting: ServeStaticConfigService,
|
||||
imports: [PicsurConfigModule],
|
||||
}),
|
||||
|
||||
AuthManagerModule,
|
||||
AuthModule,
|
||||
ImageModule,
|
||||
|
|
84
backend/src/collections/roledb/roledb.module.ts
Normal file
84
backend/src/collections/roledb/roledb.module.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import {
|
||||
ImmuteableRolesList,
|
||||
SystemRoleDefaults,
|
||||
SystemRoles,
|
||||
SystemRolesList
|
||||
} from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { PicsurConfigModule } from '../../config/config.module';
|
||||
import { HostConfigService } from '../../config/host.config.service';
|
||||
import { ERoleBackend } from '../../models/entities/role.entity';
|
||||
import { RolesService } from './roledb.service';
|
||||
|
||||
@Module({
|
||||
imports: [PicsurConfigModule, TypeOrmModule.forFeature([ERoleBackend])],
|
||||
providers: [RolesService],
|
||||
exports: [RolesService],
|
||||
})
|
||||
export class RolesModule implements OnModuleInit {
|
||||
private readonly logger = new Logger('RolesModule');
|
||||
|
||||
constructor(
|
||||
private rolesService: RolesService,
|
||||
private hostConfig: HostConfigService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
if (!this.hostConfig.isProduction()) {
|
||||
await this.nukeRoles();
|
||||
}
|
||||
|
||||
await this.ensureSystemRolesExist();
|
||||
await this.updateImmutableRoles();
|
||||
}
|
||||
|
||||
private async nukeRoles() {
|
||||
this.logger.error('Nuking all roles');
|
||||
const result = this.rolesService.nuke(true);
|
||||
if (HasFailed(result)) {
|
||||
this.logger.error(`Failed to nuke roles because: ${result.getReason()}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureSystemRolesExist() {
|
||||
for (const systemRole of SystemRolesList as SystemRoles) {
|
||||
this.logger.debug(`Ensuring system role "${systemRole}" exists`);
|
||||
|
||||
const exists = await this.rolesService.exists(systemRole);
|
||||
if (exists) continue;
|
||||
|
||||
const newRole = await this.rolesService.create(
|
||||
systemRole,
|
||||
SystemRoleDefaults[systemRole],
|
||||
);
|
||||
if (HasFailed(newRole)) {
|
||||
this.logger.error(
|
||||
`Failed to create system role "${systemRole}" because: ${newRole.getReason()}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateImmutableRoles() {
|
||||
for (const immutableRole of ImmuteableRolesList as SystemRoles) {
|
||||
this.logger.debug(
|
||||
`Updating permissions for immutable role "${immutableRole}"`,
|
||||
);
|
||||
|
||||
const result = await this.rolesService.setPermissions(
|
||||
immutableRole,
|
||||
SystemRoleDefaults[immutableRole],
|
||||
true,
|
||||
);
|
||||
if (HasFailed(result)) {
|
||||
this.logger.error(
|
||||
`Failed to update permissions for immutable role "${immutableRole}" because: ${result.getReason()}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
173
backend/src/collections/roledb/roledb.service.ts
Normal file
173
backend/src/collections/roledb/roledb.service.ts
Normal file
|
@ -0,0 +1,173 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import { Permissions } from 'picsur-shared/dist/dto/permissions';
|
||||
import {
|
||||
ImmuteableRolesList,
|
||||
Roles,
|
||||
SystemRolesList
|
||||
} from 'picsur-shared/dist/dto/roles.dto';
|
||||
import {
|
||||
AsyncFailable,
|
||||
Fail,
|
||||
HasFailed,
|
||||
HasSuccess
|
||||
} from 'picsur-shared/dist/types';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ERoleBackend } from '../../models/entities/role.entity';
|
||||
|
||||
@Injectable()
|
||||
export class RolesService {
|
||||
private readonly logger = new Logger('UsersService');
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ERoleBackend)
|
||||
private rolesRepository: Repository<ERoleBackend>,
|
||||
) {}
|
||||
|
||||
public async create(
|
||||
name: string,
|
||||
permissions: Permissions,
|
||||
): AsyncFailable<ERoleBackend> {
|
||||
if (await this.exists(name)) return Fail('Role already exists');
|
||||
|
||||
let role = new ERoleBackend();
|
||||
role.name = name;
|
||||
role.permissions = permissions;
|
||||
|
||||
try {
|
||||
role = await this.rolesRepository.save(role, { reload: true });
|
||||
} catch (e: any) {
|
||||
return Fail(e?.message);
|
||||
}
|
||||
|
||||
return plainToClass(ERoleBackend, role);
|
||||
}
|
||||
|
||||
public async delete(
|
||||
role: string | ERoleBackend,
|
||||
): AsyncFailable<ERoleBackend> {
|
||||
const roleToModify = await this.resolve(role);
|
||||
if (HasFailed(roleToModify)) return roleToModify;
|
||||
|
||||
if (SystemRolesList.includes(roleToModify.name)) {
|
||||
return Fail('Cannot delete system role');
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.rolesRepository.remove(roleToModify);
|
||||
} catch (e: any) {
|
||||
return Fail(e?.message);
|
||||
}
|
||||
}
|
||||
|
||||
public async getPermissions(roles: Roles): AsyncFailable<Permissions> {
|
||||
const permissions: Permissions = [];
|
||||
const foundRoles = await Promise.all(
|
||||
roles.map((role: string) => this.findOne(role)),
|
||||
);
|
||||
|
||||
for (const foundRole of foundRoles) {
|
||||
if (HasFailed(foundRole)) return foundRole;
|
||||
permissions.push(...foundRole.permissions);
|
||||
}
|
||||
|
||||
return [...new Set(...[permissions])];
|
||||
}
|
||||
|
||||
public async addPermissions(
|
||||
role: string | ERoleBackend,
|
||||
permissions: Permissions,
|
||||
): AsyncFailable<true> {
|
||||
const roleToModify = await this.resolve(role);
|
||||
if (HasFailed(roleToModify)) return roleToModify;
|
||||
|
||||
// This is stupid
|
||||
const newPermissions = [
|
||||
...new Set([...roleToModify.permissions, ...permissions]),
|
||||
];
|
||||
|
||||
return this.setPermissions(roleToModify, newPermissions);
|
||||
}
|
||||
|
||||
public async removePermissions(
|
||||
role: string | ERoleBackend,
|
||||
permissions: Permissions,
|
||||
): AsyncFailable<true> {
|
||||
const roleToModify = await this.resolve(role);
|
||||
if (HasFailed(roleToModify)) return roleToModify;
|
||||
|
||||
const newPermissions = roleToModify.permissions.filter(
|
||||
(permission) => !permissions.includes(permission),
|
||||
);
|
||||
|
||||
return this.setPermissions(roleToModify, newPermissions);
|
||||
}
|
||||
|
||||
public async setPermissions(
|
||||
role: string | ERoleBackend,
|
||||
permissions: Permissions,
|
||||
allowImmutable: boolean = false,
|
||||
): AsyncFailable<true> {
|
||||
const roleToModify = await this.resolve(role);
|
||||
if (HasFailed(roleToModify)) return roleToModify;
|
||||
|
||||
if (!allowImmutable && ImmuteableRolesList.includes(roleToModify.name)) {
|
||||
return Fail('Cannot modify immutable role');
|
||||
}
|
||||
|
||||
roleToModify.permissions = permissions;
|
||||
|
||||
try {
|
||||
await this.rolesRepository.save(roleToModify);
|
||||
} catch (e: any) {
|
||||
return Fail(e?.message);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async findOne(name: string): AsyncFailable<ERoleBackend> {
|
||||
try {
|
||||
const found = await this.rolesRepository.findOne({
|
||||
where: { name },
|
||||
});
|
||||
|
||||
if (!found) return Fail('User not found');
|
||||
return found as ERoleBackend;
|
||||
} catch (e: any) {
|
||||
return Fail(e?.message);
|
||||
}
|
||||
}
|
||||
|
||||
public async exists(username: string): Promise<boolean> {
|
||||
return HasSuccess(await this.findOne(username));
|
||||
}
|
||||
|
||||
public async nuke(iamsure: boolean = false): AsyncFailable<true> {
|
||||
if (!iamsure) return Fail('Nuke aborted');
|
||||
try {
|
||||
await this.rolesRepository.delete({});
|
||||
} catch (e: any) {
|
||||
return Fail(e?.message);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async resolve(
|
||||
user: string | ERoleBackend,
|
||||
): AsyncFailable<ERoleBackend> {
|
||||
if (typeof user === 'string') {
|
||||
return await this.findOne(user);
|
||||
} else {
|
||||
user = plainToClass(ERoleBackend, user);
|
||||
const errors = await validate(user, { forbidUnknownValues: true });
|
||||
if (errors.length > 0) {
|
||||
this.logger.warn(errors);
|
||||
return Fail('Invalid role');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,55 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { AuthConfigService } from '../../config/auth.config.service';
|
||||
import { PicsurConfigModule } from '../../config/config.module';
|
||||
import { EUserBackend } from '../../models/entities/user.entity';
|
||||
import { RolesModule } from '../roledb/roledb.module';
|
||||
import { UsersService } from './userdb.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([EUserBackend])],
|
||||
imports: [
|
||||
PicsurConfigModule,
|
||||
RolesModule,
|
||||
TypeOrmModule.forFeature([EUserBackend]),
|
||||
],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
export class UsersModule implements OnModuleInit {
|
||||
private readonly logger = new Logger('UsersModule');
|
||||
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
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.usersService.exists(username);
|
||||
if (exists) return;
|
||||
|
||||
const newUser = await this.usersService.create(username, password);
|
||||
if (HasFailed(newUser)) {
|
||||
this.logger.error(
|
||||
`Failed to create admin user "${username}" because: ${newUser.getReason()}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.usersService.addRoles(newUser, ['admin']);
|
||||
if (HasFailed(result)) {
|
||||
this.logger.error(
|
||||
`Failed to make admin user "${username}" because: ${result.getReason()}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { Permissions } from 'picsur-shared/dist/dto/permissions';
|
||||
import { PermanentRolesList, Roles } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import {
|
||||
AsyncFailable,
|
||||
Fail,
|
||||
|
@ -12,6 +14,7 @@ import {
|
|||
import { Repository } from 'typeorm';
|
||||
import { EUserBackend } from '../../models/entities/user.entity';
|
||||
import { GetCols } from '../collectionutils';
|
||||
import { RolesService } from '../roledb/roledb.service';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
|
@ -20,15 +23,18 @@ export class UsersService {
|
|||
constructor(
|
||||
@InjectRepository(EUserBackend)
|
||||
private usersRepository: Repository<EUserBackend>,
|
||||
private rolesService: RolesService,
|
||||
) {}
|
||||
|
||||
public async create(
|
||||
username: string,
|
||||
hashedPassword: string,
|
||||
password: string,
|
||||
roles?: Roles,
|
||||
): AsyncFailable<EUserBackend> {
|
||||
if (await this.exists(username)) return Fail('User already exists');
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
|
||||
let user = new EUserBackend();
|
||||
user.username = username;
|
||||
user.password = hashedPassword;
|
||||
|
@ -57,6 +63,70 @@ export class UsersService {
|
|||
}
|
||||
}
|
||||
|
||||
async authenticate(
|
||||
username: string,
|
||||
password: string,
|
||||
): AsyncFailable<EUserBackend> {
|
||||
const user = await this.findOne(username, true);
|
||||
if (HasFailed(user)) return user;
|
||||
|
||||
if (!(await bcrypt.compare(password, user.password)))
|
||||
return Fail('Wrong password');
|
||||
|
||||
return await this.findOne(username);
|
||||
}
|
||||
|
||||
public async getPermissions(
|
||||
user: string | EUserBackend,
|
||||
): AsyncFailable<Permissions> {
|
||||
const userToModify = await this.resolve(user);
|
||||
if (HasFailed(userToModify)) return userToModify;
|
||||
|
||||
return await this.rolesService.getPermissions(userToModify.roles);
|
||||
}
|
||||
|
||||
public async addRoles(
|
||||
user: string | EUserBackend,
|
||||
roles: Roles,
|
||||
): AsyncFailable<true> {
|
||||
const userToModify = await this.resolve(user);
|
||||
if (HasFailed(userToModify)) return 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;
|
||||
}
|
||||
|
||||
public async removeRoles(
|
||||
user: string | EUserBackend,
|
||||
roles: Roles,
|
||||
): AsyncFailable<true> {
|
||||
const userToModify = await this.resolve(user);
|
||||
if (HasFailed(userToModify)) return userToModify;
|
||||
|
||||
// Make sure we don't remove unremovable roles
|
||||
roles = roles.filter((role) => !PermanentRolesList.includes(role));
|
||||
|
||||
userToModify.roles = userToModify.roles.filter(
|
||||
(role) => !roles.includes(role),
|
||||
);
|
||||
|
||||
try {
|
||||
await this.usersRepository.save(userToModify);
|
||||
} catch (e: any) {
|
||||
return Fail(e?.message);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async findOne<B extends true | undefined = undefined>(
|
||||
username: string,
|
||||
getPrivate?: B,
|
||||
|
@ -90,45 +160,6 @@ export class UsersService {
|
|||
return HasSuccess(await this.findOne(username));
|
||||
}
|
||||
|
||||
public async addRoles(
|
||||
user: string | EUserBackend,
|
||||
roles: Roles,
|
||||
): AsyncFailable<true> {
|
||||
const userToModify = await this.resolve(user);
|
||||
if (HasFailed(userToModify)) return 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;
|
||||
}
|
||||
|
||||
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> {
|
||||
|
|
17
backend/src/decorators/permissions.decorator.ts
Normal file
17
backend/src/decorators/permissions.decorator.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { SetMetadata, UseGuards } from '@nestjs/common';
|
||||
import { Permissions } from 'picsur-shared/dist/dto/permissions';
|
||||
import { CombineDecorators } from 'picsur-shared/dist/util/decorator';
|
||||
import { LocalAuthGuard } from '../managers/auth/guards/localauth.guard';
|
||||
|
||||
export const RequiredPermissions = (...permissions: Permissions) => {
|
||||
return SetMetadata('permissions', permissions);
|
||||
};
|
||||
|
||||
// Easy to read roles
|
||||
export const NoAuth = () => RequiredPermissions();
|
||||
|
||||
export const UseLocalAuth = (...permissions: Permissions) =>
|
||||
CombineDecorators(
|
||||
RequiredPermissions(...permissions),
|
||||
UseGuards(LocalAuthGuard),
|
||||
);
|
|
@ -1,21 +0,0 @@
|
|||
import { SetMetadata, UseGuards } from '@nestjs/common';
|
||||
import { Roles as RolesList } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { CombineDecorators } from 'picsur-shared/dist/util/decorator';
|
||||
import { LocalAuthGuard } from '../managers/auth/guards/localauth.guard';
|
||||
|
||||
export const GuestRoles = (...roles: RolesList) => {
|
||||
return SetMetadata('roles', roles);
|
||||
};
|
||||
|
||||
export const UserRoles = (...roles: RolesList) => {
|
||||
const fullRoles = [...new Set(['user', ...roles])];
|
||||
return SetMetadata('roles', fullRoles);
|
||||
};
|
||||
|
||||
// Easy to read roles
|
||||
export const Guest = () => GuestRoles();
|
||||
export const User = () => UserRoles();
|
||||
export const Admin = () => UserRoles('admin');
|
||||
|
||||
export const UseLocalAuth = () =>
|
||||
CombineDecorators(Guest(), UseGuards(LocalAuthGuard));
|
|
@ -6,6 +6,7 @@ import {
|
|||
} from '@nestjs/platform-fastify';
|
||||
import * as multipart from 'fastify-multipart';
|
||||
import { AppModule } from './app.module';
|
||||
import { UsersService } from './collections/userdb/userdb.service';
|
||||
import { HostConfigService } from './config/host.config.service';
|
||||
import { MainExceptionFilter } from './layers/httpexception/httpexception.filter';
|
||||
import { SuccessInterceptor } from './layers/success/success.interceptor';
|
||||
|
@ -33,7 +34,9 @@ async function bootstrap() {
|
|||
forbidUnknownValues: true,
|
||||
}),
|
||||
);
|
||||
app.useGlobalGuards(new MainAuthGuard(new Reflector()));
|
||||
app.useGlobalGuards(
|
||||
new MainAuthGuard(app.get(Reflector), app.get(UsersService)),
|
||||
);
|
||||
|
||||
app.useLogger(app.get(PicsurLoggerService));
|
||||
|
||||
|
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { EImageBackend } from './image.entity';
|
||||
import { ERoleBackend } from './role.entity';
|
||||
import { ESysPreferenceBackend } from './syspreference.entity';
|
||||
import { EUserBackend } from './user.entity';
|
||||
|
||||
export const EntityList = [EImageBackend, EUserBackend, ESysPreferenceBackend];
|
||||
export const EntityList = [EImageBackend, EUserBackend, ERoleBackend, ESysPreferenceBackend];
|
||||
|
|
16
backend/src/models/entities/role.entity.ts
Normal file
16
backend/src/models/entities/role.entity.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Permissions } from 'picsur-shared/dist/dto/permissions';
|
||||
import { ERole } from 'picsur-shared/dist/entities/role.entity';
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class ERoleBackend extends ERole {
|
||||
@PrimaryGeneratedColumn()
|
||||
override id?: number;
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: false, unique: true })
|
||||
override name: string;
|
||||
|
||||
@Column('text', { nullable: false, array: true })
|
||||
override permissions: Permissions;
|
||||
}
|
|
@ -3,6 +3,7 @@ import {
|
|||
Controller,
|
||||
Get,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
Post,
|
||||
Request
|
||||
} from '@nestjs/common';
|
||||
|
@ -13,16 +14,22 @@ import {
|
|||
AuthRegisterRequest
|
||||
} from 'picsur-shared/dist/dto/auth.dto';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { Admin, UseLocalAuth, User } from '../../../decorators/roles.decorator';
|
||||
import { UsersService } from '../../../collections/userdb/userdb.service';
|
||||
import { RequiredPermissions, UseLocalAuth } from '../../../decorators/permissions.decorator';
|
||||
import { AuthManagerService } from '../../../managers/auth/auth.service';
|
||||
import AuthFasityRequest from '../../../models/dto/authrequest.dto';
|
||||
|
||||
@Controller('api/auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthManagerService) {}
|
||||
private readonly logger = new Logger('AuthController');
|
||||
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private authService: AuthManagerService,
|
||||
) {}
|
||||
|
||||
@Post('login')
|
||||
@UseLocalAuth()
|
||||
@UseLocalAuth('user-login')
|
||||
async login(@Request() req: AuthFasityRequest) {
|
||||
const response: AuthLoginResponse = {
|
||||
jwt_token: await this.authService.createToken(req.user),
|
||||
|
@ -31,37 +38,37 @@ export class AuthController {
|
|||
return response;
|
||||
}
|
||||
|
||||
@Post('create')
|
||||
@Admin()
|
||||
@Post('register')
|
||||
@RequiredPermissions('user-register')
|
||||
async register(
|
||||
@Request() req: AuthFasityRequest,
|
||||
@Body() register: AuthRegisterRequest,
|
||||
) {
|
||||
const user = await this.authService.createUser(
|
||||
const user = await this.usersService.create(
|
||||
register.username,
|
||||
register.password,
|
||||
);
|
||||
if (HasFailed(user)) {
|
||||
console.warn(user.getReason());
|
||||
throw new InternalServerErrorException('Could not create user');
|
||||
this.logger.warn(user.getReason());
|
||||
throw new InternalServerErrorException('Could not register user');
|
||||
}
|
||||
|
||||
if (register.isAdmin) {
|
||||
await this.authService.makeAdmin(user);
|
||||
await this.usersService.addRoles(user, ['admin']);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@Post('delete')
|
||||
@Admin()
|
||||
@RequiredPermissions('user-manage')
|
||||
async delete(
|
||||
@Request() req: AuthFasityRequest,
|
||||
@Body() deleteData: AuthDeleteRequest,
|
||||
) {
|
||||
const user = await this.authService.deleteUser(deleteData.username);
|
||||
const user = await this.usersService.delete(deleteData.username);
|
||||
if (HasFailed(user)) {
|
||||
console.warn(user.getReason());
|
||||
this.logger.warn(user.getReason());
|
||||
throw new InternalServerErrorException('Could not delete user');
|
||||
}
|
||||
|
||||
|
@ -69,11 +76,11 @@ export class AuthController {
|
|||
}
|
||||
|
||||
@Get('list')
|
||||
@Admin()
|
||||
@RequiredPermissions('user-manage')
|
||||
async listUsers(@Request() req: AuthFasityRequest) {
|
||||
const users = this.authService.listUsers();
|
||||
const users = this.usersService.findAll();
|
||||
if (HasFailed(users)) {
|
||||
console.warn(users.getReason());
|
||||
this.logger.warn(users.getReason());
|
||||
throw new InternalServerErrorException('Could not list users');
|
||||
}
|
||||
|
||||
|
@ -81,10 +88,17 @@ export class AuthController {
|
|||
}
|
||||
|
||||
@Get('me')
|
||||
@User()
|
||||
@RequiredPermissions('user-view')
|
||||
async me(@Request() req: AuthFasityRequest) {
|
||||
const permissions = await this.usersService.getPermissions(req.user);
|
||||
if (HasFailed(permissions)) {
|
||||
this.logger.warn(permissions.getReason());
|
||||
throw new InternalServerErrorException('Could not get permissions');
|
||||
}
|
||||
|
||||
const meResponse: AuthMeResponse = new AuthMeResponse();
|
||||
meResponse.user = req.user;
|
||||
meResponse.permissions = permissions;
|
||||
meResponse.newJwtToken = await this.authService.createToken(req.user);
|
||||
|
||||
return meResponse;
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { Controller, Get, Request } from '@nestjs/common';
|
||||
import { Guest } from '../../../decorators/roles.decorator';
|
||||
import AuthFasityRequest from '../../../models/dto/authrequest.dto';
|
||||
|
||||
|
||||
@Controller('api/experiment')
|
||||
export class ExperimentController {
|
||||
|
||||
@Get()
|
||||
@Guest()
|
||||
// @Guest()
|
||||
async testRoute(@Request() req: AuthFasityRequest) {
|
||||
return {
|
||||
message: req.user,
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
Controller,
|
||||
Get,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
Param,
|
||||
Post
|
||||
} from '@nestjs/common';
|
||||
|
@ -12,11 +13,13 @@ import {
|
|||
} from 'picsur-shared/dist/dto/syspreferences.dto';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { SysPreferenceService } from '../../../collections/syspreferencesdb/syspreferencedb.service';
|
||||
import { Admin } from '../../../decorators/roles.decorator';
|
||||
import { RequiredPermissions } from '../../../decorators/permissions.decorator';
|
||||
|
||||
@Controller('api/pref')
|
||||
@Admin()
|
||||
@RequiredPermissions('syspref-manage')
|
||||
export class PrefController {
|
||||
private readonly logger = new Logger('PrefController');
|
||||
|
||||
constructor(private prefService: SysPreferenceService) {}
|
||||
|
||||
@Get('sys/:key')
|
||||
|
@ -25,7 +28,7 @@ export class PrefController {
|
|||
key as SysPreferences,
|
||||
);
|
||||
if (HasFailed(returned)) {
|
||||
console.warn(returned.getReason());
|
||||
this.logger.warn(returned.getReason());
|
||||
throw new InternalServerErrorException('Could not get preference');
|
||||
}
|
||||
|
||||
|
@ -43,7 +46,7 @@ export class PrefController {
|
|||
value,
|
||||
);
|
||||
if (HasFailed(returned)) {
|
||||
console.warn(returned.getReason());
|
||||
this.logger.warn(returned.getReason());
|
||||
throw new InternalServerErrorException('Could not set preference');
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
Controller,
|
||||
Get,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
|
@ -13,13 +14,15 @@ import { isHash } from 'class-validator';
|
|||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { MultiPart } from '../../decorators/multipart.decorator';
|
||||
import { Guest } from '../../decorators/roles.decorator';
|
||||
import { RequiredPermissions } from '../../decorators/permissions.decorator';
|
||||
import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service';
|
||||
import { ImageUploadDto } from '../../models/dto/imageroute.dto';
|
||||
|
||||
@Controller('i')
|
||||
@Guest()
|
||||
@RequiredPermissions('image-view')
|
||||
export class ImageController {
|
||||
private readonly logger = new Logger('ImageController');
|
||||
|
||||
constructor(private readonly imagesService: ImageManagerService) {}
|
||||
|
||||
@Get(':hash')
|
||||
|
@ -31,7 +34,7 @@ export class ImageController {
|
|||
|
||||
const image = await this.imagesService.retrieveComplete(hash);
|
||||
if (HasFailed(image)) {
|
||||
console.warn(image.getReason());
|
||||
this.logger.warn(image.getReason());
|
||||
throw new NotFoundException('Could not find image');
|
||||
}
|
||||
|
||||
|
@ -45,7 +48,7 @@ export class ImageController {
|
|||
|
||||
const image = await this.imagesService.retrieveInfo(hash);
|
||||
if (HasFailed(image)) {
|
||||
console.warn(image.getReason());
|
||||
this.logger.warn(image.getReason());
|
||||
throw new NotFoundException('Could not find image');
|
||||
}
|
||||
|
||||
|
@ -53,7 +56,7 @@ export class ImageController {
|
|||
}
|
||||
|
||||
@Post()
|
||||
//@User()
|
||||
@RequiredPermissions('image-upload')
|
||||
async uploadImage(
|
||||
@Req() req: FastifyRequest,
|
||||
@MultiPart(ImageUploadDto) multipart: ImageUploadDto,
|
||||
|
@ -61,7 +64,7 @@ export class ImageController {
|
|||
const fileBuffer = await multipart.image.toBuffer();
|
||||
const image = await this.imagesService.upload(fileBuffer);
|
||||
if (HasFailed(image)) {
|
||||
console.warn(image.getReason());
|
||||
this.logger.warn(image.getReason());
|
||||
throw new InternalServerErrorException('Could not upload image');
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ import { KeyService } from './key.service';
|
|||
providedIn: 'root',
|
||||
})
|
||||
export class ApiService {
|
||||
private readonly logger = console;
|
||||
|
||||
constructor(private keyService: KeyService) {}
|
||||
|
||||
public async get<T extends Object>(
|
||||
|
@ -32,7 +34,7 @@ export class ApiService {
|
|||
const sendClass = plainToClass(sendType, data);
|
||||
const errors = await validate(sendClass);
|
||||
if (errors.length > 0) {
|
||||
console.warn(errors);
|
||||
this.logger.warn(errors);
|
||||
return Fail('Something went wrong');
|
||||
}
|
||||
|
||||
|
@ -69,14 +71,14 @@ export class ApiService {
|
|||
>(ApiSuccessResponse, result);
|
||||
const resultErrors = await validate(resultClass);
|
||||
if (resultErrors.length > 0) {
|
||||
console.warn('result', resultErrors);
|
||||
this.logger.warn('result', resultErrors);
|
||||
return Fail('Something went wrong');
|
||||
}
|
||||
|
||||
const dataClass = plainToClass(type, result.data);
|
||||
const dataErrors = await validate(dataClass);
|
||||
if (dataErrors.length > 0) {
|
||||
console.warn('data', dataErrors);
|
||||
this.logger.warn('data', dataErrors);
|
||||
return Fail('Something went wrong');
|
||||
}
|
||||
|
||||
|
@ -94,7 +96,7 @@ export class ApiService {
|
|||
try {
|
||||
return await response.json();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
this.logger.warn(e);
|
||||
return Fail('Something went wrong');
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +111,7 @@ export class ApiService {
|
|||
try {
|
||||
return await response.arrayBuffer();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
this.logger.warn(e);
|
||||
return Fail('Something went wrong');
|
||||
}
|
||||
}
|
||||
|
@ -130,7 +132,7 @@ export class ApiService {
|
|||
|
||||
return await window.fetch(url, options);
|
||||
} catch (e: any) {
|
||||
console.warn(e);
|
||||
this.logger.warn(e);
|
||||
return Fail('Something went wrong');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ import { KeyService } from './key.service';
|
|||
providedIn: 'root',
|
||||
})
|
||||
export class UserService {
|
||||
private readonly logger = console;
|
||||
|
||||
public get liveUser() {
|
||||
return this.userSubject;
|
||||
}
|
||||
|
@ -79,7 +81,7 @@ export class UserService {
|
|||
|
||||
const user = await this.extractUser(apikey);
|
||||
if (HasFailed(user)) {
|
||||
console.warn(user.getReason());
|
||||
this.logger.warn(user.getReason());
|
||||
await this.logout();
|
||||
return;
|
||||
}
|
||||
|
@ -88,7 +90,7 @@ export class UserService {
|
|||
|
||||
const fetchedUser = await this.fetchUser();
|
||||
if (HasFailed(fetchedUser)) {
|
||||
console.warn(fetchedUser.getReason());
|
||||
this.logger.warn(fetchedUser.getReason());
|
||||
await this.logout();
|
||||
return;
|
||||
}
|
||||
|
@ -107,7 +109,7 @@ export class UserService {
|
|||
const jwtData = plainToClass(JwtDataDto, decoded);
|
||||
const errors = await validate(jwtData);
|
||||
if (errors.length > 0) {
|
||||
console.warn(errors);
|
||||
this.logger.warn(errors);
|
||||
return Fail('Invalid token data');
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ import { LoginControl } from './login.model';
|
|||
styleUrls: ['./login.component.scss'],
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
private readonly logger = console;
|
||||
|
||||
model = new LoginControl();
|
||||
loginFail = false;
|
||||
|
||||
|
@ -35,7 +37,7 @@ export class LoginComponent implements OnInit {
|
|||
|
||||
const user = await this.userService.login(data.username, data.password);
|
||||
if (HasFailed(user)) {
|
||||
console.warn(user);
|
||||
this.logger.warn(user);
|
||||
this.loginFail = true;
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsDefined,
|
||||
IsNotEmpty,
|
||||
IsArray, IsBoolean,
|
||||
IsDefined, IsEnum, IsInt, IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsInt,
|
||||
ValidateNested,
|
||||
IsString, ValidateNested
|
||||
} from 'class-validator';
|
||||
import { EUser } from '../entities/user.entity';
|
||||
import { Type } from 'class-transformer';
|
||||
import { Permissions, PermissionsList } from './permissions';
|
||||
|
||||
// Api
|
||||
|
||||
|
@ -56,6 +54,11 @@ export class AuthMeResponse {
|
|||
@Type(() => EUser)
|
||||
user: EUser;
|
||||
|
||||
@IsDefined()
|
||||
@IsArray()
|
||||
@IsEnum(PermissionsList, { each: true })
|
||||
permissions: Permissions;
|
||||
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
newJwtToken: string;
|
||||
|
|
20
shared/src/dto/permissions.ts
Normal file
20
shared/src/dto/permissions.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import tuple from '../types/tuple';
|
||||
|
||||
// Config
|
||||
|
||||
const PermissionsTuple = tuple(
|
||||
'image-view',
|
||||
'image-upload',
|
||||
'user-login', // Ability to log in
|
||||
'user-register', // Ability to register
|
||||
'user-view', // Ability to view user info, only granted if logged in
|
||||
'user-manage',
|
||||
'syspref-manage',
|
||||
);
|
||||
|
||||
// Derivatives
|
||||
|
||||
export const PermissionsList: string[] = PermissionsTuple;
|
||||
|
||||
export type Permission = typeof PermissionsTuple[number];
|
||||
export type Permissions = Permission[];
|
|
@ -1,12 +1,36 @@
|
|||
import tuple from '../types/tuple';
|
||||
import { Permissions, PermissionsList } from './permissions';
|
||||
|
||||
// Config
|
||||
|
||||
const RolesTuple = tuple('user', 'admin');
|
||||
// These roles can never be removed from a user
|
||||
const PermanentRolesTuple = tuple('guest', 'user');
|
||||
// These reles can never be modified
|
||||
const ImmuteableRolesTuple = tuple('admin');
|
||||
// These roles can never be removed from the server
|
||||
const SystemRolesTuple = tuple(...PermanentRolesTuple, ...ImmuteableRolesTuple);
|
||||
|
||||
// Derivatives
|
||||
|
||||
export const RolesList: string[] = RolesTuple;
|
||||
export const PermanentRolesList: string[] = PermanentRolesTuple;
|
||||
export const ImmuteableRolesList: string[] = ImmuteableRolesTuple;
|
||||
export const SystemRolesList: string[] = SystemRolesTuple;
|
||||
|
||||
export type Role = typeof RolesTuple[number];
|
||||
export type SystemRole = typeof SystemRolesTuple[number];
|
||||
export type SystemRoles = SystemRole[];
|
||||
|
||||
// Defaults
|
||||
|
||||
export const SystemRoleDefaults: {
|
||||
[key in SystemRole]: Permissions;
|
||||
} = {
|
||||
guest: ['image-view', 'user-login'],
|
||||
user: ['image-view', 'user-view', 'user-login', 'image-upload'],
|
||||
// Grant all permissions to admin
|
||||
admin: PermissionsList as Permissions,
|
||||
};
|
||||
|
||||
// Normal roles types
|
||||
|
||||
export type Role = SystemRole | string;
|
||||
export type Roles = Role[];
|
||||
|
|
14
shared/src/entities/role.entity.ts
Normal file
14
shared/src/entities/role.entity.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { IsArray, IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
|
||||
import { Permissions, PermissionsList } from '../dto/permissions';
|
||||
|
||||
export class ERole {
|
||||
@IsOptional()
|
||||
id?: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsArray()
|
||||
@IsEnum(PermissionsList, { each: true })
|
||||
permissions: Permissions;
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
import { Exclude } from 'class-transformer';
|
||||
import { IsArray, IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
|
||||
import { Roles, RolesList } from '../dto/roles.dto';
|
||||
import {
|
||||
IsArray, IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString
|
||||
} from 'class-validator';
|
||||
import { Roles } from '../dto/roles.dto';
|
||||
|
||||
export class EUser {
|
||||
@IsOptional()
|
||||
|
@ -10,7 +14,7 @@ export class EUser {
|
|||
username: string;
|
||||
|
||||
@IsArray()
|
||||
@IsEnum(RolesList, { each: true })
|
||||
@IsString({ each: true })
|
||||
roles: Roles;
|
||||
|
||||
@IsOptional()
|
||||
|
|
Loading…
Reference in a new issue