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 { AuthManagerModule } from './managers/auth/auth.module';
|
||||||
import { DemoManagerModule } from './managers/demo/demomanager.module';
|
import { DemoManagerModule } from './managers/demo/demomanager.module';
|
||||||
import { AuthModule } from './routes/api/auth/auth.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 { PrefModule } from './routes/api/pref/pref.module';
|
||||||
import { ImageModule } from './routes/image/imageroute.module';
|
import { ImageModule } from './routes/image/imageroute.module';
|
||||||
import { ExperimentModule } from './routes/api/experiment/experiment.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -22,6 +22,7 @@ import { ExperimentModule } from './routes/api/experiment/experiment.module';
|
||||||
useExisting: ServeStaticConfigService,
|
useExisting: ServeStaticConfigService,
|
||||||
imports: [PicsurConfigModule],
|
imports: [PicsurConfigModule],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
AuthManagerModule,
|
AuthManagerModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
ImageModule,
|
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 { 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 { EUserBackend } from '../../models/entities/user.entity';
|
||||||
|
import { RolesModule } from '../roledb/roledb.module';
|
||||||
import { UsersService } from './userdb.service';
|
import { UsersService } from './userdb.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([EUserBackend])],
|
imports: [
|
||||||
|
PicsurConfigModule,
|
||||||
|
RolesModule,
|
||||||
|
TypeOrmModule.forFeature([EUserBackend]),
|
||||||
|
],
|
||||||
providers: [UsersService],
|
providers: [UsersService],
|
||||||
exports: [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 { Injectable, Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
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 { Permissions } from 'picsur-shared/dist/dto/permissions';
|
||||||
|
import { PermanentRolesList, Roles } from 'picsur-shared/dist/dto/roles.dto';
|
||||||
import {
|
import {
|
||||||
AsyncFailable,
|
AsyncFailable,
|
||||||
Fail,
|
Fail,
|
||||||
|
@ -12,6 +14,7 @@ import {
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { EUserBackend } from '../../models/entities/user.entity';
|
import { EUserBackend } from '../../models/entities/user.entity';
|
||||||
import { GetCols } from '../collectionutils';
|
import { GetCols } from '../collectionutils';
|
||||||
|
import { RolesService } from '../roledb/roledb.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
|
@ -20,15 +23,18 @@ export class UsersService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(EUserBackend)
|
@InjectRepository(EUserBackend)
|
||||||
private usersRepository: Repository<EUserBackend>,
|
private usersRepository: Repository<EUserBackend>,
|
||||||
|
private rolesService: RolesService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async create(
|
public async create(
|
||||||
username: string,
|
username: string,
|
||||||
hashedPassword: string,
|
password: string,
|
||||||
roles?: Roles,
|
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');
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 12);
|
||||||
|
|
||||||
let user = new EUserBackend();
|
let user = new EUserBackend();
|
||||||
user.username = username;
|
user.username = username;
|
||||||
user.password = hashedPassword;
|
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>(
|
public async findOne<B extends true | undefined = undefined>(
|
||||||
username: string,
|
username: string,
|
||||||
getPrivate?: B,
|
getPrivate?: B,
|
||||||
|
@ -90,45 +160,6 @@ export class UsersService {
|
||||||
return HasSuccess(await this.findOne(username));
|
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(
|
private async resolve(
|
||||||
user: string | EUserBackend,
|
user: string | EUserBackend,
|
||||||
): AsyncFailable<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';
|
} from '@nestjs/platform-fastify';
|
||||||
import * as multipart from 'fastify-multipart';
|
import * as multipart from 'fastify-multipart';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import { UsersService } from './collections/userdb/userdb.service';
|
||||||
import { HostConfigService } from './config/host.config.service';
|
import { HostConfigService } from './config/host.config.service';
|
||||||
import { MainExceptionFilter } from './layers/httpexception/httpexception.filter';
|
import { MainExceptionFilter } from './layers/httpexception/httpexception.filter';
|
||||||
import { SuccessInterceptor } from './layers/success/success.interceptor';
|
import { SuccessInterceptor } from './layers/success/success.interceptor';
|
||||||
|
@ -33,7 +34,9 @@ async function bootstrap() {
|
||||||
forbidUnknownValues: true,
|
forbidUnknownValues: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.useGlobalGuards(new MainAuthGuard(new Reflector()));
|
app.useGlobalGuards(
|
||||||
|
new MainAuthGuard(app.get(Reflector), app.get(UsersService)),
|
||||||
|
);
|
||||||
|
|
||||||
app.useLogger(app.get(PicsurLoggerService));
|
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 { 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 {
|
import {
|
||||||
JwtConfigService,
|
JwtConfigService,
|
||||||
JwtSecretProvider
|
JwtSecretProvider
|
||||||
|
@ -35,42 +33,6 @@ import { GuestService } from './guest.service';
|
||||||
JwtSecretProvider,
|
JwtSecretProvider,
|
||||||
GuestService,
|
GuestService,
|
||||||
],
|
],
|
||||||
exports: [AuthManagerService],
|
exports: [UsersModule, AuthManagerService],
|
||||||
})
|
})
|
||||||
export class AuthManagerModule implements OnModuleInit {
|
export class AuthManagerModule {}
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,48 +1,15 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
import { instanceToPlain, plainToClass } from 'class-transformer';
|
import { instanceToPlain, plainToClass } from 'class-transformer';
|
||||||
import { validate } from 'class-validator';
|
import { validate } from 'class-validator';
|
||||||
import { JwtDataDto } from 'picsur-shared/dist/dto/auth.dto';
|
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';
|
import { EUserBackend } from '../../models/entities/user.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthManagerService {
|
export class AuthManagerService {
|
||||||
private readonly logger = new Logger('AuthService');
|
private readonly logger = new Logger('AuthService');
|
||||||
|
|
||||||
constructor(
|
constructor(private jwtService: JwtService) {}
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createToken(user: EUserBackend): Promise<string> {
|
async createToken(user: EUserBackend): Promise<string> {
|
||||||
const jwtData: JwtDataDto = plainToClass(JwtDataDto, {
|
const jwtData: JwtDataDto = plainToClass(JwtDataDto, {
|
||||||
|
@ -57,12 +24,4 @@ export class AuthManagerService {
|
||||||
|
|
||||||
return this.jwtService.signAsync(instanceToPlain(jwtData));
|
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 { PassportStrategy } from '@nestjs/passport';
|
||||||
import { Strategy } from 'passport-local';
|
import { Strategy } from 'passport-local';
|
||||||
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
||||||
|
import { UsersService } from '../../../collections/userdb/userdb.service';
|
||||||
import { EUserBackend } from '../../../models/entities/user.entity';
|
import { EUserBackend } from '../../../models/entities/user.entity';
|
||||||
import { AuthManagerService } from '../auth.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LocalAuthStrategy extends PassportStrategy(Strategy, 'local') {
|
export class LocalAuthStrategy extends PassportStrategy(Strategy, 'local') {
|
||||||
constructor(private authService: AuthManagerService) {
|
constructor(private usersService: UsersService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate(username: string, password: string): AsyncFailable<EUserBackend> {
|
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)) {
|
if (HasFailed(user)) {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,15 +8,23 @@ import { Reflector } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { plainToClass } from 'class-transformer';
|
import { plainToClass } from 'class-transformer';
|
||||||
import { isArray, isEnum, isString, validate } from 'class-validator';
|
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 { Fail, Failable, HasFailed } from 'picsur-shared/dist/types';
|
||||||
|
import { UsersService } from '../../../collections/userdb/userdb.service';
|
||||||
import { EUserBackend } from '../../../models/entities/user.entity';
|
import { EUserBackend } from '../../../models/entities/user.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
|
export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
|
||||||
private readonly logger = new Logger('MainAuthGuard');
|
private readonly logger = new Logger('MainAuthGuard');
|
||||||
|
|
||||||
constructor(private reflector: Reflector) {
|
constructor(
|
||||||
|
private reflector: Reflector,
|
||||||
|
private usersService: UsersService,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,39 +39,47 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
|
||||||
context.switchToHttp().getRequest().user,
|
context.switchToHttp().getRequest().user,
|
||||||
);
|
);
|
||||||
|
|
||||||
const roles = this.extractRoles(context);
|
const permissions = this.extractPermissions(context);
|
||||||
if (HasFailed(roles)) {
|
if (HasFailed(permissions)) {
|
||||||
this.logger.warn(roles.getReason());
|
this.logger.warn(permissions.getReason());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// User must have all roles
|
const userPermissions = await this.usersService.getPermissions(user);
|
||||||
return roles.every((role) => user.roles.includes(role));
|
if (HasFailed(userPermissions)) {
|
||||||
|
this.logger.warn(userPermissions.getReason());
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractRoles(context: ExecutionContext): Failable<Roles> {
|
return permissions.every((permission) =>
|
||||||
const handlerName = context.getHandler().name;
|
userPermissions.includes(permission),
|
||||||
const roles =
|
|
||||||
this.reflector.get<Roles>('roles', context.getHandler()) ??
|
|
||||||
this.reflector.get<Roles>('roles', context.getClass());
|
|
||||||
|
|
||||||
if (roles === undefined) {
|
|
||||||
return Fail(
|
|
||||||
`${handlerName} does not have any roles defined, denying access`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isRolesArray(roles)) {
|
private extractPermissions(context: ExecutionContext): Failable<Permissions> {
|
||||||
return Fail(`Roles for ${handlerName} is not a string array`);
|
const handlerName = context.getHandler().name;
|
||||||
|
const permissions =
|
||||||
|
this.reflector.get<Permissions>('permissions', context.getHandler()) ??
|
||||||
|
this.reflector.get<Permissions>('permissions', context.getClass());
|
||||||
|
|
||||||
|
if (permissions === undefined) {
|
||||||
|
return Fail(
|
||||||
|
`${handlerName} does not have any permissions defined, denying access`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return roles;
|
if (!this.isPermissionsArray(permissions)) {
|
||||||
|
return Fail(`Permissions for ${handlerName} is not a string array`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private isRolesArray(value: any): value is Roles {
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isPermissionsArray(value: any): value is Roles {
|
||||||
if (!isArray(value)) return false;
|
if (!isArray(value)) return false;
|
||||||
if (!value.every((item: unknown) => isString(item))) 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
|
|
||||||
import { EUserBackend } from '../../models/entities/user.entity';
|
import { EUserBackend } from '../../models/entities/user.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -7,13 +6,9 @@ export class GuestService {
|
||||||
public createGuest(): EUserBackend {
|
public createGuest(): EUserBackend {
|
||||||
const guest = new EUserBackend();
|
const guest = new EUserBackend();
|
||||||
guest.id = -1;
|
guest.id = -1;
|
||||||
guest.roles = this.createGuestRoles();
|
guest.roles = ['guest'];
|
||||||
guest.username = 'guest';
|
guest.username = 'guest';
|
||||||
|
|
||||||
return guest;
|
return guest;
|
||||||
}
|
}
|
||||||
|
|
||||||
private createGuestRoles(): Roles {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { EImageBackend } from './image.entity';
|
import { EImageBackend } from './image.entity';
|
||||||
|
import { ERoleBackend } from './role.entity';
|
||||||
import { ESysPreferenceBackend } from './syspreference.entity';
|
import { ESysPreferenceBackend } from './syspreference.entity';
|
||||||
import { EUserBackend } from './user.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,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
|
Logger,
|
||||||
Post,
|
Post,
|
||||||
Request
|
Request
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
@ -13,16 +14,22 @@ import {
|
||||||
AuthRegisterRequest
|
AuthRegisterRequest
|
||||||
} from 'picsur-shared/dist/dto/auth.dto';
|
} from 'picsur-shared/dist/dto/auth.dto';
|
||||||
import { HasFailed } from 'picsur-shared/dist/types';
|
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 { AuthManagerService } from '../../../managers/auth/auth.service';
|
||||||
import AuthFasityRequest from '../../../models/dto/authrequest.dto';
|
import AuthFasityRequest from '../../../models/dto/authrequest.dto';
|
||||||
|
|
||||||
@Controller('api/auth')
|
@Controller('api/auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private authService: AuthManagerService) {}
|
private readonly logger = new Logger('AuthController');
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private usersService: UsersService,
|
||||||
|
private authService: AuthManagerService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
@UseLocalAuth()
|
@UseLocalAuth('user-login')
|
||||||
async login(@Request() req: AuthFasityRequest) {
|
async login(@Request() req: AuthFasityRequest) {
|
||||||
const response: AuthLoginResponse = {
|
const response: AuthLoginResponse = {
|
||||||
jwt_token: await this.authService.createToken(req.user),
|
jwt_token: await this.authService.createToken(req.user),
|
||||||
|
@ -31,37 +38,37 @@ export class AuthController {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('create')
|
@Post('register')
|
||||||
@Admin()
|
@RequiredPermissions('user-register')
|
||||||
async register(
|
async register(
|
||||||
@Request() req: AuthFasityRequest,
|
@Request() req: AuthFasityRequest,
|
||||||
@Body() register: AuthRegisterRequest,
|
@Body() register: AuthRegisterRequest,
|
||||||
) {
|
) {
|
||||||
const user = await this.authService.createUser(
|
const user = await this.usersService.create(
|
||||||
register.username,
|
register.username,
|
||||||
register.password,
|
register.password,
|
||||||
);
|
);
|
||||||
if (HasFailed(user)) {
|
if (HasFailed(user)) {
|
||||||
console.warn(user.getReason());
|
this.logger.warn(user.getReason());
|
||||||
throw new InternalServerErrorException('Could not create user');
|
throw new InternalServerErrorException('Could not register user');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (register.isAdmin) {
|
if (register.isAdmin) {
|
||||||
await this.authService.makeAdmin(user);
|
await this.usersService.addRoles(user, ['admin']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('delete')
|
@Post('delete')
|
||||||
@Admin()
|
@RequiredPermissions('user-manage')
|
||||||
async delete(
|
async delete(
|
||||||
@Request() req: AuthFasityRequest,
|
@Request() req: AuthFasityRequest,
|
||||||
@Body() deleteData: AuthDeleteRequest,
|
@Body() deleteData: AuthDeleteRequest,
|
||||||
) {
|
) {
|
||||||
const user = await this.authService.deleteUser(deleteData.username);
|
const user = await this.usersService.delete(deleteData.username);
|
||||||
if (HasFailed(user)) {
|
if (HasFailed(user)) {
|
||||||
console.warn(user.getReason());
|
this.logger.warn(user.getReason());
|
||||||
throw new InternalServerErrorException('Could not delete user');
|
throw new InternalServerErrorException('Could not delete user');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,11 +76,11 @@ export class AuthController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('list')
|
@Get('list')
|
||||||
@Admin()
|
@RequiredPermissions('user-manage')
|
||||||
async listUsers(@Request() req: AuthFasityRequest) {
|
async listUsers(@Request() req: AuthFasityRequest) {
|
||||||
const users = this.authService.listUsers();
|
const users = this.usersService.findAll();
|
||||||
if (HasFailed(users)) {
|
if (HasFailed(users)) {
|
||||||
console.warn(users.getReason());
|
this.logger.warn(users.getReason());
|
||||||
throw new InternalServerErrorException('Could not list users');
|
throw new InternalServerErrorException('Could not list users');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,10 +88,17 @@ export class AuthController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('me')
|
@Get('me')
|
||||||
@User()
|
@RequiredPermissions('user-view')
|
||||||
async me(@Request() req: AuthFasityRequest) {
|
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();
|
const meResponse: AuthMeResponse = new AuthMeResponse();
|
||||||
meResponse.user = req.user;
|
meResponse.user = req.user;
|
||||||
|
meResponse.permissions = permissions;
|
||||||
meResponse.newJwtToken = await this.authService.createToken(req.user);
|
meResponse.newJwtToken = await this.authService.createToken(req.user);
|
||||||
|
|
||||||
return meResponse;
|
return meResponse;
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { Controller, Get, Request } from '@nestjs/common';
|
import { Controller, Get, Request } from '@nestjs/common';
|
||||||
import { Guest } from '../../../decorators/roles.decorator';
|
|
||||||
import AuthFasityRequest from '../../../models/dto/authrequest.dto';
|
import AuthFasityRequest from '../../../models/dto/authrequest.dto';
|
||||||
|
|
||||||
|
|
||||||
@Controller('api/experiment')
|
@Controller('api/experiment')
|
||||||
export class ExperimentController {
|
export class ExperimentController {
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@Guest()
|
// @Guest()
|
||||||
async testRoute(@Request() req: AuthFasityRequest) {
|
async testRoute(@Request() req: AuthFasityRequest) {
|
||||||
return {
|
return {
|
||||||
message: req.user,
|
message: req.user,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
|
Logger,
|
||||||
Param,
|
Param,
|
||||||
Post
|
Post
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
@ -12,11 +13,13 @@ import {
|
||||||
} from 'picsur-shared/dist/dto/syspreferences.dto';
|
} from 'picsur-shared/dist/dto/syspreferences.dto';
|
||||||
import { HasFailed } from 'picsur-shared/dist/types';
|
import { HasFailed } from 'picsur-shared/dist/types';
|
||||||
import { SysPreferenceService } from '../../../collections/syspreferencesdb/syspreferencedb.service';
|
import { SysPreferenceService } from '../../../collections/syspreferencesdb/syspreferencedb.service';
|
||||||
import { Admin } from '../../../decorators/roles.decorator';
|
import { RequiredPermissions } from '../../../decorators/permissions.decorator';
|
||||||
|
|
||||||
@Controller('api/pref')
|
@Controller('api/pref')
|
||||||
@Admin()
|
@RequiredPermissions('syspref-manage')
|
||||||
export class PrefController {
|
export class PrefController {
|
||||||
|
private readonly logger = new Logger('PrefController');
|
||||||
|
|
||||||
constructor(private prefService: SysPreferenceService) {}
|
constructor(private prefService: SysPreferenceService) {}
|
||||||
|
|
||||||
@Get('sys/:key')
|
@Get('sys/:key')
|
||||||
|
@ -25,7 +28,7 @@ export class PrefController {
|
||||||
key as SysPreferences,
|
key as SysPreferences,
|
||||||
);
|
);
|
||||||
if (HasFailed(returned)) {
|
if (HasFailed(returned)) {
|
||||||
console.warn(returned.getReason());
|
this.logger.warn(returned.getReason());
|
||||||
throw new InternalServerErrorException('Could not get preference');
|
throw new InternalServerErrorException('Could not get preference');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +46,7 @@ export class PrefController {
|
||||||
value,
|
value,
|
||||||
);
|
);
|
||||||
if (HasFailed(returned)) {
|
if (HasFailed(returned)) {
|
||||||
console.warn(returned.getReason());
|
this.logger.warn(returned.getReason());
|
||||||
throw new InternalServerErrorException('Could not set preference');
|
throw new InternalServerErrorException('Could not set preference');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
|
Logger,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
Param,
|
Param,
|
||||||
Post,
|
Post,
|
||||||
|
@ -13,13 +14,15 @@ import { isHash } from 'class-validator';
|
||||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import { HasFailed } from 'picsur-shared/dist/types';
|
import { HasFailed } from 'picsur-shared/dist/types';
|
||||||
import { MultiPart } from '../../decorators/multipart.decorator';
|
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 { ImageManagerService } from '../../managers/imagemanager/imagemanager.service';
|
||||||
import { ImageUploadDto } from '../../models/dto/imageroute.dto';
|
import { ImageUploadDto } from '../../models/dto/imageroute.dto';
|
||||||
|
|
||||||
@Controller('i')
|
@Controller('i')
|
||||||
@Guest()
|
@RequiredPermissions('image-view')
|
||||||
export class ImageController {
|
export class ImageController {
|
||||||
|
private readonly logger = new Logger('ImageController');
|
||||||
|
|
||||||
constructor(private readonly imagesService: ImageManagerService) {}
|
constructor(private readonly imagesService: ImageManagerService) {}
|
||||||
|
|
||||||
@Get(':hash')
|
@Get(':hash')
|
||||||
|
@ -31,7 +34,7 @@ export class ImageController {
|
||||||
|
|
||||||
const image = await this.imagesService.retrieveComplete(hash);
|
const image = await this.imagesService.retrieveComplete(hash);
|
||||||
if (HasFailed(image)) {
|
if (HasFailed(image)) {
|
||||||
console.warn(image.getReason());
|
this.logger.warn(image.getReason());
|
||||||
throw new NotFoundException('Could not find image');
|
throw new NotFoundException('Could not find image');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +48,7 @@ export class ImageController {
|
||||||
|
|
||||||
const image = await this.imagesService.retrieveInfo(hash);
|
const image = await this.imagesService.retrieveInfo(hash);
|
||||||
if (HasFailed(image)) {
|
if (HasFailed(image)) {
|
||||||
console.warn(image.getReason());
|
this.logger.warn(image.getReason());
|
||||||
throw new NotFoundException('Could not find image');
|
throw new NotFoundException('Could not find image');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +56,7 @@ export class ImageController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
//@User()
|
@RequiredPermissions('image-upload')
|
||||||
async uploadImage(
|
async uploadImage(
|
||||||
@Req() req: FastifyRequest,
|
@Req() req: FastifyRequest,
|
||||||
@MultiPart(ImageUploadDto) multipart: ImageUploadDto,
|
@MultiPart(ImageUploadDto) multipart: ImageUploadDto,
|
||||||
|
@ -61,7 +64,7 @@ export class ImageController {
|
||||||
const fileBuffer = await multipart.image.toBuffer();
|
const fileBuffer = await multipart.image.toBuffer();
|
||||||
const image = await this.imagesService.upload(fileBuffer);
|
const image = await this.imagesService.upload(fileBuffer);
|
||||||
if (HasFailed(image)) {
|
if (HasFailed(image)) {
|
||||||
console.warn(image.getReason());
|
this.logger.warn(image.getReason());
|
||||||
throw new InternalServerErrorException('Could not upload image');
|
throw new InternalServerErrorException('Could not upload image');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@ import { KeyService } from './key.service';
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class ApiService {
|
export class ApiService {
|
||||||
|
private readonly logger = console;
|
||||||
|
|
||||||
constructor(private keyService: KeyService) {}
|
constructor(private keyService: KeyService) {}
|
||||||
|
|
||||||
public async get<T extends Object>(
|
public async get<T extends Object>(
|
||||||
|
@ -32,7 +34,7 @@ export class ApiService {
|
||||||
const sendClass = plainToClass(sendType, data);
|
const sendClass = plainToClass(sendType, data);
|
||||||
const errors = await validate(sendClass);
|
const errors = await validate(sendClass);
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
console.warn(errors);
|
this.logger.warn(errors);
|
||||||
return Fail('Something went wrong');
|
return Fail('Something went wrong');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,14 +71,14 @@ export class ApiService {
|
||||||
>(ApiSuccessResponse, result);
|
>(ApiSuccessResponse, result);
|
||||||
const resultErrors = await validate(resultClass);
|
const resultErrors = await validate(resultClass);
|
||||||
if (resultErrors.length > 0) {
|
if (resultErrors.length > 0) {
|
||||||
console.warn('result', resultErrors);
|
this.logger.warn('result', resultErrors);
|
||||||
return Fail('Something went wrong');
|
return Fail('Something went wrong');
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataClass = plainToClass(type, result.data);
|
const dataClass = plainToClass(type, result.data);
|
||||||
const dataErrors = await validate(dataClass);
|
const dataErrors = await validate(dataClass);
|
||||||
if (dataErrors.length > 0) {
|
if (dataErrors.length > 0) {
|
||||||
console.warn('data', dataErrors);
|
this.logger.warn('data', dataErrors);
|
||||||
return Fail('Something went wrong');
|
return Fail('Something went wrong');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +96,7 @@ export class ApiService {
|
||||||
try {
|
try {
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
this.logger.warn(e);
|
||||||
return Fail('Something went wrong');
|
return Fail('Something went wrong');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,7 +111,7 @@ export class ApiService {
|
||||||
try {
|
try {
|
||||||
return await response.arrayBuffer();
|
return await response.arrayBuffer();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
this.logger.warn(e);
|
||||||
return Fail('Something went wrong');
|
return Fail('Something went wrong');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,7 +132,7 @@ export class ApiService {
|
||||||
|
|
||||||
return await window.fetch(url, options);
|
return await window.fetch(url, options);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.warn(e);
|
this.logger.warn(e);
|
||||||
return Fail('Something went wrong');
|
return Fail('Something went wrong');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@ import { KeyService } from './key.service';
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class UserService {
|
export class UserService {
|
||||||
|
private readonly logger = console;
|
||||||
|
|
||||||
public get liveUser() {
|
public get liveUser() {
|
||||||
return this.userSubject;
|
return this.userSubject;
|
||||||
}
|
}
|
||||||
|
@ -79,7 +81,7 @@ export class UserService {
|
||||||
|
|
||||||
const user = await this.extractUser(apikey);
|
const user = await this.extractUser(apikey);
|
||||||
if (HasFailed(user)) {
|
if (HasFailed(user)) {
|
||||||
console.warn(user.getReason());
|
this.logger.warn(user.getReason());
|
||||||
await this.logout();
|
await this.logout();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -88,7 +90,7 @@ export class UserService {
|
||||||
|
|
||||||
const fetchedUser = await this.fetchUser();
|
const fetchedUser = await this.fetchUser();
|
||||||
if (HasFailed(fetchedUser)) {
|
if (HasFailed(fetchedUser)) {
|
||||||
console.warn(fetchedUser.getReason());
|
this.logger.warn(fetchedUser.getReason());
|
||||||
await this.logout();
|
await this.logout();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -107,7 +109,7 @@ export class UserService {
|
||||||
const jwtData = plainToClass(JwtDataDto, decoded);
|
const jwtData = plainToClass(JwtDataDto, decoded);
|
||||||
const errors = await validate(jwtData);
|
const errors = await validate(jwtData);
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
console.warn(errors);
|
this.logger.warn(errors);
|
||||||
return Fail('Invalid token data');
|
return Fail('Invalid token data');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,8 @@ import { LoginControl } from './login.model';
|
||||||
styleUrls: ['./login.component.scss'],
|
styleUrls: ['./login.component.scss'],
|
||||||
})
|
})
|
||||||
export class LoginComponent implements OnInit {
|
export class LoginComponent implements OnInit {
|
||||||
|
private readonly logger = console;
|
||||||
|
|
||||||
model = new LoginControl();
|
model = new LoginControl();
|
||||||
loginFail = false;
|
loginFail = false;
|
||||||
|
|
||||||
|
@ -35,7 +37,7 @@ export class LoginComponent implements OnInit {
|
||||||
|
|
||||||
const user = await this.userService.login(data.username, data.password);
|
const user = await this.userService.login(data.username, data.password);
|
||||||
if (HasFailed(user)) {
|
if (HasFailed(user)) {
|
||||||
console.warn(user);
|
this.logger.warn(user);
|
||||||
this.loginFail = true;
|
this.loginFail = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsArray, IsBoolean,
|
||||||
IsDefined,
|
IsDefined, IsEnum, IsInt, IsNotEmpty,
|
||||||
IsNotEmpty,
|
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString, ValidateNested
|
||||||
IsInt,
|
|
||||||
ValidateNested,
|
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { EUser } from '../entities/user.entity';
|
import { EUser } from '../entities/user.entity';
|
||||||
import { Type } from 'class-transformer';
|
import { Permissions, PermissionsList } from './permissions';
|
||||||
|
|
||||||
// Api
|
// Api
|
||||||
|
|
||||||
|
@ -56,6 +54,11 @@ export class AuthMeResponse {
|
||||||
@Type(() => EUser)
|
@Type(() => EUser)
|
||||||
user: EUser;
|
user: EUser;
|
||||||
|
|
||||||
|
@IsDefined()
|
||||||
|
@IsArray()
|
||||||
|
@IsEnum(PermissionsList, { each: true })
|
||||||
|
permissions: Permissions;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsDefined()
|
@IsDefined()
|
||||||
newJwtToken: string;
|
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 tuple from '../types/tuple';
|
||||||
|
import { Permissions, PermissionsList } from './permissions';
|
||||||
|
|
||||||
// Config
|
// 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
|
// 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[];
|
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 { Exclude } from 'class-transformer';
|
||||||
import { IsArray, IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
|
import {
|
||||||
import { Roles, RolesList } from '../dto/roles.dto';
|
IsArray, IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString
|
||||||
|
} from 'class-validator';
|
||||||
|
import { Roles } from '../dto/roles.dto';
|
||||||
|
|
||||||
export class EUser {
|
export class EUser {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ -10,7 +14,7 @@ export class EUser {
|
||||||
username: string;
|
username: string;
|
||||||
|
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsEnum(RolesList, { each: true })
|
@IsString({ each: true })
|
||||||
roles: Roles;
|
roles: Roles;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|
Loading…
Reference in a new issue