Skip to content

Commit

Permalink
Merge pull request #13 from gantispam/master
Browse files Browse the repository at this point in the history
#11 Store auth information on CrudRequest object
  • Loading branch information
zaro committed Mar 16, 2023
2 parents e8fddfe + 3b15ce8 commit 5e74c1c
Show file tree
Hide file tree
Showing 11 changed files with 196 additions and 14 deletions.
2 changes: 1 addition & 1 deletion integration/crud-typeorm/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class AuthGuard implements CanActivate {

async canActivate(ctx: ExecutionContext): Promise<boolean> {
const req = ctx.switchToHttp().getRequest();
req[USER_REQUEST_KEY] = await this.usersService.findOne(1);
req[USER_REQUEST_KEY] = await this.usersService.findOne({where: {id: 1}});

return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
QueryExtra,
QueryFields,
QueryFilter,
QueryFilterArr,
Expand All @@ -21,4 +22,5 @@ export interface CreateQueryParams {
page?: number;
resetCache?: boolean;
includeDeleted?: number;
extra?: QueryExtra;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ObjectLiteral } from '@dataui/crud-util';
import { QueryFields, QueryFilter, QueryJoin, QuerySort, SCondition } from '../types';

export interface ParsedRequestParams {
export interface ParsedRequestParams<EXTRA = {}> {
fields: QueryFields;
paramsFilter: QueryFilter[];
authPersist: ObjectLiteral;
Expand All @@ -15,4 +15,11 @@ export interface ParsedRequestParams {
page: number;
cache: number;
includeDeleted: number;
/**
* Extra options.
*
* Custom extra option come from Request and can be used anywhere you want for your business rules.
* CrudRequest lib. do not evaluat this attribut.
*/
extra?: EXTRA;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export interface RequestQueryBuilderOptions {
page?: string | string[];
cache?: string | string[];
includeDeleted?: string | string[];
extra?: string | string[];
};
}
1 change: 1 addition & 0 deletions packages/crud-request/src/request-query.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export class RequestQueryBuilder {
page: 'page',
cache: 'cache',
includeDeleted: 'include_deleted',
extra: 'extra.',
},
};
private paramNames: {
Expand Down
38 changes: 37 additions & 1 deletion packages/crud-request/src/request-query.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
validateUUID,
} from './request-query.validator';
import {
ComparisonOperator,
ComparisonOperator, QueryExtra,
QueryFields,
QueryFilter,
QueryJoin,
Expand All @@ -54,6 +54,7 @@ export class RequestQueryParser implements ParsedRequestParams {
public page: number;
public cache: number;
public includeDeleted: number;
public extra?: QueryExtra;

private _params: any;
private _query: any;
Expand Down Expand Up @@ -83,6 +84,7 @@ export class RequestQueryParser implements ParsedRequestParams {
page: this.page,
cache: this.cache,
includeDeleted: this.includeDeleted,
extra: this.extra,
};
}

Expand Down Expand Up @@ -129,6 +131,8 @@ export class RequestQueryParser implements ParsedRequestParams {
'includeDeleted',
this.numericParser.bind(this, 'includeDeleted'),
)[0];

this.extra = this.parseExtraFromQueryParam();
}
}

Expand Down Expand Up @@ -209,6 +213,38 @@ export class RequestQueryParser implements ParsedRequestParams {
return [];
}

private parseExtraFromQueryParam(): QueryExtra {
const params = Array.isArray(this._options.paramNamesMap.extra) ? this._options.paramNamesMap.extra : [this._options.paramNamesMap.extra];
const extraKeys = Object
.keys(this._query || {})
.filter(k => params.find(p => k?.startsWith(p)))
.reduce((o, k) => {
const key = k.replace('extra.', '');
this.parseDotChainToObject(this._query[k], key, o)
return o;
}, {});
return Object.keys(extraKeys).length > 0 ? extraKeys : undefined;
}

/**
* Build an object from data and composite key.
*
* @param data to used on parse workflow
* @param key composite key as 'foo.bar.hero'
* @param result object with parsed "data" and "key" structure
* @private
*/
private parseDotChainToObject(data: any, key: string, result = {}): QueryExtra {
if (key.includes('.')) {
const keys = key.split('.');
const firstKey = keys.shift();
result[firstKey] = {};
this.parseDotChainToObject(data, keys.join('.'), result[firstKey]);
} else {
result[key] = this.parseValue(data)
}
}

private parseValue(val: any) {
try {
const parsed = JSON.parse(val);
Expand Down
3 changes: 3 additions & 0 deletions packages/crud-request/src/types/request-query.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export interface QuerySort {
order: QuerySortOperator;
}

/** Extra object or null */
export type QueryExtra = any | undefined;

export type QuerySortArr = [string, QuerySortOperator];

export type QuerySortOperator = 'ASC' | 'DESC';
Expand Down
64 changes: 64 additions & 0 deletions packages/crud-request/test/request-query.parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,70 @@ describe('#request-query', () => {
});
});

describe('#parse extra', () => {
it('should set undefined 1', () => {
const query = {};
const test = qp.parseQuery(query);
expect(test.extra).toBeUndefined();
});
it('should set undefined 2', () => {
const query = { extra: '' };
const test = qp.parseQuery(query);
expect(test.extra).toBeUndefined();
});
it('should set as string', () => {
const query = {'extra': 'bar'};
const test = qp.parseQuery(query);
expect(test.extra).toBeUndefined();
});
it('should set as object 1', () => {
const query = {'extra.foo': 'bar'};
const test = qp.parseQuery(query);
expect(test.extra).toEqual({foo: 'bar'});
});
it('should set as object 2', () => {
const query = {'extra.foo': 'bar', 'extra.foo2': 'bar2'};
const test = qp.parseQuery(query);
expect(test.extra).toEqual({foo: 'bar', foo2: 'bar2'});
});
it('should set as object contain array', () => {
const query = {'extra.foo': ['bar', 'bar2']};
const test = qp.parseQuery(query);
expect(test.extra).toEqual({foo: ['bar','bar2']});
});

it('should set as simple object', () => {
const query = {'extra.foo': 'bar'};
const test = qp.parseQuery(query);
expect(test.extra).toEqual({foo: 'bar'});
});

it('should set as hero object', () => {
const query = {'extra.foo.bar.hero': 'me'};
const test = qp.parseQuery(query);
expect(test.extra).toEqual({foo: {bar: {hero: 'me'}}});
});

it('should set as number object', () => {
const query = {'extra.foo.bar.number': 100};
const test = qp.parseQuery(query);
expect(test.extra).toEqual({foo: {bar: {number: 100}}});
});

it('should set as object with undefined value', () => {
const query = {'extra.foo.bar.none': undefined};
const test = qp.parseQuery(query);
expect(test.extra).toEqual({foo: {bar: {none: undefined}}});
});

it('should set as object with null value', () => {
const query = {'extra.foo.bar.none': null};
const test = qp.parseQuery(query);
expect(test.extra).toEqual({foo: {bar: {none: null}}});
});

});

describe('#setAuthPersist', () => {
it('it should set authPersist, 1', () => {
qp.setAuthPersist();
Expand Down
20 changes: 16 additions & 4 deletions packages/crud/src/interceptors/crud-request.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,18 @@ export class CrudRequestInterceptor extends CrudBaseInterceptor

parser.parseQuery(req.query, crudOptions.operators?.custom);

let auth = null;
if (!isNil(ctrlOptions)) {
const search = this.getSearch(parser, crudOptions, action, req.params);
const auth = this.getAuth(parser, crudOptions, req);
auth = this.getAuth(parser, crudOptions, req);
parser.search = auth.or
? { $or: [auth.or, { $and: search }] }
: { $and: [auth.filter, ...search] };
} else {
parser.search = { $and: this.getSearch(parser, crudOptions, action) };
}

req[PARSED_CRUD_REQUEST_KEY] = this.getCrudRequest(parser, crudOptions);
req[PARSED_CRUD_REQUEST_KEY] = this.getCrudRequest(parser, crudOptions, auth?.auth);
}

return next.handle();
Expand All @@ -58,10 +59,10 @@ export class CrudRequestInterceptor extends CrudBaseInterceptor
getCrudRequest(
parser: RequestQueryParser,
crudOptions: Partial<MergedCrudOptions>,
auth?: any,
): CrudRequest {
const parsed = parser.getParsed();
const { query, routes, params, operators } = crudOptions;

return {
parsed,
options: {
Expand All @@ -70,6 +71,7 @@ export class CrudRequestInterceptor extends CrudBaseInterceptor
params,
operators,
},
auth,
};
}

Expand Down Expand Up @@ -159,7 +161,7 @@ export class CrudRequestInterceptor extends CrudBaseInterceptor
parser: RequestQueryParser,
crudOptions: Partial<MergedCrudOptions>,
req: any,
): { filter?: any; or?: any } {
): { filter?: any; or?: any; auth?: any } {
const auth: any = {};

/* istanbul ignore else */
Expand All @@ -168,6 +170,16 @@ export class CrudRequestInterceptor extends CrudBaseInterceptor
? req[crudOptions.auth.property]
: req;

if (crudOptions.auth.property && req[crudOptions.auth.property]) {
if (typeof req[crudOptions.auth.property] === 'object') {
if (Object.keys(req[crudOptions.auth.property]).length > 0) {
auth.auth = req[crudOptions.auth.property];
}
} else {
auth.auth = req[crudOptions.auth.property];
}
}

if (isFunction(crudOptions.auth.or)) {
auth.or = crudOptions.auth.or(userOrRequest);
}
Expand Down
6 changes: 4 additions & 2 deletions packages/crud/src/interfaces/crud-request.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { ParsedRequestParams } from '@dataui/crud-request';

import { CrudRequestOptions } from '../interfaces';

export interface CrudRequest {
parsed: ParsedRequestParams;
export interface CrudRequest<AUTH = {}, EXTRA = {}> {
parsed: ParsedRequestParams<EXTRA>;
options: CrudRequestOptions;
/** authenticated user's from request */
auth?: AUTH;
}
64 changes: 59 additions & 5 deletions packages/crud/test/crud-request.interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {
Controller,
Get,
CanActivate,
Controller, ExecutionContext,
Get, Injectable,
Param,
ParseIntPipe,
Query,
Query, UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { NestApplication } from '@nestjs/core';
import { APP_GUARD, NestApplication } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import { RequestQueryBuilder } from '@dataui/crud-request';
import * as supertest from 'supertest';
Expand All @@ -15,9 +16,22 @@ import { CrudRequestInterceptor } from '../src/interceptors';
import { CrudRequest } from '../src/interfaces';
import { TestModel } from './__fixture__/models';
import { TestService } from './__fixture__/services';
import { USER_REQUEST_KEY } from '../../../integration/crud-typeorm/constants';

const MOCKED_AUTH_OBJ = {id: 1, firstname: 'foo', lastname: 'bar'}

// tslint:disable:max-classes-per-file
describe('#crud', () => {

@Injectable()
class AuthGuardMock implements CanActivate {
async canActivate(ctx: ExecutionContext): Promise<boolean> {
const req = ctx.switchToHttp().getRequest();
req[USER_REQUEST_KEY] = MOCKED_AUTH_OBJ;
return true;
}
}

@UseInterceptors(CrudRequestInterceptor)
@Controller('test')
class TestController {
Expand Down Expand Up @@ -123,18 +137,37 @@ describe('#crud', () => {
constructor(public service: TestService<TestModel>) {}
}

@Crud({
model: { type: TestModel },
})
@CrudAuth({
property: 'user',
})
@UseGuards(AuthGuardMock)
@Controller('test-with-auth')
class TestWithAuthController {
constructor(public service: TestService<TestModel>) {}
}

let $: supertest.SuperTest<supertest.Test>;
let app: NestApplication;

beforeAll(async () => {
const module = await Test.createTestingModule({
providers: [TestService],
providers: [
TestService,
{
provide: APP_GUARD,
useClass: AuthGuardMock,
},
],
controllers: [
TestController,
Test2Controller,
Test3Controller,
Test4Controller,
Test5Controller,
TestWithAuthController,
],
}).compile();
app = module.createNestApplication();
Expand Down Expand Up @@ -255,5 +288,26 @@ describe('#crud', () => {
const search = { $and: [{ user: 'test', buz: 1 }, { name: 'persist' }] };
expect(res.body.parsed.search).toMatchObject(search);
});


it('should not contain auth object', async () => {
const res = await $.get('/test2')
.send({})
.expect(200);

const { auth } = res.body.req;
expect(auth).toBeUndefined();
});

it('should contain auth object', async () => {
const res = await $.get('/test-with-auth')
.send({})
.expect(200);

const { auth } = res.body.req;
expect(auth).toMatchObject(MOCKED_AUTH_OBJ);
});


});
});

0 comments on commit 5e74c1c

Please sign in to comment.