Skip to content

Commit

Permalink
Merge pull request #380 from internxt/feat/support-actions-on-sharing…
Browse files Browse the repository at this point in the history
…s-root

[PB-2430] fix: allow sharing password deactivation, workspace sharing permissions fixed
  • Loading branch information
apsantiso authored Aug 12, 2024
2 parents 8873a23 + 56d7e7b commit 6e9c077
Show file tree
Hide file tree
Showing 16 changed files with 548 additions and 211 deletions.
133 changes: 133 additions & 0 deletions src/common/extract-data-from-request.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { BadRequestException, ExecutionContext, Logger } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import {
extractDataFromRequest,
DataSource,
} from './extract-data-from-request'; // Update with actual path

describe('extractDataFromRequest', () => {
let request;
let reflector: Reflector;
let context: ExecutionContext;

beforeEach(() => {
request = {
body: { field1: 'value1' },
query: { field2: 'value2' },
params: { field3: 'value3' },
headers: { field4: 'value4' },
};

reflector = new Reflector();
context = {
getHandler: jest.fn(),
} as unknown as ExecutionContext;

jest.spyOn(Logger.prototype, 'error').mockImplementation(() => {});
jest.spyOn(reflector, 'get').mockImplementation();
});

it('When all fields are present, then it should extract data correctly', () => {
const dataSources: DataSource[] = [
{ sourceKey: 'body', fieldName: 'field1' },
{ sourceKey: 'query', fieldName: 'field2' },
{
sourceKey: 'params',
fieldName: 'field3',
newFieldName: 'renamedField3',
},
{ sourceKey: 'headers', fieldName: 'field4' },
];

(reflector.get as jest.Mock).mockReturnValue({ dataSources });
(context.getHandler as jest.Mock).mockReturnValue('handler');

const result = extractDataFromRequest(request, reflector, context);

expect(result).toEqual({
field1: 'value1',
field2: 'value2',
renamedField3: 'value3',
field4: 'value4',
});
});

it('When a required field is missing, then it should throw BadRequestException', () => {
const dataSources: DataSource[] = [
{ sourceKey: 'body', fieldName: 'missingField' },
];

(reflector.get as jest.Mock).mockReturnValue({ dataSources });
(context.getHandler as jest.Mock).mockReturnValue('handler');

expect(() => extractDataFromRequest(request, reflector, context)).toThrow(
BadRequestException,
);
expect(Logger.prototype.error).toHaveBeenCalledWith(
'Missing required field for guard! field: missingField',
);
});

it('When a provided value is given, then it should use the provided value', () => {
const dataSources: DataSource[] = [
{ sourceKey: 'body', fieldName: 'field1', value: 'providedValue' },
];

(reflector.get as jest.Mock).mockReturnValue({ dataSources });
(context.getHandler as jest.Mock).mockReturnValue('handler');

const result = extractDataFromRequest(request, reflector, context);

expect(result).toEqual({
field1: 'providedValue',
});
});

it('When the provided value is null or undefined, then it should throw BadRequestException', () => {
const dataSources: DataSource[] = [
{ sourceKey: 'body', fieldName: 'field1', value: null },
];

(reflector.get as jest.Mock).mockReturnValue({ dataSources });
(context.getHandler as jest.Mock).mockReturnValue('handler');

expect(() => extractDataFromRequest(request, reflector, context)).toThrow(
BadRequestException,
);
expect(Logger.prototype.error).toHaveBeenCalledWith(
'Missing required field for guard! field: field1',
);
});

it('When fields need to be renamed, then it should rename fields correctly', () => {
const dataSources: DataSource[] = [
{ sourceKey: 'query', fieldName: 'field2', newFieldName: 'newField2' },
];

(reflector.get as jest.Mock).mockReturnValue({ dataSources });
(context.getHandler as jest.Mock).mockReturnValue('handler');

const result = extractDataFromRequest(request, reflector, context);

expect(result).toEqual({
newField2: 'value2',
});
});

it('When multiple data sources are provided, then it should handle all sources correctly', () => {
const dataSources: DataSource[] = [
{ sourceKey: 'body', fieldName: 'field1' },
{ sourceKey: 'query', fieldName: 'field2' },
];

(reflector.get as jest.Mock).mockReturnValue({ dataSources });
(context.getHandler as jest.Mock).mockReturnValue('handler');

const result = extractDataFromRequest(request, reflector, context);

expect(result).toEqual({
field1: 'value1',
field2: 'value2',
});
});
});
55 changes: 55 additions & 0 deletions src/common/extract-data-from-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
BadRequestException,
ExecutionContext,
Logger,
SetMetadata,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';

const extractDataFromRequestMetaName = 'dataFromRequest';

export const GetDataFromRequest = (dataSources: DataSource[]) =>
SetMetadata(extractDataFromRequestMetaName, { dataSources });

export interface DataSource {
sourceKey?: 'body' | 'query' | 'params' | 'headers';
newFieldName?: string; // renames field name to be passed to guard
fieldName: string;
value?: any;
}

export const extractDataFromRequest = (
request: Request,
reflector: Reflector,
context: ExecutionContext,
) => {
const metadataOptions = reflector.get<{ dataSources: DataSource[] }>(
extractDataFromRequestMetaName,
context.getHandler(),
);

const { dataSources } = metadataOptions;

const extractedData = {};

for (const { sourceKey, fieldName, value, newFieldName } of dataSources) {
const extractedValue =
value !== undefined ? value : request[sourceKey][fieldName];

const isValueUndefined =
extractedValue === undefined || extractedValue === null;

if (isValueUndefined) {
new Logger().error(
`Missing required field for guard! field: ${fieldName}`,
);
throw new BadRequestException(`Missing required field: ${fieldName}`);
}

const targetFieldName = newFieldName ?? fieldName;

extractedData[targetFieldName] = extractedValue;
}

return extractedData;
};
55 changes: 46 additions & 9 deletions src/modules/file/file.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { CreateFileDto } from './dto/create-file.dto';
import { RequiredSharingPermissions } from '../sharing/guards/sharing-permissions.decorator';
import { SharingActionName } from '../sharing/sharing.domain';
import { SharingPermissionsGuard } from '../sharing/guards/sharing-permissions.guard';
import { GetDataFromRequest } from '../../common/extract-data-from-request';

const filesStatuses = ['ALL', 'EXISTS', 'TRASHED', 'DELETED'] as const;

Expand Down Expand Up @@ -80,9 +81,18 @@ export class FileController {
}

@Get('/:uuid/meta')
@WorkspacesInBehalfValidationFile([
{ sourceKey: 'params', fieldName: 'uuid', newFieldName: 'itemId' },
@GetDataFromRequest([
{
sourceKey: 'params',
fieldName: 'uuid',
newFieldName: 'itemId',
},
{
fieldName: 'itemType',
value: 'file',
},
])
@WorkspacesInBehalfValidationFile()
async getFileMetadata(
@UserDecorator() user: User,
@Param('uuid') fileUuid: File['uuid'],
Expand Down Expand Up @@ -112,9 +122,18 @@ export class FileController {
}

@Put('/:uuid')
@WorkspacesInBehalfValidationFile([
{ sourceKey: 'params', fieldName: 'uuid', newFieldName: 'itemId' },
@GetDataFromRequest([
{
sourceKey: 'params',
fieldName: 'uuid',
newFieldName: 'itemId',
},
{
fieldName: 'itemType',
value: 'file',
},
])
@WorkspacesInBehalfValidationFile()
async replaceFile(
@UserDecorator() user: User,
@Param('uuid') fileUuid: File['uuid'],
Expand Down Expand Up @@ -152,10 +171,19 @@ export class FileController {
required: true,
description: 'file uuid',
})
@RequiredSharingPermissions(SharingActionName.RenameItems)
@WorkspacesInBehalfValidationFile([
{ sourceKey: 'params', fieldName: 'uuid', newFieldName: 'itemId' },
@GetDataFromRequest([
{
sourceKey: 'params',
fieldName: 'uuid',
newFieldName: 'itemId',
},
{
fieldName: 'itemType',
value: 'file',
},
])
@RequiredSharingPermissions(SharingActionName.RenameItems)
@WorkspacesInBehalfValidationFile()
async updateFileMetadata(
@UserDecorator() user: User,
@Param('uuid', ValidateUUIDPipe)
Expand Down Expand Up @@ -241,9 +269,18 @@ export class FileController {
}

@Patch('/:uuid')
@WorkspacesInBehalfValidationFile([
{ sourceKey: 'params', fieldName: 'uuid', newFieldName: 'itemId' },
@GetDataFromRequest([
{
sourceKey: 'params',
fieldName: 'uuid',
newFieldName: 'itemId',
},
{
fieldName: 'itemType',
value: 'file',
},
])
@WorkspacesInBehalfValidationFile()
async moveFile(
@UserDecorator() user: User,
@Param('uuid') fileUuid: File['uuid'],
Expand Down
Loading

0 comments on commit 6e9c077

Please sign in to comment.