From 49537e737e94858cde89f8c7364535a915cb28e4 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 22 Jan 2024 19:22:13 +0530 Subject: [PATCH] feat: add csrf error message using i18n --- package.json | 5 +++++ src/errors.ts | 18 ++++++++++++++-- tests/csrf.spec.ts | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 244662d..3f7efe3 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@adonisjs/assembler": "^7.1.0", "@adonisjs/core": "^6.2.1", "@adonisjs/eslint-config": "^1.2.1", + "@adonisjs/i18n": "^2.0.0", "@adonisjs/prettier-config": "^1.2.1", "@adonisjs/session": "^7.1.0", "@adonisjs/tsconfig": "^1.2.1", @@ -75,6 +76,7 @@ "peerDependencies": { "@adonisjs/core": "^6.2.0", "@adonisjs/session": "^7.0.0", + "@adonisjs/i18n": "^2.0.0", "@japa/api-client": "^2.0.2", "edge.js": "^6.0.1" }, @@ -84,6 +86,9 @@ }, "edge.js": { "optional": true + }, + "@adonisjs/i18n": { + "optional": true } }, "author": "virk", diff --git a/src/errors.ts b/src/errors.ts index 40b1349..95dab8d 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -9,6 +9,7 @@ /// +import type { I18n } from '@adonisjs/i18n' import { Exception } from '@poppinss/utils' import { HttpContext } from '@adonisjs/core/http' @@ -16,11 +17,24 @@ export const E_BAD_CSRF_TOKEN = class InvalidCSRFToken extends Exception { code = 'E_BAD_CSRF_TOKEN' status = 403 message = 'Invalid or expired CSRF token' + identifier = 'errors.E_BAD_CSRF_TOKEN' - async handle(error: InvalidCSRFToken, ctx: HttpContext) { + /** + * Returns the message to be sent in the HTTP response. + * Feel free to override this method and return a custom + * response. + */ + getResponseMessage(error: this, ctx: HttpContext) { + if ('i18n' in ctx) { + return (ctx.i18n as I18n).t(error.identifier, {}, error.message) + } + return error.message + } + + async handle(error: this, ctx: HttpContext) { ctx.session.flashExcept(['_csrf', '_method', 'password', 'password_confirmation']) ctx.session.flashErrors({ - [error.code]: error.message, + [error.code]: this.getResponseMessage(error, ctx), }) ctx.response.redirect().back() } diff --git a/tests/csrf.spec.ts b/tests/csrf.spec.ts index b904e27..bba5a1f 100644 --- a/tests/csrf.spec.ts +++ b/tests/csrf.spec.ts @@ -18,6 +18,7 @@ import { SessionMiddlewareFactory } from '@adonisjs/session/factories' import { setup } from './helpers.js' import { csrfFactory } from '../src/guards/csrf.js' import { E_BAD_CSRF_TOKEN } from '../src/errors.js' +import { I18nManagerFactory } from '@adonisjs/i18n/factories' const tokens = new Tokens() @@ -391,4 +392,57 @@ test.group('Csrf', () => { }) } }) + + test('get error message from i18n', async ({ assert }) => { + assert.plan(1) + + const app = await setup() + const ctx = new HttpContextFactory().create() + const encrpytion = await app.container.make('encryption') + const middleware = await new SessionMiddlewareFactory().create() + + const i18nManager = new I18nManagerFactory() + .merge({ + config: { + loaders: [ + () => { + return { + async load() { + return { + en: { + 'errors.E_BAD_CSRF_TOKEN': 'Session expired', + }, + } + }, + } + }, + ], + }, + }) + .create() + + await middleware.handle(ctx, async () => { + ctx.route = { pattern: '/' } as any + ctx.request.request.method = 'PATCH' + await i18nManager.loadTranslations() + ctx.i18n = i18nManager.locale('en') + + const secret = await tokens.secret() + const csrfToken = tokens.create(secret) + ctx.request.updateBody({ _csrf: csrfToken }) + }) + + const csrf = csrfFactory({ enabled: true, enableXsrfCookie: false }, encrpytion) + try { + await csrf(ctx) + } catch (error) { + await error.handle(error, ctx) + assert.deepEqual(ctx.session.responseFlashMessages.all(), { + errorsBag: { + E_BAD_CSRF_TOKEN: 'Session expired', + }, + input: {}, + }) + } + }) })