From e515944fb9137cb485fd6cc35c3991db8af1e9b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 10:04:00 +0400 Subject: [PATCH] =?UTF-8?q?refactor:=20extraire=20la=20v=C3=A9rification?= =?UTF-8?q?=20JWT/payload=20en=20policy=20partag=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/__tests__/artiste.test.js | 28 +------- src/api/artiste/controllers/artiste.js | 14 ---- src/api/artiste/routes/artiste.js | 8 ++- .../controllers/__tests__/commentaire.test.js | 15 +--- .../commentaire/controllers/commentaire.js | 15 +--- src/api/commentaire/routes/commentaire.js | 8 ++- .../controllers/__tests__/parole.test.js | 28 +------- src/api/parole/controllers/parole.js | 14 ---- src/api/parole/routes/parole.js | 8 ++- .../__tests__/is-payload-owner.test.js | 68 +++++++++++++++++++ src/policies/is-payload-owner.js | 23 +++++++ 11 files changed, 122 insertions(+), 107 deletions(-) create mode 100644 src/policies/__tests__/is-payload-owner.test.js create mode 100644 src/policies/is-payload-owner.js diff --git a/src/api/artiste/controllers/__tests__/artiste.test.js b/src/api/artiste/controllers/__tests__/artiste.test.js index c053605..7d4ab90 100644 --- a/src/api/artiste/controllers/__tests__/artiste.test.js +++ b/src/api/artiste/controllers/__tests__/artiste.test.js @@ -2,7 +2,7 @@ import {describe, it, expect, vi} from 'vitest' const {default: createController} = await import('../artiste.js') -function buildStrapi({jwtUserId, dbUser, existingArtiste = null}) { +function buildStrapi({dbUser, existingArtiste = null}) { const dbQuery = { findOne: vi.fn(async () => existingArtiste) } @@ -15,15 +15,6 @@ function buildStrapi({jwtUserId, dbUser, existingArtiste = null}) { const strapi = { contentType: vi.fn(() => ({uid: 'api::artiste.artiste', kind: 'collectionType'})), - plugins: { - 'users-permissions': { - services: { - jwt: { - getToken: vi.fn(async () => ({id: jwtUserId})) - } - } - } - }, db: { query: vi.fn(() => dbQuery) }, @@ -43,7 +34,6 @@ function buildCtx(data) { body: {data}, header: {authorization: 'Bearer faketoken'} }, - unauthorized: vi.fn(), badRequest: vi.fn(), notFound: vi.fn() } @@ -60,25 +50,13 @@ function buildData(overrides = {}) { } describe('artiste.create', () => { - it('refuse et ne crée rien quand le user du JWT ne correspond pas au user du payload, sans planter', async () => { - const {strapi, artisteDocuments} = buildStrapi({jwtUserId: 999, dbUser}) + it('crée l\'artiste quand le user existe et que l\'alias est nouveau', async () => { + const {strapi, artisteDocuments} = buildStrapi({dbUser}) const controller = createController({strapi}) const ctx = buildCtx(buildData()) await controller.create(ctx) - expect(ctx.unauthorized).toHaveBeenCalled() - expect(artisteDocuments.create).not.toHaveBeenCalled() - }) - - it('crée l\'artiste quand le user du JWT correspond au user du payload', async () => { - const {strapi, artisteDocuments} = buildStrapi({jwtUserId: 1, dbUser}) - const controller = createController({strapi}) - const ctx = buildCtx(buildData()) - - await controller.create(ctx) - - expect(ctx.unauthorized).not.toHaveBeenCalled() expect(artisteDocuments.create).toHaveBeenCalled() }) }) diff --git a/src/api/artiste/controllers/artiste.js b/src/api/artiste/controllers/artiste.js index a5e053b..90c0ba7 100644 --- a/src/api/artiste/controllers/artiste.js +++ b/src/api/artiste/controllers/artiste.js @@ -12,20 +12,6 @@ module.exports = createCoreController('api::artiste.artiste', ({strapi}) => ({ const {body} = ctx.request let {data} = body - if (ctx.request && ctx.request.header && ctx.request.header.authorization) { - try { - const {id} = await strapi.plugins[ - 'users-permissions' - ].services.jwt.getToken(ctx) - - if (id !== data.user.id) { - return ctx.unauthorized('Opération non autorisée') - } - } catch (err) { - return ctx.unauthorized('Opération non autorisée') - } - } - const user = await strapi.documents('plugin::users-permissions.user').findOne({ documentId: body.data.user.documentId }) diff --git a/src/api/artiste/routes/artiste.js b/src/api/artiste/routes/artiste.js index 27ff441..7e331e3 100644 --- a/src/api/artiste/routes/artiste.js +++ b/src/api/artiste/routes/artiste.js @@ -2,4 +2,10 @@ const { createCoreRouter } = require('@strapi/strapi').factories; -module.exports = createCoreRouter('api::artiste.artiste') +module.exports = createCoreRouter('api::artiste.artiste', { + config: { + create: { + policies: ['global::is-payload-owner'] + } + } +}) diff --git a/src/api/commentaire/controllers/__tests__/commentaire.test.js b/src/api/commentaire/controllers/__tests__/commentaire.test.js index 53f7b65..960ca94 100644 --- a/src/api/commentaire/controllers/__tests__/commentaire.test.js +++ b/src/api/commentaire/controllers/__tests__/commentaire.test.js @@ -5,7 +5,7 @@ const {default: createController} = await import('../commentaire.js') const dbUser = {id: 1, username: 'foo', email: 'foo@bar.com'} const dbParole = {id: 7, documentId: 'parole-doc-7'} -function buildStrapi({jwtUserId, existingParole = dbParole}) { +function buildStrapi({existingParole = dbParole} = {}) { const commentaireDocuments = { create: vi.fn(async ({data}) => ({id: 99, ...data})) } @@ -21,15 +21,6 @@ function buildStrapi({jwtUserId, existingParole = dbParole}) { const strapi = { contentType: vi.fn(() => ({uid: 'api::commentaire.commentaire', kind: 'collectionType'})), - plugins: { - 'users-permissions': { - services: { - jwt: { - getToken: vi.fn(async () => ({id: jwtUserId})) - } - } - } - }, db: { query: vi.fn(uid => { if (uid === 'plugin::users-permissions.user') return userDbQuery @@ -68,7 +59,7 @@ function buildData(overrides = {}) { describe('commentaire.create', () => { it('retrouve la parole par son id (pas par le documentId du user) et l\'associe correctement', async () => { - const {strapi, commentaireDocuments, paroleDocuments, paroleDbQuery} = buildStrapi({jwtUserId: dbUser.id}) + const {strapi, commentaireDocuments, paroleDocuments, paroleDbQuery} = buildStrapi() const controller = createController({strapi}) const ctx = buildCtx(buildData()) @@ -83,7 +74,7 @@ describe('commentaire.create', () => { }) it('rejette quand la parole ciblée n\'existe pas', async () => { - const {strapi, commentaireDocuments} = buildStrapi({jwtUserId: dbUser.id, existingParole: null}) + const {strapi, commentaireDocuments} = buildStrapi({existingParole: null}) const controller = createController({strapi}) const ctx = buildCtx(buildData()) diff --git a/src/api/commentaire/controllers/commentaire.js b/src/api/commentaire/controllers/commentaire.js index 76d75fa..9e33bd8 100644 --- a/src/api/commentaire/controllers/commentaire.js +++ b/src/api/commentaire/controllers/commentaire.js @@ -1,26 +1,13 @@ 'use strict'; const { createCoreController } = require('@strapi/strapi').factories; -const { ApplicationError, NotFoundError, UnauthorizedError } = require("@strapi/utils").errors +const { ApplicationError, NotFoundError } = require("@strapi/utils").errors module.exports = createCoreController('api::commentaire.commentaire', ({strapi}) => ({ async create(ctx) { const {body} = ctx.request let {data} = body - if (ctx.request && ctx.request.header && ctx.request.header.authorization) { - try { - const {id} = await strapi.plugins[ - 'users-permissions' - ].services.jwt.getToken(ctx) - - if (id !== data.user.id) { - throw new UnauthorizedError('Opération non autorisée') - } - } catch (err) { - throw new UnauthorizedError(ctx, err, 'Opération non autorisée') - } - } const user = await strapi.db.query('plugin::users-permissions.user').findOne({ where: {id: data.user.id} }) diff --git a/src/api/commentaire/routes/commentaire.js b/src/api/commentaire/routes/commentaire.js index 1d1afba..2a5dc0e 100644 --- a/src/api/commentaire/routes/commentaire.js +++ b/src/api/commentaire/routes/commentaire.js @@ -2,4 +2,10 @@ const { createCoreRouter } = require('@strapi/strapi').factories; -module.exports = createCoreRouter('api::commentaire.commentaire') +module.exports = createCoreRouter('api::commentaire.commentaire', { + config: { + create: { + policies: ['global::is-payload-owner'] + } + } +}) diff --git a/src/api/parole/controllers/__tests__/parole.test.js b/src/api/parole/controllers/__tests__/parole.test.js index 0820cf4..0127440 100644 --- a/src/api/parole/controllers/__tests__/parole.test.js +++ b/src/api/parole/controllers/__tests__/parole.test.js @@ -2,7 +2,7 @@ import {describe, it, expect, vi} from 'vitest' const {default: createController} = await import('../parole.js') -function buildStrapi({jwtUserId, dbUser, artiste}) { +function buildStrapi({dbUser, artiste}) { const paroleDocuments = { findMany: vi.fn(async () => []), create: vi.fn(async ({data}) => ({id: 42, ...data})) @@ -21,15 +21,6 @@ function buildStrapi({jwtUserId, dbUser, artiste}) { validateParoles: vi.fn(), translateLyrics: vi.fn() })), - plugins: { - 'users-permissions': { - services: { - jwt: { - getToken: vi.fn(async () => ({id: jwtUserId})) - } - } - } - }, documents: vi.fn(uid => { if (uid === 'plugin::users-permissions.user') return userDocuments if (uid === 'api::artiste.artiste') return artisteDocuments @@ -47,7 +38,6 @@ function buildCtx(data) { body: {data}, header: {authorization: 'Bearer faketoken'} }, - unauthorized: vi.fn(), badRequest: vi.fn(), notFound: vi.fn() } @@ -67,25 +57,13 @@ function buildData(overrides = {}) { } describe('parole.create', () => { - it('refuse et ne crée rien quand le user du JWT ne correspond pas au user du payload', async () => { - const {strapi, paroleDocuments} = buildStrapi({jwtUserId: 999, dbUser, artiste}) + it('crée la parole quand le user et l\'artiste existent', async () => { + const {strapi, paroleDocuments} = buildStrapi({dbUser, artiste}) const controller = createController({strapi}) const ctx = buildCtx(buildData()) await controller.create(ctx) - expect(ctx.unauthorized).toHaveBeenCalled() - expect(paroleDocuments.create).not.toHaveBeenCalled() - }) - - it('crée la parole quand le user du JWT correspond au user du payload', async () => { - const {strapi, paroleDocuments} = buildStrapi({jwtUserId: 1, dbUser, artiste}) - const controller = createController({strapi}) - const ctx = buildCtx(buildData()) - - await controller.create(ctx) - - expect(ctx.unauthorized).not.toHaveBeenCalled() expect(paroleDocuments.create).toHaveBeenCalled() }) }) diff --git a/src/api/parole/controllers/parole.js b/src/api/parole/controllers/parole.js index b275864..37a0ccf 100644 --- a/src/api/parole/controllers/parole.js +++ b/src/api/parole/controllers/parole.js @@ -71,20 +71,6 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({ const {data} = body strapi.service('api::parole.parole').validateParoles(data.titre, data.transcription) - if (ctx.request && ctx.request.header && ctx.request.header.authorization) { - try { - const {id} = await strapi.plugins[ - 'users-permissions' - ].services.jwt.getToken(ctx) - - if (id !== data.user.id) { - return ctx.unauthorized('Opération non autorisée') - } - } catch (err) { - return ctx.unauthorized('Opération non autorisée') - } - } - const user = await strapi.documents('plugin::users-permissions.user').findOne({ documentId: body.data.user.documentId }) diff --git a/src/api/parole/routes/parole.js b/src/api/parole/routes/parole.js index 3e80996..58106f4 100644 --- a/src/api/parole/routes/parole.js +++ b/src/api/parole/routes/parole.js @@ -2,4 +2,10 @@ const { createCoreRouter } = require('@strapi/strapi').factories; -module.exports = createCoreRouter('api::parole.parole') +module.exports = createCoreRouter('api::parole.parole', { + config: { + create: { + policies: ['global::is-payload-owner'] + } + } +}) diff --git a/src/policies/__tests__/is-payload-owner.test.js b/src/policies/__tests__/is-payload-owner.test.js new file mode 100644 index 0000000..a13cd84 --- /dev/null +++ b/src/policies/__tests__/is-payload-owner.test.js @@ -0,0 +1,68 @@ +import {describe, it, expect, vi} from 'vitest' + +const {default: isPayloadOwner} = await import('../is-payload-owner.js') + +function buildStrapi(jwtUserId) { + return { + plugins: { + 'users-permissions': { + services: { + jwt: { + getToken: vi.fn(async () => ({id: jwtUserId})) + } + } + } + } + } +} + +function buildPolicyContext({authorization, payloadUserId}) { + return { + request: { + header: authorization ? {authorization} : {}, + body: {data: {user: {id: payloadUserId}}} + } + } +} + +describe('is-payload-owner policy', () => { + it("autorise quand aucun en-tête d'autorisation n'est présent", async () => { + const strapi = buildStrapi(999) + const policyContext = buildPolicyContext({authorization: undefined, payloadUserId: 1}) + + await expect(isPayloadOwner(policyContext, {}, {strapi})).resolves.toBe(true) + }) + + it('autorise quand le user du JWT correspond au user du payload', async () => { + const strapi = buildStrapi(1) + const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1}) + + await expect(isPayloadOwner(policyContext, {}, {strapi})).resolves.toBe(true) + }) + + it('refuse quand le user du JWT ne correspond pas au user du payload', async () => { + const strapi = buildStrapi(999) + const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1}) + + await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée') + }) + + it('refuse quand le token est invalide', async () => { + const strapi = { + plugins: { + 'users-permissions': { + services: { + jwt: { + getToken: vi.fn(async () => { + throw new Error('Invalid token.') + }) + } + } + } + } + } + const policyContext = buildPolicyContext({authorization: 'Bearer faketoken', payloadUserId: 1}) + + await expect(isPayloadOwner(policyContext, {}, {strapi})).rejects.toThrow('Opération non autorisée') + }) +}) diff --git a/src/policies/is-payload-owner.js b/src/policies/is-payload-owner.js new file mode 100644 index 0000000..76db9e4 --- /dev/null +++ b/src/policies/is-payload-owner.js @@ -0,0 +1,23 @@ +'use strict'; + +const { UnauthorizedError } = require('@strapi/utils').errors + +module.exports = async (policyContext, config, {strapi}) => { + const {request} = policyContext + + if (!request?.header?.authorization) { + return true + } + + try { + const {id} = await strapi.plugins['users-permissions'].services.jwt.getToken(policyContext) + + if (id !== request.body?.data?.user?.id) { + throw new UnauthorizedError('Opération non autorisée') + } + } catch (err) { + throw new UnauthorizedError('Opération non autorisée') + } + + return true +}