Skip to content

Commit

Permalink
Merge pull request #4 from vh13294/dev
Browse files Browse the repository at this point in the history
Support NestJs 8
  • Loading branch information
vh13294 authored Sep 8, 2021
2 parents 8bfaf1d + 001ffcb commit 621cd38
Show file tree
Hide file tree
Showing 13 changed files with 15,691 additions and 5,735 deletions.
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import { UrlGeneratorModule } from 'nestjs-url-generator';
imports: [
UrlGeneratorModule.forRoot({
secret: 'secret', // optional, required only for signed URL
appUrl: 'localhost:3000',
appUrl: 'https://localhost:3000',
}),
],
})
Expand All @@ -69,7 +69,7 @@ Or Async Import With .ENV usage
```.env
APP_KEY=secret
APP_URL=localhost:3000
APP_URL=https://localhost:3000
```

> signed-url.config.ts
Expand Down Expand Up @@ -142,7 +142,7 @@ export class AppController {
};

// This will generate:
// localhost:3000/emailVerification/1.0/12?email=email%40email
// https://localhost:3000/emailVerification/1.0/12?email=email%40email
return this.urlGeneratorService.generateUrlFromController({
controller: AppController,
controllerMethod: AppController.prototype.emailVerification,
Expand All @@ -158,15 +158,15 @@ export class AppController {
There are two methods for generating url:

```typescript
signedControllerUrl({
SignControllerUrl({
controller,
controllerMethod,
/*?*/ expirationDate,
/*?*/ query,
/*?*/ params,
});

signedUrl({
SignUrl({
relativePath,
/*?*/ expirationDate,
/*?*/ query,
Expand All @@ -183,13 +183,13 @@ import { UrlGeneratorService } from 'nestjs-url-generator';
export class AppController {
constructor(private readonly urlGeneratorService: UrlGeneratorService) {}

@Get('makeSignedUrl')
async makeSignedUrl(): Promise<string> {
@Get('makeSignUrl')
async makeSignUrl(): Promise<string> {
// This will generate:
// localhost:3000/emailVerification?
// https://localhost:3000/emailVerification?
// expirationDate=2021-12-12T00%3A00%3A00.000Z&
// signed=84b5a021c433d0ee961932ac0ec04d5dd5ffd6f7fdb60b46083cfe474dfae3c0
return this.urlGeneratorService.signedControllerUrl({
return this.urlGeneratorService.SignControllerUrl({
controller: AppController,
controllerMethod: AppController.prototype.emailVerification,
expirationDate: new Date('2021-12-12'),
Expand All @@ -215,7 +215,7 @@ The difference between params & query in ExpressJS

## Using Guard

You can use SignedUrlGuard to verify the signed url in controller.
You can use SignUrlGuard to verify the signed url in controller.

If the url has been tampered or when the expiration date is due, then a Forbidden exception will be thrown.

Expand Down Expand Up @@ -253,6 +253,6 @@ require('crypto').randomBytes(64, (err, buf) => {

### TODO

- [ ] Create test (expiration, query clash, tampered, with or without globalPrefix, request with query & param)
- [ ] Create unit test (expiration, tampered, with or without globalPrefix, request with or without query & param, if target for signerUrl doesn't have guard)

- [ ] Automate CI, npm run build, push, npm publish
- [ ] Add warning if target for signerUrl doesn't have guard
61 changes: 31 additions & 30 deletions lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,55 @@
import { createHmac, timingSafeEqual } from 'crypto';
import { stringify as qsStringify } from 'qs';

import { PATH_METADATA } from '@nestjs/common/constants';
import { Controller } from '@nestjs/common/interfaces/controllers/controller.interface';

import { RESERVED_QUERY_PARAM_NAMES } from './url-generator.constants';
import { BadRequestException } from '@nestjs/common';
import { ControllerMethod } from './interfaces';
import { GUARDS_METADATA, PATH_METADATA } from '@nestjs/common/constants';

import { ControllerMethod, Query, Params, ControllerClass } from './interfaces';
import { SignedUrlGuard } from './signed-url-guard';

export function generateUrl(
appUrl: string,
prefix: string,
relativePath: string,
query: any = {},
params: any = {},
query?: Query,
params?: Params,
): string {
relativePath = putParamsInUrl(relativePath, params);
const path = joinRoutes(appUrl, prefix, relativePath);
const queryString = stringifyQueryParams(query);
const fullPath = appendQueryParams(path, queryString);
const queryString = stringifyQuery(query);
const fullPath = appendQuery(path, queryString);
return fullPath;
}

export function stringifyQueryParams(query: Record<string, unknown>): string {
export function stringifyQuery(query?: Query): string {
return qsStringify(query);
}

export function getControllerMethodRoute(
controller: Controller,
controller: ControllerClass,
controllerMethod: ControllerMethod,
): string {
const controllerRoute = Reflect.getMetadata(PATH_METADATA, controller);
const methodRoute = Reflect.getMetadata(PATH_METADATA, controllerMethod);
return joinRoutes(controllerRoute, methodRoute);
}

export function checkIfMethodHasSignedGuardDecorator(
controller: ControllerClass,
controllerMethod: ControllerMethod,
): void {
const arrOfClasses = Reflect.getMetadata(GUARDS_METADATA, controllerMethod);
const errorMessage = `Please add SignedUrlGuard to ${controller.name}.${controllerMethod.name}`;
if (!arrOfClasses) {
throw new BadRequestException(errorMessage);
}

const guardExist = arrOfClasses.includes(SignedUrlGuard);
if (!guardExist) {
throw new BadRequestException(errorMessage);
}
}

export function generateHmac(url: string, secret?: string): string {
if (!secret) {
throw new BadRequestException('Secret key is needed for signing URL');
Expand All @@ -49,30 +64,16 @@ export function isSignatureEqual(signed: string, hmacValue: string): boolean {
return timingSafeEqual(Buffer.from(signed), Buffer.from(hmacValue));
}

export function signatureHasExpired(expiryDate: Date): boolean {
export function signatureHasExpired(expirationDate: Date): boolean {
const currentDate = new Date();
return currentDate > expiryDate;
}

export function checkIfQueryHasReservedKeys(
query: Record<string, unknown>,
): boolean {
const keyArr = Object.keys(query);
return RESERVED_QUERY_PARAM_NAMES.some((r: string) => keyArr.includes(r));
}

export function isObjectEmpty(obj = {}): boolean {
return Object.keys(obj).length == 0;
return currentDate > expirationDate;
}

function isRouteNotEmpty(route: string): boolean {
return !!route && route !== '/';
}

function isParamsNameInUrl(
route: string,
params: Record<string, string>,
): boolean {
function isParamsNameInUrl(route: string, params: Params): boolean {
const routeParts = route
.split('/')
.filter((path) => path[0] === ':')
Expand All @@ -87,14 +88,14 @@ function joinRoutes(...routes: string[]): string {
return routes.filter((route) => isRouteNotEmpty(route)).join('/');
}

function appendQueryParams(route: string, query: string): string {
export function appendQuery(route: string, query: string): string {
if (query) {
return `${route}?${query}`;
}
return route;
}

function putParamsInUrl(route: string, params: Record<string, string>): string {
function putParamsInUrl(route: string, params?: Params): string {
if (params) {
if (isParamsNameInUrl(route, params)) {
for (const [key, value] of Object.entries(params)) {
Expand Down
58 changes: 44 additions & 14 deletions lib/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,67 @@
import { Controller } from '@nestjs/common/interfaces/controllers/controller.interface';
import { Request } from 'express';
import { Controller } from '@nestjs/common/interfaces';

export interface ControllerClass extends Controller {
name: string;
}

export type ControllerMethod = (...args: any[]) => Promise<any> | any;

export interface Query {
[key: string]: any;
}
export interface Params {
[key: string]: string;
}

export interface ReservedQuery {
signed?: string;
expirationDate?: string;
}

type SignedQuery = Query & ReservedQuery;

type Not<T> = {
[Key in keyof T]?: never;
};

type NotReservedQuery = Query & Not<ReservedQuery>;

export interface RequestWithSignature extends Request {
query: SignedQuery;
}

export interface GenerateUrlFromControllerArgs {
controller: Controller;
controller: ControllerClass;
controllerMethod: ControllerMethod;
query?: any;
params?: any;
query?: Query;
params?: Params;
}

export interface GenerateUrlFromPathArgs {
relativePath: string;
query?: any;
params?: any;
query?: Query;
params?: Params;
}

export interface SignedControllerUrlArgs {
controller: Controller;
export interface SignControllerUrlArgs {
controller: ControllerClass;
controllerMethod: ControllerMethod;
expirationDate?: Date;
query?: any;
params?: any;
query?: NotReservedQuery;
params?: Params;
}

export interface SignedUrlArgs {
export interface SignUrlArgs {
relativePath: string;
expirationDate?: Date;
query?: any;
params?: any;
query?: NotReservedQuery;
params?: Params;
}

export interface IsSignatureValidArgs {
protocol: string;
host: string;
routePath: string;
query?: any;
query: SignedQuery;
}
14 changes: 12 additions & 2 deletions lib/signed-url-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
MethodNotAllowedException,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { RequestWithSignature } from './interfaces';
import { UrlGeneratorService } from './url-generator.service';

@Injectable()
Expand All @@ -18,16 +19,25 @@ export class SignedUrlGuard implements CanActivate {
return this.validateRequest(request);
}

private validateRequest(request: any): boolean {
private validateRequest(request: RequestWithSignature): boolean {
if (!request.headers.host) {
throw new MethodNotAllowedException(
'Unable to derive host name from request',
);
}

if (!request.path) {
throw new MethodNotAllowedException('Unable to derive path from request');
}

if (!request.query) {
throw new MethodNotAllowedException('Signed Query is invalid');
}

return this.urlGeneratorService.isSignatureValid({
protocol: request.protocol,
host: request.headers.host,
routePath: request._parsedUrl.pathname,
routePath: request.path,
query: request.query,
});
}
Expand Down
2 changes: 0 additions & 2 deletions lib/url-generator.constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
export const URL_GENERATOR_MODULE_OPTIONS = 'UrlGeneratorModuleOptions';

export const RESERVED_QUERY_PARAM_NAMES = ['expirationDate', 'signed'];
Loading

0 comments on commit 621cd38

Please sign in to comment.