mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
feat(api): add plugin allowing server to update cookies (#55395)
This commit is contained in:
committed by
GitHub
parent
9e31e53ec7
commit
bb95e2ff54
113
api/src/plugins/cookie-update.test.ts
Normal file
113
api/src/plugins/cookie-update.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import Fastify, { FastifyInstance } from 'fastify';
|
||||
import cookies, { type CookieSerializeOptions, sign } from './cookies';
|
||||
import { cookieUpdate } from './cookie-update';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
jest.mock('../utils/env', () => ({
|
||||
...jest.requireActual('../utils/env'),
|
||||
COOKIE_DOMAIN: 'www.example.com',
|
||||
FREECODECAMP_NODE_ENV: 'not-development'
|
||||
}));
|
||||
|
||||
describe('Cookie updates', () => {
|
||||
let fastify: FastifyInstance;
|
||||
|
||||
const setup = async (attributes: CookieSerializeOptions) => {
|
||||
// Since register creates a new scope, we need to create a route inside the
|
||||
// scope for the plugin to be applied.
|
||||
await fastify.register(cookieUpdate, fastify => {
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
fastify.get('/', async () => {
|
||||
return { hello: 'world' };
|
||||
});
|
||||
return {
|
||||
cookies: ['cookie_name'],
|
||||
attributes
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
fastify = Fastify();
|
||||
await fastify.register(cookies);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fastify.close();
|
||||
});
|
||||
|
||||
it('should not set cookies that are not in the request', async () => {
|
||||
await setup({});
|
||||
|
||||
const res = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: '/',
|
||||
headers: {
|
||||
cookie: 'cookie_name_two=cookie_value'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.headers['set-cookie']).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should update the cookie's attributes without changing the value", async () => {
|
||||
await setup({ sameSite: 'strict' });
|
||||
const signedCookie = sign('cookie_value');
|
||||
const encodedCookie = encodeURIComponent(signedCookie);
|
||||
|
||||
const res = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: '/',
|
||||
headers: {
|
||||
cookie: `cookie_name=${signedCookie}`
|
||||
}
|
||||
});
|
||||
|
||||
const updatedCookie = res.headers['set-cookie'] as string;
|
||||
expect(updatedCookie).toEqual(
|
||||
expect.stringContaining(`cookie_name=${encodedCookie}`)
|
||||
);
|
||||
expect(updatedCookie).toEqual(expect.stringContaining('SameSite=Strict'));
|
||||
});
|
||||
|
||||
it('should unsign the cookie if required', async () => {
|
||||
await setup({ signed: false });
|
||||
const signedCookie = sign('cookie_value');
|
||||
|
||||
const res = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: '/',
|
||||
headers: {
|
||||
cookie: `cookie_name=${signedCookie}`
|
||||
}
|
||||
});
|
||||
|
||||
const updatedCookie = res.headers['set-cookie'] as string;
|
||||
expect(updatedCookie).toEqual(
|
||||
expect.stringContaining('cookie_name=cookie_value')
|
||||
);
|
||||
});
|
||||
|
||||
it('should respect the default cookie config if not overriden', async () => {
|
||||
await setup({});
|
||||
|
||||
const res = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: '/',
|
||||
headers: {
|
||||
cookie: 'cookie_name=anything'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.cookies[0]).toStrictEqual({
|
||||
domain: 'www.example.com',
|
||||
httpOnly: true,
|
||||
name: 'cookie_name',
|
||||
path: '/',
|
||||
sameSite: 'Lax',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
value: expect.any(String),
|
||||
secure: true
|
||||
});
|
||||
});
|
||||
});
|
||||
37
api/src/plugins/cookie-update.ts
Normal file
37
api/src/plugins/cookie-update.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { FastifyPluginCallback } from 'fastify';
|
||||
|
||||
import type { CookieSerializeOptions } from './cookies';
|
||||
|
||||
type Options = { cookies: string[]; attributes: CookieSerializeOptions };
|
||||
|
||||
/**
|
||||
* Plugin that updates the attributes of cookies in the response, without
|
||||
* changing the value.
|
||||
*
|
||||
* @param fastify The Fastify instance.
|
||||
* @param options Options passed to the plugin via `fastify.register(plugin,
|
||||
* options)`.
|
||||
* @param options.cookies The names of the cookies to update.
|
||||
* @param options.attributes The attributes to update the cookies with. NOTE:
|
||||
* The attributes are merged with the default values given to \@fastify/cookie.
|
||||
* @param done Callback to signal that the logic has completed.
|
||||
*/
|
||||
export const cookieUpdate: FastifyPluginCallback<Options> = (
|
||||
fastify,
|
||||
options,
|
||||
done
|
||||
) => {
|
||||
fastify.addHook('onSend', (request, reply, _payload, next) => {
|
||||
for (const cookie of options.cookies) {
|
||||
const oldCookie = request.cookies[cookie];
|
||||
if (!oldCookie) continue;
|
||||
|
||||
const unsigned = reply.unsignCookie(oldCookie);
|
||||
const raw = unsigned.valid ? unsigned.value : oldCookie;
|
||||
void reply.setCookie(cookie, raw, options.attributes);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
done();
|
||||
};
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
FREECODECAMP_NODE_ENV
|
||||
} from '../utils/env';
|
||||
|
||||
export { type CookieSerializeOptions } from '@fastify/cookie';
|
||||
|
||||
/**
|
||||
* Signs a cookie value by prefixing it with "s:" and using the COOKIE_SECRET.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user