├── .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 |
8 |
9 | Decorators for Managing Cookies with NestJS
10 |
11 |
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 |
--------------------------------------------------------------------------------