├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── index.ts ├── nest-cli.json ├── package-lock.json ├── package.json ├── src ├── decorators │ ├── clear-cookies.decorator.ts │ ├── get-cookies.decorator.ts │ ├── index.ts │ └── set-cookies.decorator.ts ├── index.ts ├── interceptors │ ├── clear-cookies.interceptor.ts │ └── set-cookies.interceptor.ts └── interfaces │ ├── cookies.interfaces.ts │ └── index.ts ├── test ├── app.e2e-spec.ts ├── jest-e2e.json ├── src │ ├── app.controller.ts │ └── app.module.ts └── views │ └── clear.hbs ├── tsconfig.build.json ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | tsconfig.json 3 | tsconfig.build.json 4 | nodemon.json 5 | .prettierrc 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": {} 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | RELEASE 1.0.0 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. [Fork it](https://help.github.com/articles/fork-a-repo/) 4 | 2. Install dependencies (`npm install`) 5 | 3. Create your feature branch (`git checkout -b my-new-feature`) 6 | 4. Commit your changes (`git commit -am 'Added some feature'`) 7 | 5. Test your changes (`npm test`) 8 | 6. Push to the branch (`git push origin my-new-feature`) 9 | 7. [Create new Pull Request](https://help.github.com/articles/creating-a-pull-request/) 10 | 11 | ## Testing 12 | 13 | We use [Jest](https://github.com/facebook/jest) to write tests. Run our test suite with this command: 14 | 15 | ``` 16 | npm test 17 | ``` 18 | 19 | ## Code Style 20 | 21 | We use [Prettier](https://prettier.io/) and tslint to maintain code style and best practices. 22 | Please make sure your PR adheres to the guides by running: 23 | 24 | ``` 25 | npm run format 26 | ``` 27 | 28 | and 29 | ``` 30 | npm run lint 31 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 John Biundo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | 5 | Nest Logo 6 | 7 |
8 | 9 |

Decorators for Managing Cookies with NestJS

10 | 11 |
12 | License 13 | npm version 14 | 15 | Built with NestJS 16 | 17 |
18 | 19 | ### Version Compatibility - Nest v7 20 | 21 | For the time being at least (until I can invest more time in this), Nest v7 will be supported in the `@nestjsplus/cookies@1.1.x` code line (sorry if that's not correct SEMVER; what I mean is version `1.1.x` is built on Nest 7). Nest 6 will be supported on the `1.0.x` version line. 22 | 23 | The incompatibility is due to a change in Nest's `createParamDecorator()` function, and at the moment I haven't tried to figure out how to maintain compatibility across the two. 24 | 25 | Please file an issue if you have difficulties with `v1.1.x` and Nest v7. 26 | 27 | ### Installation 28 | 29 | ```bash 30 | npm install @nestjsplus/cookies 31 | ``` 32 | 33 | ### Motivation 34 | NestJS doesn't currently have decorators for getting and setting cookies. While it's not 35 | too hard to read cookies, it's convenient to have a parameter decorator to do so. 36 | 37 | ```typescript 38 | @Post('login') 39 | login(@Cookies() cookies) { 40 | console.log('Got cookies:', cookies); 41 | } 42 | ``` 43 | 44 | Setting cookies is a little less straightforward. You either need to utilize the platform-specific 45 | response (`res`) object, or write an interceptor. The former is pretty straightforward, though 46 | takes a non-Nest-like imperative style. It also puts you into 47 | [manual response mode](https://docs.nestjs.com/controllers#routing), 48 | meaning you can no longer rely on features like `@Render()`, `@HttpCode()` or [interceptors that modify the response](https://docs.nestjs.com/interceptors#response-mapping), 49 | and makes testing harder (you'll have to mock the response 50 | object, etc.). The `@SetCookies()` decorator from this package wraps an interceptor 51 | in a declarative decorator that solves these issues. 52 | 53 | Collectively, the `@Cookies()`, `@SignedCookies()`, `@SetCookies()` and `@ClearCookies()` decorators in this package 54 | provide a convenient set of features that make it easier to manage cookies in a standard and declarative way, 55 | and minimize boilerplate code. 56 | 57 | ### See Also 58 | 59 | > **Note**: While still maintained, the `@nestsplus/redirect` package is essentially superceded by built-in support (I made the contribution) in `@nestjs/core`. 60 | 61 | If you like these decorators, you may also be interested in the 62 | [NestJS Redirect decorator](https://github.com/nestjsplus/redirect). 63 | 64 | 65 | ### Importing the Decorators 66 | Import the decorators, just as you would other Nest decorators, in the controllers 67 | that use them as shown below: 68 | 69 | ```typescript 70 | import { Controller, Get } from '@nestjs/common'; 71 | import { AppService } from './app.service'; 72 | import { Cookies, SignedCookies } from '@nestjsplus/cookies'; 73 | 74 | @Controller() 75 | export class AppController { 76 | ... 77 | ``` 78 | 79 | ### Reading Cookies 80 | Reading cookies requires the [cookie-parser](https://github.com/expressjs/cookie-parser#readme) 81 | package to be installed. [See here for details](#cookie-parser). 82 | Reading **signed cookies** requires that the `CookieParser` be configured with a 83 | [signing secret](https://github.com/expressjs/cookie-parser#cookieparsersecret-options). 84 | 85 | #### Regular (non-signed) Cookies 86 | Use the `@Cookies()` route parameter decorator to get "regular" cookies. 87 | ```typescript 88 | @Get('get') 89 | get(@Cookies() cookies): string { 90 | console.log('cookies: ', cookies); 91 | return this.appService.getHello(); 92 | } 93 | ``` 94 | 95 | This will bind an array of **all** (non-signed) cookies to the `cookies` parameter. 96 | See [below](#accessing-specific-named-cookies) to access a named cookie. 97 | 98 | #### Signed Cookies 99 | Use the `@SignedCookies()` route parameter decorator to get signed cookies. 100 | ```typescript 101 | @Get('getSigned') 102 | getSigned(@SignedCookies() cookies) { 103 | console.log('signed cookies: ', cookies); 104 | } 105 | ``` 106 | 107 | As with `@Cookies()`, this will bind an array of **all** signed cookies to the `cookies` 108 | parameter. Access individual signed cookies [as described below](#accessing-specific-named-cookies). 109 | 110 | #### Accessing Specific (Named) Cookies 111 | Pass the name of a specific cookie in the `@Cookies()` or `@SignedCookies()` decorator 112 | to access a specific cookie: 113 | 114 | ```typescript 115 | get(@SignedCookies('cookie1') cookie1) { ... } 116 | ``` 117 | 118 | ### Setting Cookies 119 | Use the `@SetCookies()` route handler *method decorator* to set cookies. 120 | 121 | Here's the API: 122 | ```typescript 123 | @SetCookies( 124 | options?: CookieOptions, 125 | cookies?: CookieSettings | CookieSettings[] 126 | ) 127 | ``` 128 | 129 | Here's how it works. You have two options, depending on whether the cookie settings 130 | are static or dynamic. 131 | 1. For *static* cookies, where the cookie name and/or value are known at compile time, 132 | you can set them in the `@SetCookies()` decorator by passing a [CookieSettings](#cookie-settings) 133 | object. 134 | 135 |
For example: 136 | ```typescript 137 | @SetCookies({name: 'cookie1', value: 'cookie 1 value'}) 138 | @Get('set') 139 | set() { 140 | ... 141 | } 142 | ``` 143 | 144 | 2. For *dynamic* cookies, where the cookie name and/or value are computed at run-time, 145 | you can provide the cookie name/value pairs to be set when the 146 | route handler method runs. Provide these values by passing them on the `req._cookies` 147 | array property. (The decorator creates the `_cookies` property automatically for you). 148 | **Note:** Of course if you are using this technique, you are de facto accessing 149 | the `request` object, so you must bind `@Request()` to a route parameter. 150 | 151 |
For example: 152 | 153 | ```typescript 154 | set(@Request() req) { 155 | const cookie1Value = 'chocoloate chip'; 156 | req._cookies = [ 157 | { 158 | name: 'cookie1', 159 | value: cookie1Value, 160 | options: { 161 | signed: true, 162 | sameSite: true, 163 | }, 164 | }, 165 | { name: 'cookie2', value: 'oatmeal raisin' }, 166 | ]; 167 | ... 168 | ``` 169 | 170 | #### Defaults and overriding 171 | You can mix and match `CookieOptions` and `CookieSettings` in the decorator and 172 | in the method body as needed. This example 173 | shows *dynamic* cookies with defaults inherited from the decorator, and 174 | overrides in the body: 175 | ```typescript 176 | @SetCookies({httpOnly: true}, 177 | [ 178 | {name: 'cookie1', value: 'cookie 1 value'}, 179 | {name: 'cookie2', value: 'cookie 2 value', {httpOnly: false}} 180 | ] 181 | ) 182 | ``` 183 | As a result of the above, `cookie1` will be set as `HttpOnly`, but `cookie2` will not. 184 | 185 | - Set default [cookie options](#cookieoptions) by passing a 186 | `CookieOptions` object in the decorator. Options set on individual cookies, 187 | if provided, override these defaults. 188 | 189 | #### Cookie Settings 190 | As shown above, each cookie you set has the shape: 191 | ```typescript 192 | interface CookieSettings { 193 | /** 194 | * name of the cookie. 195 | */ 196 | name: string; 197 | /** 198 | * value of the cookie. 199 | */ 200 | value?: string; 201 | /** 202 | * cookie options. 203 | */ 204 | options?: CookieOptions; 205 | } 206 | ``` 207 | If `options` are provided for a cookie, they completely replace any options 208 | specified in the `@SetCookies()` decorator. If omitted for a cookie, they default 209 | to options specified on the `@SetCookies()` decorator, or [Express's default cookie settings](https://expressjs.com/en/api.html#res.cookie) 210 | if none were set. 211 | 212 | #### CookieOptions 213 | Cookie options may be set at the method level (`@SetCookies()`), providing a set of 214 | defaults, or for individual cookies. In either case, they have the following shape: 215 | ```typescript 216 | interface CookieOptions { 217 | /** 218 | * Domain name for the cookie. 219 | */ 220 | domain?: string; 221 | /** 222 | * A synchronous function used for cookie value encoding. Defaults to encodeURIComponent. 223 | */ 224 | encode?: (val: string) => string; 225 | /** 226 | * Expiry date of the cookie in GMT. If not specified or set to 0, creates a session cookie. 227 | */ 228 | expires?: Date; 229 | /** 230 | * Flags the cookie to be accessible only by the web server. 231 | */ 232 | httpOnly?: boolean; 233 | /** 234 | * Convenient option for setting the expiry time relative to the current time in milliseconds. 235 | */ 236 | maxAge?: number; 237 | /** 238 | * Path for the cookie. Defaults to “/”. 239 | */ 240 | path?: string; 241 | /** 242 | * Marks the cookie to be used with HTTPS only. 243 | */ 244 | secure?: boolean; 245 | /** 246 | * Indicates if the cookie should be signed. 247 | */ 248 | signed?: boolean; 249 | /** 250 | * Value of the “SameSite” Set-Cookie attribute. More information at 251 | * https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1. 252 | */ 253 | sameSite?: boolean | string; 254 | } 255 | ``` 256 | #### Route Handler Results and Behavior 257 | The route handler otherwise proceeds as normal. It can return values, and it can 258 | use other route handler method decorators (such as `@Render()`) and other route 259 | parameter decorators (such as `@Headers()`, `@Query()`). 260 | 261 | #### Example 262 | Setting cookies isn't hard! See a [full example here in the test folder](https://github.com/nestjsplus/cookies/blob/master/test/src/app.controller.ts). 263 | 264 | ### Clearing (deleting) Cookies 265 | 266 | Delete cookies in one of two ways: 267 | 1. Use `@SetCookies()` and pass in **only** the cookie name (leave the value property 268 | off of the object). 269 | 2. Use `@ClearCookies()`, passing in a comma separated list of cookies to clear. 270 | ```typescript 271 | @ClearCookies('cookie1', 'cookie2') 272 | @Get('kill') 273 | @Render('clear') 274 | kill() { 275 | return { message: 'cookies killed!' }; 276 | } 277 | ``` 278 | 279 | ### Restrictions 280 | #### Express Only 281 | These decorators currently only work with Nest applications running on `@platform-express`. Fastify support is not 282 | currently available. 283 | 284 | #### Cookie Parser 285 | Note that reading cookies depends on the standard Express [cookie-parser]() package. Be sure to install it 286 | and configure it in your app. For example: 287 | 288 | ```bash 289 | npm install cookie-parser 290 | ``` 291 | and in your `main.ts` file: 292 | ```typescript 293 | import { NestFactory } from '@nestjs/core'; 294 | import { NestExpressApplication } from '@nestjs/platform-express'; 295 | import { AppModule } from './app.module'; 296 | 297 | import * as CookieParser from 'cookie-parser'; 298 | 299 | async function bootstrap() { 300 | const app = await NestFactory.create(AppModule); 301 | 302 | app.use(CookieParser('secret')); 303 | 304 | await app.listen(3000); 305 | } 306 | bootstrap(); 307 | ``` 308 | 309 | #### Decorators Can't Access `this` 310 | Note that decorators have access to the `class` (Controller), but not the instance. This means that, for example, 311 | if you want to pass a variable to a `SetCookies()` decorator, you should pass a variable set in the outer scope of 312 | the file (e.g., a `const` above the controller class definition), as opposed to a property on the controller class. 313 | 314 | See [the controller in the test folder](https://github.com/nestjsplus/cookies/blob/master/test/src/app.controller.ts) for an example. 315 | 316 | ## Change Log 317 | 318 | See [Changelog](CHANGELOG.md) for more information. 319 | 320 | ## Contributing 321 | 322 | Contributions welcome! See [Contributing](CONTRIBUTING.md). 323 | 324 | ## Author 325 | 326 | - **John Biundo (Y Prospect on [Discord](https://discord.gg/G7Qnnhy))** 327 | 328 | ## License 329 | 330 | Licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 331 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist'; 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function __export(m) { 3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; 4 | } 5 | exports.__esModule = true; 6 | __export(require("./dist")); 7 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './dist'; 2 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nestjsplus/cookies", 3 | "version": "1.0.3", 4 | "description": "Decorators for Managing Cookies with NestJS", 5 | "author": "John Biundo ", 6 | "license": "MIT", 7 | "readmeFilename": "README.md", 8 | "scripts": { 9 | "build": "rm -rf ./dist && tsc && npm run build:index", 10 | "build:index": "rm -rf ./index.js ./index.d.ts && tsc -d --skipLibCheck ./index.ts", 11 | "format": "prettier --write \"src/**/*.ts\"", 12 | "lint": "tslint -p tsconfig.json -c tslint.json", 13 | "test": "jest", 14 | "test:watch": "jest --watch", 15 | "test:cov": "jest --coverage", 16 | "test:e2e": "jest --config ./test/jest-e2e.json" 17 | }, 18 | "keywords": [ 19 | "nestjs", 20 | "decorators", 21 | "cookies" 22 | ], 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/nestjsplus/cookies" 29 | }, 30 | "bugs": "https://github.com/nestjsplus/cookies/issues", 31 | "dependencies": { 32 | "lodash": "^4.17.15", 33 | "reflect-metadata": "0.1.12", 34 | "rxjs": "^6.3.3" 35 | }, 36 | "peerDependencies": { 37 | "@nestjs/common": "^6.0.0", 38 | "@nestjs/core": "^6.0.0", 39 | "@nestjs/platform-express": "^6.0.0" 40 | }, 41 | "devDependencies": { 42 | "@nestjs/common": "^6.0.0", 43 | "@nestjs/core": "^6.0.0", 44 | "@nestjs/platform-express": "^6.0.0", 45 | "@nestjs/testing": "6.1.1", 46 | "@types/express": "4.16.1", 47 | "@types/jest": "24.0.11", 48 | "@types/lodash": "^4.14.136", 49 | "@types/node": "11.13.4", 50 | "@types/supertest": "2.0.7", 51 | "cookie-parser": "^1.4.4", 52 | "hbs": "^4.0.4", 53 | "jest": "24.7.1", 54 | "prettier": "1.17.0", 55 | "rimraf": "^2.6.2", 56 | "supertest": "4.0.2", 57 | "ts-jest": "24.0.2", 58 | "ts-node": "8.1.0", 59 | "tsc-watch": "2.2.1", 60 | "tsconfig-paths": "3.8.0", 61 | "tslint": "5.16.0", 62 | "typescript": "3.4.3" 63 | }, 64 | "jest": { 65 | "moduleFileExtensions": [ 66 | "js", 67 | "json", 68 | "ts" 69 | ], 70 | "rootDir": "src", 71 | "testRegex": ".spec.ts$", 72 | "transform": { 73 | "^.+\\.(t|j)s$": "ts-jest" 74 | }, 75 | "coverageDirectory": "../coverage", 76 | "testEnvironment": "node" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/decorators/clear-cookies.decorator.ts: -------------------------------------------------------------------------------- 1 | import { UseInterceptors, SetMetadata } from '@nestjs/common'; 2 | import { ClearCookiesInterceptor } from '../interceptors/clear-cookies.interceptor'; 3 | 4 | /** 5 | * Request method decorator. Causes decorated method to clear 6 | * all named cookies. 7 | * 8 | * @param cookies List of cookie names to be cleared. 9 | * 10 | */ 11 | export function ClearCookies(...cookies: string[]) { 12 | return (target, propertyKey, descriptor) => { 13 | SetMetadata('cookieNames', cookies)(target, propertyKey, descriptor); 14 | UseInterceptors(ClearCookiesInterceptor)(target, propertyKey, descriptor); 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/decorators/get-cookies.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from '@nestjs/common'; 2 | 3 | /** 4 | * Route handler parameter decorator. Extracts all cookies, or a 5 | * named cookie from the `cookies` object and populates the decorated 6 | * parameter with that value. 7 | * 8 | * @param data (optional) string containing name of cookie to extract 9 | * If omitted, all cookies will be extracted. 10 | */ 11 | export const Cookies = createParamDecorator((data, req) => { 12 | return data ? req.cookies && req.cookies[data] : req.cookies; 13 | }); 14 | 15 | /** 16 | * Route handler parameter decorator. Extracts all signed cookies, or a 17 | * named signed cookie from the `signedCookies` object and populates the decorated 18 | * parameter with that value. 19 | * 20 | * @param data (optional) string containing name of signed cookie to extract 21 | * If omitted, all signed cookies will be extracted. 22 | */ 23 | export const SignedCookies = createParamDecorator((data, req) => { 24 | return data 25 | ? req.signedCookies && req.signedCookies[data] 26 | : req.signedCookies; 27 | }); 28 | -------------------------------------------------------------------------------- /src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './clear-cookies.decorator'; 2 | export * from './get-cookies.decorator'; 3 | export * from './set-cookies.decorator'; 4 | -------------------------------------------------------------------------------- /src/decorators/set-cookies.decorator.ts: -------------------------------------------------------------------------------- 1 | import { UseInterceptors, SetMetadata } from '@nestjs/common'; 2 | import { SetCookiesInterceptor } from '../interceptors/set-cookies.interceptor'; 3 | import { CookieOptions, CookieSettings } from '../interfaces'; 4 | 5 | /** 6 | * Request method decorator. Causes decorated method to set cookies if specified 7 | * in method. 8 | * 9 | * @param options (Optional) Cookie Options. If set, supplies default 10 | * options for all cookies set within the decorated method. 11 | * 12 | * @param cookies (Optional) Cookie Settings. If set, supplies default 13 | * values for all cookies set within the decorated method. 14 | * 15 | * Decorated method may set `req._cookies` with array of `CookieSettings` to 16 | * be set upon completion of method execution. 17 | * 18 | * `CookieSettings` interface: 19 | * 20 | * ```typescript 21 | * interface CookieSettings { 22 | * // name of the cookie. 23 | * name: string; 24 | * // value of the cookie. 25 | * value ?: string; 26 | * //cookie options. 27 | * options ?: CookieOptions; 28 | * } 29 | * ``` 30 | * 31 | * Note: due to render issues, we substituted for the actual 32 | * @ character in the example below. 33 | * 34 | * @example 35 | * Get('set') 36 | * SetCookies() 37 | * set(Request() req, Query() query): any { 38 | * const value = query.test ? query.test : this.appService.getHello(); 39 | * req._cookies = [ 40 | * { name: 'cookie1', value }, 41 | * { name: 'cookie2', value: 'c2 value' }, 42 | * ]; 43 | * return `cookie1 set to ${value}, cookie2 set to 'c2 value'`; 44 | * } 45 | * 46 | */ 47 | export function SetCookies( 48 | options?: CookieOptions | CookieSettings | CookieSettings[], 49 | ); 50 | /** 51 | * Request method decorator. Causes decorated method to set cookies if specified 52 | * in method. 53 | * 54 | * @param options (Optional) Cookie Options. If set, supplies default 55 | * options for all cookies set within the decorated method. 56 | * 57 | * @param cookies (Optional) Cookie Settings. If set, supplies default 58 | * values for all cookies set within the decorated method. 59 | * 60 | * Decorated method may set `req._cookies` with array of `CookieSettings` to 61 | * be set upon completion of method execution. 62 | * 63 | * `CookieSettings` interface: 64 | * 65 | * ```typescript 66 | * interface CookieSettings { 67 | * // name of the cookie. 68 | * name: string; 69 | * // value of the cookie. 70 | * value ?: string; 71 | * //cookie options. 72 | * options ?: CookieOptions; 73 | * } 74 | * ``` 75 | * 76 | * Note: due to render issues, we substituted for the actual 77 | * @ character in the example below. 78 | * 79 | * @example 80 | * Get('set') 81 | * SetCookies() 82 | * set(Request() req, Query() query): any { 83 | * const value = query.test ? query.test : this.appService.getHello(); 84 | * req._cookies = [ 85 | * { name: 'cookie1', value }, 86 | * { name: 'cookie2', value: 'c2 value' }, 87 | * ]; 88 | * return `cookie1 set to ${value}, cookie2 set to 'c2 value'`; 89 | * } 90 | * 91 | */ 92 | export function SetCookies( 93 | options: CookieOptions, 94 | cookies: CookieSettings | CookieSettings[], 95 | ); 96 | /** 97 | * Request method decorator. Causes decorated method to set cookies if specified 98 | * in method. 99 | * 100 | * @param options (Optional) Cookie Options. If set, supplies default 101 | * options for all cookies set within the decorated method. 102 | * 103 | * @param cookies (Optional) Cookie Settings. If set, supplies default 104 | * values for all cookies set within the decorated method. 105 | * 106 | * Decorated method may set `req._cookies` with array of `CookieSettings` to 107 | * be set upon completion of method execution. 108 | * 109 | * `CookieSettings` interface: 110 | * 111 | * ```typescript 112 | * interface CookieSettings { 113 | * // name of the cookie. 114 | * name: string; 115 | * // value of the cookie. 116 | * value ?: string; 117 | * //cookie options. 118 | * options ?: CookieOptions; 119 | * } 120 | * ``` 121 | * 122 | * Note: due to render issues, we substituted for the actual 123 | * @ character in the example below. 124 | * 125 | * @example 126 | * Get('set') 127 | * SetCookies() 128 | * set(Request() req, Query() query): any { 129 | * const value = query.test ? query.test : this.appService.getHello(); 130 | * req._cookies = [ 131 | * { name: 'cookie1', value }, 132 | * { name: 'cookie2', value: 'c2 value' }, 133 | * ]; 134 | * return `cookie1 set to ${value}, cookie2 set to 'c2 value'`; 135 | * } 136 | * 137 | */ 138 | export function SetCookies( 139 | options?: CookieOptions | CookieSettings | CookieSettings[], 140 | cookies?: CookieSettings | CookieSettings[], 141 | ) { 142 | return (target, propertyKey, descriptor) => { 143 | if (options) { 144 | // since we're overloaded, and first param could be either options or 145 | // cookies, must check which it is 146 | if (!Array.isArray(options) && !options.hasOwnProperty('name')) { 147 | // first param must be an options object 148 | SetMetadata('cookieOptions', options)(target, propertyKey, descriptor); 149 | } else { 150 | // first param is a cookies object 151 | cookies = [].concat(options) as CookieSettings[]; 152 | } 153 | } 154 | 155 | if (cookies) { 156 | SetMetadata('cookieSettings', [].concat(cookies))( 157 | target, 158 | propertyKey, 159 | descriptor, 160 | ); 161 | } 162 | UseInterceptors(SetCookiesInterceptor)(target, propertyKey, descriptor); 163 | }; 164 | } 165 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces'; 2 | export * from './decorators'; 3 | -------------------------------------------------------------------------------- /src/interceptors/clear-cookies.interceptor.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { 3 | Injectable, 4 | NestInterceptor, 5 | ExecutionContext, 6 | CallHandler, 7 | } from '@nestjs/common'; 8 | import { Observable } from 'rxjs'; 9 | import { Response } from 'express'; 10 | 11 | @Injectable() 12 | export class ClearCookiesInterceptor implements NestInterceptor { 13 | intercept( 14 | context: ExecutionContext, 15 | next: CallHandler, 16 | ): Promise> { 17 | const ctx = context.switchToHttp(); 18 | const response = ctx.getResponse(); 19 | const handler = context.getHandler(); 20 | const cookieNames = [].concat(Reflect.getMetadata('cookieNames', handler)); 21 | const res$ = next.handle(); 22 | return res$.toPromise().then(res => { 23 | if (cookieNames) { 24 | for (const name of cookieNames) { 25 | response.clearCookie(name); 26 | } 27 | } 28 | return res || undefined; 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/interceptors/set-cookies.interceptor.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import unionBy = require('lodash/unionBy'); 3 | import { CookieSettings } from '../interfaces'; 4 | 5 | import { 6 | Injectable, 7 | NestInterceptor, 8 | ExecutionContext, 9 | CallHandler, 10 | } from '@nestjs/common'; 11 | import { Observable } from 'rxjs'; 12 | import { tap } from 'rxjs/operators'; 13 | import { Response } from 'express'; 14 | 15 | @Injectable() 16 | export class SetCookiesInterceptor implements NestInterceptor { 17 | intercept(context: ExecutionContext, next: CallHandler): Observable { 18 | const ctx = context.switchToHttp(); 19 | const response = ctx.getResponse(); 20 | const request = ctx.getRequest(); 21 | const handler = context.getHandler(); 22 | const options = Reflect.getMetadata('cookieOptions', handler); 23 | const cookies = Reflect.getMetadata('cookieSettings', handler); 24 | request._cookies = []; 25 | return next.handle().pipe( 26 | tap(() => { 27 | const allCookies = unionBy( 28 | request._cookies, 29 | cookies, 30 | item => item.name, 31 | ) as CookieSettings[]; 32 | for (const cookie of allCookies) { 33 | const cookieOptions = cookie.options 34 | ? cookie.options 35 | : options 36 | ? options 37 | : {}; 38 | if (cookie.value) { 39 | response.cookie(cookie.name, cookie.value, cookieOptions); 40 | } else { 41 | response.clearCookie(cookie.name); 42 | } 43 | } 44 | }), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/interfaces/cookies.interfaces.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface defining options to set for cookie. 3 | * 4 | * @see [Express cookies](https://expressjs.com/en/api.html#res.cookie) 5 | */ 6 | export interface CookieOptions { 7 | /** 8 | * Domain name for the cookie. 9 | */ 10 | domain?: string; 11 | /** 12 | * A synchronous function used for cookie value encoding. Defaults to encodeURIComponent. 13 | */ 14 | encode?: (val: string) => string; 15 | /** 16 | * Expiry date of the cookie in GMT. If not specified or set to 0, creates a session cookie. 17 | */ 18 | expires?: Date; 19 | /** 20 | * Flags the cookie to be accessible only by the web server. 21 | */ 22 | httpOnly?: boolean; 23 | /** 24 | * Convenient option for setting the expiry time relative to the current time in milliseconds. 25 | */ 26 | maxAge?: number; 27 | /** 28 | * Path for the cookie. Defaults to “/”. 29 | */ 30 | path?: string; 31 | /** 32 | * Marks the cookie to be used with HTTPS only. 33 | */ 34 | secure?: boolean; 35 | /** 36 | * Indicates if the cookie should be signed. 37 | */ 38 | signed?: boolean; 39 | /** 40 | * Value of the “SameSite” Set-Cookie attribute. More information at 41 | * https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1. 42 | */ 43 | sameSite?: boolean | string; 44 | } 45 | 46 | /** 47 | * Interface defining the cookie itself. 48 | */ 49 | export interface CookieSettings { 50 | /** 51 | * name of the cookie. 52 | */ 53 | name: string; 54 | /** 55 | * value of the cookie. 56 | */ 57 | value?: string; 58 | /** 59 | * cookie options. 60 | */ 61 | options?: CookieOptions; 62 | } 63 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cookies.interfaces'; 2 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { AppModule } from './src/app.module'; 4 | import { join } from 'path'; 5 | import * as CookieParser from 'cookie-parser'; 6 | 7 | let agent; 8 | 9 | let saveCookie1; 10 | 11 | describe('AppController (e2e)', () => { 12 | let app; 13 | 14 | beforeAll(async () => { 15 | const moduleFixture: TestingModule = await Test.createTestingModule({ 16 | imports: [AppModule], 17 | }).compile(); 18 | 19 | app = moduleFixture.createNestApplication(); 20 | 21 | app.use(CookieParser('secret')); 22 | app.setBaseViewsDir(join(__dirname, 'views')); 23 | app.setViewEngine('hbs'); 24 | 25 | await app.init(); 26 | 27 | agent = request(app.getHttpServer()); 28 | }); 29 | 30 | it('/set should set cookie1, cookie2', async () => { 31 | return await agent 32 | .get('/set') 33 | .expect(200) 34 | // tslint:disable-next-line: quotemark 35 | .expect("cookie1 set to 'Hello World!', cookie2 set to 'c2 value'") 36 | .expect(res => { 37 | expect(res.header['set-cookie']).toHaveLength(2); 38 | saveCookie1 = res.header['set-cookie'].find( 39 | (cookie: string) => cookie.includes('cookie1'), 40 | ); 41 | expect(saveCookie1).toMatch(/s%3AHello%20World!/); 42 | expect(saveCookie1).toMatch(/SameSite=Strict/); 43 | 44 | const cookie2 = res.header['set-cookie'].find( 45 | (cookie: string) => cookie.includes('cookie2'), 46 | ); 47 | expect(cookie2).toMatch(/c2%20value/); 48 | }); 49 | }); 50 | 51 | it('/cookieSet1 should set cookie3, cookie4', async () => { 52 | return await agent 53 | .get('/cookieSet1') 54 | .expect(200) 55 | .expect(res => { 56 | const cookie3 = res.header['set-cookie'].find( 57 | (cookie: string) => cookie.includes('cookie3'), 58 | ); 59 | const cookie4 = res.header['set-cookie'].find( 60 | (cookie: string) => cookie.includes('cookie4'), 61 | ); 62 | expect(cookie3).toMatch(/cookie3%20value/); 63 | expect(cookie4).toMatch(/cookie4%20value/); 64 | }); 65 | }); 66 | 67 | it('/cookieSet2 should set cookie1, cookie3, cookie4', async () => { 68 | return await agent 69 | .get('/cookieSet2') 70 | .expect(200) 71 | .expect(res => { 72 | const cookie1 = res.header['set-cookie'].find( 73 | (cookie: string) => cookie.includes('cookie1'), 74 | ); 75 | const cookie3 = res.header['set-cookie'].find( 76 | (cookie: string) => cookie.includes('cookie3'), 77 | ); 78 | const cookie4 = res.header['set-cookie'].find( 79 | (cookie: string) => cookie.includes('cookie4'), 80 | ); 81 | expect(cookie1).toMatch(/cookie1%20value/); 82 | expect(cookie3).toMatch(/cookie3%20value/); 83 | expect(cookie4).toMatch(/cookie4%20value/); 84 | }); 85 | }); 86 | 87 | it('/cookieSet3 should override cookie3, set cookie4', async () => { 88 | return await agent 89 | .get('/cookieSet3') 90 | .expect(200) 91 | .expect(res => { 92 | const cookie3 = res.header['set-cookie'].find( 93 | (cookie: string) => cookie.includes('cookie3'), 94 | ); 95 | const cookie4 = res.header['set-cookie'].find( 96 | (cookie: string) => cookie.includes('cookie4'), 97 | ); 98 | expect(cookie3).toMatch(/overridden/); 99 | expect(cookie4).toMatch(/cookie4%20value/); 100 | }); 101 | }); 102 | 103 | it('/cookieSet4 should set cookie3, cookie4 with httpOnly', async () => { 104 | return await agent 105 | .get('/cookieSet4') 106 | .expect(200) 107 | .expect(res => { 108 | const cookie3 = res.header['set-cookie'].find( 109 | (cookie: string) => cookie.includes('cookie3'), 110 | ); 111 | const cookie4 = res.header['set-cookie'].find( 112 | (cookie: string) => cookie.includes('cookie4'), 113 | ); 114 | expect(cookie3).toMatch(/cookie3%20value/); 115 | expect(cookie4).toMatch(/cookie4%20value/); 116 | expect(cookie3).toMatch(/HttpOnly/); 117 | expect(cookie4).toMatch(/HttpOnly/); 118 | }); 119 | }); 120 | 121 | it('/dget should get cookie sent', async () => { 122 | const testCookieName = 'cookie1'; 123 | const testCookieVal = 'mycookie'; 124 | const testCookieHeader = `${testCookieName}=${testCookieVal}`; 125 | const testCookieResponse = {}; 126 | testCookieResponse[testCookieName] = testCookieVal; 127 | return await agent 128 | .get('/dget') 129 | .set('cookie', testCookieHeader) 130 | .expect(200) 131 | .expect(testCookieResponse); 132 | }); 133 | 134 | it('/sget should get signed cookie sent', async () => { 135 | return await agent 136 | .get('/sget') 137 | .set('cookie', saveCookie1) 138 | .expect(200) 139 | .expect('Hello World!'); 140 | }); 141 | 142 | it('/defaults should set def1, def2 cookies', async () => { 143 | return await agent 144 | .get('/defaults') 145 | .expect(200) 146 | // tslint:disable-next-line: quotemark 147 | .expect("def1 set to 'Hello World!', def2 set to 'c2 value'") 148 | .expect(res => { 149 | const def1 = res.header['set-cookie'].find( 150 | cookie => cookie.substring(0, 4) === 'def1', 151 | ); 152 | expect(def1).toMatch(/Hello%20World!/); 153 | expect(def1).toMatch(/SameSite=Strict/); 154 | expect(def1).toMatch(/Expires=Fri, 01 Feb 2030 01:01:00 GMT;/); 155 | }); 156 | }); 157 | 158 | it('/clear should clear cookie1, render response', async () => { 159 | return await agent 160 | .get('/clear') 161 | .expect(200) 162 | .expect(res => { 163 | const cookie1 = res.header['set-cookie'].find( 164 | (cookie: string) => cookie.includes('cookie1'), 165 | ); 166 | expect(res.text).toMatch(/

cookies cleared!<\/h2>/); 167 | expect(cookie1).toMatch(/cookie1=;/); 168 | }); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Headers, 5 | Request, 6 | Query, 7 | Render, 8 | } from '@nestjs/common'; 9 | import { 10 | SetCookies, 11 | ClearCookies, 12 | Cookies, 13 | SignedCookies, 14 | } from '../../src/decorators'; 15 | import { CookieSettings } from '../../src/interfaces'; 16 | 17 | const tcookies: CookieSettings[] = [ 18 | { name: 'cookie3', value: 'cookie3 value' }, 19 | { name: 'cookie4', value: 'cookie4 value' }, 20 | ]; 21 | 22 | @Controller() 23 | export class AppController { 24 | @Get('set') 25 | @SetCookies() 26 | set(@Request() req, @Headers() headers, @Query() query): any { 27 | const value = query.test ? query.test : 'Hello World!'; 28 | req._cookies = [ 29 | { 30 | name: 'cookie1', 31 | value, 32 | options: { 33 | signed: true, 34 | sameSite: true, 35 | }, 36 | }, 37 | { name: 'cookie2', value: 'c2 value' }, 38 | ]; 39 | return `cookie1 set to '${value}', cookie2 set to 'c2 value'`; 40 | } 41 | 42 | @Get('defaults') 43 | @SetCookies({ httpOnly: true }) 44 | setdef(@Request() req, @Headers() headers, @Query() query): any { 45 | const value = query.test ? query.test : 'Hello World!'; 46 | req._cookies = [ 47 | { 48 | name: 'def1', 49 | value, 50 | options: { 51 | expires: new Date(Date.UTC(2030, 1, 1, 1, 1)), 52 | sameSite: true, 53 | }, 54 | }, 55 | { name: 'def2', value: 'c2 value' }, 56 | ]; 57 | return `def1 set to '${value}', def2 set to 'c2 value'`; 58 | } 59 | 60 | // Delete cookies by passing empty value 61 | // also ensures that @Render still works (ensuring that "default" (platform-abstracted)) 62 | // response functions work 63 | @SetCookies() 64 | @Get('clear') 65 | @Render('clear') 66 | clear(@Request() req) { 67 | req._cookies = [{ name: 'cookie1' }]; 68 | return { message: 'cookies cleared!' }; 69 | } 70 | 71 | @SetCookies(tcookies) 72 | @Get('cookieSet1') 73 | cookieSet1() { 74 | return; 75 | } 76 | 77 | @SetCookies(tcookies) 78 | @Get('cookieSet2') 79 | cookieSet2(@Request() req) { 80 | req._cookies = [{ name: 'cookie1', value: 'cookie1 value' }]; 81 | return; 82 | } 83 | 84 | @SetCookies(tcookies) 85 | @Get('cookieSet3') 86 | cookieSet3(@Request() req) { 87 | req._cookies = [{ name: 'cookie3', value: 'overridden' }]; 88 | return; 89 | } 90 | 91 | @SetCookies({ httpOnly: true }, tcookies) 92 | @Get('cookieSet4') 93 | cookieSet4() { 94 | return; 95 | } 96 | 97 | @ClearCookies('cookie1', 'cookie2') 98 | @Get('kill') 99 | @Render('clear') 100 | kill() { 101 | return { message: 'cookies killed!' }; 102 | } 103 | 104 | @Get('show') 105 | show(@Request() req) { 106 | return req.cookies; 107 | } 108 | 109 | @Get('dget') 110 | dget(@Cookies() cookies) { 111 | return cookies; 112 | } 113 | 114 | @Get('sget') 115 | sget(@SignedCookies('cookie1') cookie1: any) { 116 | return cookie1; 117 | } 118 | 119 | @Get('signed') 120 | signed(@Request() req) { 121 | return req.signedCookies; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /test/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | 4 | @Module({ 5 | imports: [], 6 | controllers: [AppController], 7 | }) 8 | export class AppModule {} 9 | -------------------------------------------------------------------------------- /test/views/clear.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | App 7 | 8 | 9 | 10 |

Result:

11 |

{{ message }}

12 | 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es6", 9 | "sourceMap": false, 10 | "outDir": "./dist", 11 | "rootDir": "./src", 12 | "baseUrl": "./", 13 | "noLib": false 14 | }, 15 | "include": ["*.ts", "**/*.ts"], 16 | "exclude": ["./index.ts", "./index.d.ts", "node_modules", "test"] 17 | } 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 150], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false 16 | }, 17 | "rulesDirectory": [] 18 | } 19 | --------------------------------------------------------------------------------