Picsur/backend/src/managers/auth/guards/main.guard.ts

116 lines
3.7 KiB
TypeScript

import { ExecutionContext, Injectable, Logger } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { EUser, EUserSchema } from 'picsur-shared/dist/entities/user.entity';
import { Fail, Failable, FT, HasFailed } from 'picsur-shared/dist/types';
import { makeUnique } from 'picsur-shared/dist/util/unique';
import { UserDbService } from '../../../collections/user-db/user-db.service';
import { Permissions } from '../../../models/constants/permissions.const';
import { isPermissionsArray } from '../../../models/validators/permissions.validator';
// This guard extends both the jwt authenticator and the guest authenticator
// The order matters here, because this results in the guest authenticator being used as a fallback
// This way a user will get his own account when logged in, but received guest permissions when not
@Injectable()
export class MainAuthGuard extends AuthGuard(['apikey', 'jwt', 'guest']) {
private readonly logger = new Logger(MainAuthGuard.name);
constructor(
private readonly reflector: Reflector,
private readonly usersService: UserDbService,
) {
super();
}
override async canActivate(context: ExecutionContext): Promise<boolean> {
// Sanity check
const result = await super.canActivate(context);
if (result !== true) {
throw Fail(
FT.Internal,
undefined,
'Main Auth has denied access, this should not happen',
);
}
const user = await this.validateUser(
context.switchToHttp().getRequest().user,
);
if (!user.id) {
throw Fail(
FT.Internal,
undefined,
'User has no id, this should not happen',
);
}
// These are the permissions required to access the route
const permissions = this.extractPermissions(context);
if (HasFailed(permissions)) {
throw Fail(
FT.Internal,
undefined,
'Fetching route permission failed: ' + permissions.getReason(),
);
}
// These are the permissions the user has
const userPermissions = await this.usersService.getPermissions(user.id);
if (HasFailed(userPermissions)) {
throw userPermissions;
}
context.switchToHttp().getRequest().userPermissions = userPermissions;
if (permissions.every((permission) => userPermissions.includes(permission)))
return true;
else throw Fail(FT.Permission, 'Permission denied');
}
private extractPermissions(context: ExecutionContext): Failable<Permissions> {
const handlerName = context.getHandler().name;
// Fall back to class permissions if none on function
// But function has higher priority than class
const permissionsHandler: Permissions | undefined =
this.reflector.get<Permissions>('permissions', context.getHandler());
const permissionsClass: Permissions | undefined =
this.reflector.get<Permissions>('permissions', context.getClass());
if (permissionsHandler === undefined && permissionsClass === undefined) {
return Fail(
FT.Internal,
undefined,
`${handlerName} does not have any permissions defined, denying access`,
);
}
const permissions = makeUnique([
...(permissionsHandler ?? []),
...(permissionsClass ?? []),
]);
if (!isPermissionsArray(permissions))
return Fail(
FT.Internal,
undefined,
`Permissions for ${handlerName} is not a string array`,
);
return permissions;
}
private async validateUser(user: EUser): Promise<EUser> {
const result = EUserSchema.safeParse(user);
if (!result.success) {
throw Fail(
FT.Internal,
undefined,
`Invalid user object, where it should always be valid: ${result.error}`,
);
}
return result.data;
}
}