From 402004496355d06ba5a46121128ad030f924ea6b Mon Sep 17 00:00:00 2001 From: Douglas DUTEIL Date: Mon, 16 Dec 2024 20:10:51 +0100 Subject: [PATCH] refactor(insee): extract insee connector to individual pkg --- .../1545154190802_create-users-table.cjs | 8 +- .../1623752868625_add-moderation-table.cjs | 2 + .../1633705667117_add-new-moderation-type.cjs | 2 + migrations/1702600151114_add-webauthn.cjs | 3 + package-lock.json | 74 +++++++++++++++++-- package.json | 6 +- .../src/connectors/get-insee-access-token.ts | 0 .../connectors/insee/get-access-token.test.ts | 63 ---------------- .../src/connectors/insee/get-access-token.ts | 43 ----------- packages/core/src/connectors/insee/index.ts | 3 - packages/identite/package.json | 64 ++++++++++++++++ packages/identite/src/types/index.ts | 3 + .../identite/src/types/user.ts | 40 +++++----- .../identite/src/user/find-by-email.test.ts | 69 +++++++++++++++++ packages/identite/src/user/find-by-email.ts | 24 ++++++ packages/identite/src/user/index.ts | 3 + packages/identite/tsconfig.json | 18 +++++ packages/identite/tsconfig.lib.json | 9 +++ packages/insee/package.json | 63 ++++++++++++++++ .../src/token/get-insee-access-token.test.ts | 26 +++++++ .../src/token/get-insee-access-token.ts} | 25 +++---- .../connectors => insee/src/token}/index.ts | 0 packages/insee/tsconfig.json | 18 +++++ packages/insee/tsconfig.lib.json | 9 +++ scripts/import-accounts-coop.ts | 19 ++++- src/connectors/api-sirene/index.ts | 35 +++------ src/managers/moderation.ts | 1 + src/managers/session/authenticated.ts | 2 +- src/managers/user.ts | 1 + src/repositories/organization/getters.ts | 1 + src/repositories/organization/setters.ts | 1 + src/repositories/user.ts | 2 +- 32 files changed, 453 insertions(+), 184 deletions(-) delete mode 100644 packages/core/src/connectors/get-insee-access-token.ts delete mode 100644 packages/core/src/connectors/insee/get-access-token.test.ts delete mode 100644 packages/core/src/connectors/insee/get-access-token.ts delete mode 100644 packages/core/src/connectors/insee/index.ts create mode 100644 packages/identite/package.json create mode 100644 packages/identite/src/types/index.ts rename src/types/user.d.ts => packages/identite/src/types/user.ts (96%) create mode 100644 packages/identite/src/user/find-by-email.test.ts create mode 100644 packages/identite/src/user/find-by-email.ts create mode 100644 packages/identite/src/user/index.ts create mode 100644 packages/identite/tsconfig.json create mode 100644 packages/identite/tsconfig.lib.json create mode 100644 packages/insee/package.json create mode 100644 packages/insee/src/token/get-insee-access-token.test.ts rename packages/{core/src/connectors/get-insee-access-token.test.ts => insee/src/token/get-insee-access-token.ts} (55%) rename packages/{core/src/connectors => insee/src/token}/index.ts (100%) create mode 100644 packages/insee/tsconfig.json create mode 100644 packages/insee/tsconfig.lib.json diff --git a/migrations/1545154190802_create-users-table.cjs b/migrations/1545154190802_create-users-table.cjs index 4c7f756ce..fcdf84100 100644 --- a/migrations/1545154190802_create-users-table.cjs +++ b/migrations/1545154190802_create-users-table.cjs @@ -1,7 +1,7 @@ exports.shorthands = undefined; exports.up = async (pgm) => { - await pgm.db.query(` + await pgm.db.query(` CREATE TABLE users ( id serial NOT NULL, email character varying DEFAULT ''::character varying NOT NULL, @@ -21,16 +21,16 @@ CREATE TABLE users ( type character varying );`); - await pgm.db.query(` + await pgm.db.query(` ALTER TABLE ONLY users ADD CONSTRAINT users_pkey PRIMARY KEY (id); `); await pgm.db.query(` -CREATE UNIQUE INDEX index_users_on_email ON users USING btree (email); +CREATE UNIQUE INDEX index_users_on_email ON users USING btree (email); `); - await pgm.db.query(` + await pgm.db.query(` CREATE UNIQUE INDEX index_users_on_reset_password_token ON users USING btree (reset_password_token); `); diff --git a/migrations/1623752868625_add-moderation-table.cjs b/migrations/1623752868625_add-moderation-table.cjs index 2b84c5dec..18e3b1e1c 100644 --- a/migrations/1623752868625_add-moderation-table.cjs +++ b/migrations/1623752868625_add-moderation-table.cjs @@ -3,6 +3,8 @@ exports.shorthands = undefined; exports.up = async (pgm) => { await pgm.db.query(` CREATE TYPE moderation_type AS ENUM('organization_join_block'); + `); + await pgm.db.query(` CREATE TABLE moderations ( id serial, user_id int NOT NULL, diff --git a/migrations/1633705667117_add-new-moderation-type.cjs b/migrations/1633705667117_add-new-moderation-type.cjs index a73141af2..ed060d6c1 100644 --- a/migrations/1633705667117_add-new-moderation-type.cjs +++ b/migrations/1633705667117_add-new-moderation-type.cjs @@ -4,6 +4,8 @@ exports.up = async (pgm) => { await pgm.db.query(` ALTER TABLE moderations ALTER COLUMN type TYPE character varying; + `); + await pgm.db.query(` DROP TYPE moderation_type; `); }; diff --git a/migrations/1702600151114_add-webauthn.cjs b/migrations/1702600151114_add-webauthn.cjs index ee963c714..52881469a 100644 --- a/migrations/1702600151114_add-webauthn.cjs +++ b/migrations/1702600151114_add-webauthn.cjs @@ -16,6 +16,9 @@ exports.up = async (pgm) => { REFERENCES users (id) ON DELETE CASCADE ); + `); + + await pgm.db.query(` CREATE UNIQUE INDEX index_authenticators_on_credential_id ON authenticators USING btree (credential_id); `); diff --git a/package-lock.json b/package-lock.json index 192b71f4e..cf56795bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,8 @@ "@gouvfr-lasuite/crisp": "https://github.com/douglasduteil/crisp/releases/download/v1.6.1/douglasduteil-crisp-1.6.1.tgz", "@gouvfr-lasuite/proconnect.core": "workspace:*", "@gouvfr-lasuite/proconnect.email": "workspace:*", + "@gouvfr-lasuite/proconnect.identite": "workspace:*", + "@gouvfr-lasuite/proconnect.insee": "workspace:*", "@gouvfr/dsfr": "^1.12.1", "@kitajs/html": "^4.2.4", "@kitajs/ts-html-plugin": "^4.1.0", @@ -877,6 +879,13 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/@electric-sql/pglite": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.2.15.tgz", + "integrity": "sha512-Jiq31Dnk+rg8rMhcSxs4lQvHTyizNo5b269c1gCC3ldQ0sCLrNVPGzy+KnmonKy1ZArTUuXZf23/UamzFMKVaA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", @@ -1288,6 +1297,14 @@ "resolved": "packages/email", "link": true }, + "node_modules/@gouvfr-lasuite/proconnect.identite": { + "resolved": "packages/identite", + "link": true + }, + "node_modules/@gouvfr-lasuite/proconnect.insee": { + "resolved": "packages/insee", + "link": true + }, "node_modules/@gouvfr/dsfr": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@gouvfr/dsfr/-/dsfr-1.12.1.tgz", @@ -7138,10 +7155,11 @@ } }, "node_modules/nock": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz", - "integrity": "sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.6.tgz", + "integrity": "sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", @@ -10441,15 +10459,55 @@ "vite": "^5.4.8" } }, - "packages/free-email": { - "name": "@gouvfr-lasuite/moncomptepro.free-email", - "version": "0.0.0", + "packages/identite": { + "name": "@gouvfr-lasuite/proconnect.identite", + "version": "0.2.0", + "license": "MIT", + "devDependencies": { + "@electric-sql/pglite": "^0.2.15", + "@gouvfr-lasuite/proconnect.core": "^0.2.0", + "@tsconfig/node22": "^22.0.0", + "@types/mocha": "^10.0.10", + "@types/node": "^22.10.2", + "chai": "^5.1.2", + "mocha": "^11.0.1", + "node-pg-migrate": "^7.6.1", + "pg": "^8.13.0", + "tsx": "^4.19.2" + } + }, + "packages/identity-repository": { + "name": "@gouvfr-lasuite/proconnect.identity-repository", + "version": "0.2.0", "extraneous": true, + "license": "MIT", + "devDependencies": { + "@electric-sql/pglite": "^0.2.15", + "@gouvfr-lasuite/proconnect.core": "^0.2.0", + "@tsconfig/node22": "^22.0.0", + "@types/mocha": "^10.0.10", + "@types/node": "^22.10.2", + "chai": "^5.1.2", + "mocha": "^11.0.1", + "pg": "^8.13.0", + "tsx": "^4.19.2" + } + }, + "packages/insee": { + "name": "@gouvfr-lasuite/proconnect.insee", + "version": "0.2.0", + "license": "MIT", "dependencies": { - "is-disposable-email-domain": "^1.0.7" + "axios": "^1.7.7" }, "devDependencies": { - "@tsconfig/node22": "^22.0.0" + "@tsconfig/node22": "^22.0.0", + "@types/mocha": "^10.0.10", + "@types/node": "^22.10.2", + "chai": "^5.1.2", + "mocha": "^11.0.1", + "nock": "^13.5.6", + "tsx": "^4.19.2" } } } diff --git a/package.json b/package.json index a342c3cba..b0861d35a 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,9 @@ "watch:js": "tsc --watch --preserveWatchOutput", "watch:node": "tsx --watch src/index.ts", "watch:workspaces:core": "npm run dev --if-present --workspace=@gouvfr-lasuite/proconnect.core", - "watch:workspaces:email": "npm run dev --if-present --workspace=@gouvfr-lasuite/proconnect.email" + "watch:workspaces:email": "npm run dev --if-present --workspace=@gouvfr-lasuite/proconnect.email", + "watch:workspaces:insee": "npm run dev --if-present --workspace=@gouvfr-lasuite/proconnect.insee", + "watch:workspaces:identite": "npm run dev --if-present --workspace=@gouvfr-lasuite/proconnect.identite" }, "prettier": { "plugins": [ @@ -51,6 +53,8 @@ "@gouvfr-lasuite/crisp": "https://github.com/douglasduteil/crisp/releases/download/v1.6.1/douglasduteil-crisp-1.6.1.tgz", "@gouvfr-lasuite/proconnect.core": "workspace:*", "@gouvfr-lasuite/proconnect.email": "workspace:*", + "@gouvfr-lasuite/proconnect.identite": "workspace:*", + "@gouvfr-lasuite/proconnect.insee": "workspace:*", "@gouvfr/dsfr": "^1.12.1", "@kitajs/html": "^4.2.4", "@kitajs/ts-html-plugin": "^4.1.0", diff --git a/packages/core/src/connectors/get-insee-access-token.ts b/packages/core/src/connectors/get-insee-access-token.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/core/src/connectors/insee/get-access-token.test.ts b/packages/core/src/connectors/insee/get-access-token.test.ts deleted file mode 100644 index bab117289..000000000 --- a/packages/core/src/connectors/insee/get-access-token.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -// - -import axios, { type AxiosResponse } from "axios"; - -// - -type GetInseeAccessTokenCradle = { - fetch: typeof globalThis.fetch; - username: string; - password: string; - timeout: number; -}; - -type GetTokenReponse = { - access_token: string; - scope: "am_application_scope default"; - token_type: "Bearer"; - expires_in: number; -}; - -// - -type Credentials = { - username: string; - password: string; - timeout: number; -}; - -function createInseeTokenRequest(credentials: Credentials) { - const { username, password, timeout } = credentials; - - return { - url: "https://api.insee.fr/token", - body: "grant_type=client_credentials", - config: { - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - auth: { username, password }, - timeout, - }, - }; -} - -export function GetInseeAccessToken(cradle: GetInseeAccessTokenCradle) { - const { username, password, timeout } = cradle; - return async function getInseeAccessToken() { - const { - data: { access_token }, - }: AxiosResponse = await axios.post( - "https://api.insee.fr/token", - "grant_type=client_credentials", - { - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - auth: { - username, - password, - }, - timeout, - }, - ); - - return access_token; - }; -} diff --git a/packages/core/src/connectors/insee/get-access-token.ts b/packages/core/src/connectors/insee/get-access-token.ts deleted file mode 100644 index 80727d027..000000000 --- a/packages/core/src/connectors/insee/get-access-token.ts +++ /dev/null @@ -1,43 +0,0 @@ -// - -import axios, { type AxiosResponse } from "axios"; - -// - -type GetInseeAccessTokenCradle = { - fetch: typeof globalThis.fetch; - username: string; - password: string; - timeout: number; -}; - -type GetTokenReponse = { - access_token: string; - scope: "am_application_scope default"; - token_type: "Bearer"; - expires_in: number; -}; - -// - -export function GetInseeAccessToken(cradle: GetInseeAccessTokenCradle) { - const { username, password, timeout } = cradle; - return async function getInseeAccessToken() { - const { - data: { access_token }, - }: AxiosResponse = await axios.post( - "https://api.insee.fr/token", - "grant_type=client_credentials", - { - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - auth: { - username, - password, - }, - timeout, - }, - ); - - return access_token; - }; -} diff --git a/packages/core/src/connectors/insee/index.ts b/packages/core/src/connectors/insee/index.ts deleted file mode 100644 index 6497cf609..000000000 --- a/packages/core/src/connectors/insee/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// - -export * from "./get-access-token.js"; diff --git a/packages/identite/package.json b/packages/identite/package.json new file mode 100644 index 000000000..428351a58 --- /dev/null +++ b/packages/identite/package.json @@ -0,0 +1,64 @@ +{ + "name": "@gouvfr-lasuite/proconnect.identite", + "version": "0.2.0", + "homepage": "https://github.com/numerique-gouv/moncomptepro/tree/master/packages/identite#readme", + "bugs": "https://github.com/numerique-gouv/moncomptepro/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/numerique-gouv/moncomptepro.git", + "directory": "packages/identite" + }, + "license": "MIT", + "sideEffects": false, + "type": "module", + "imports": { + "#src/*": { + "types": "./src/*", + "default": "./dist/*" + } + }, + "exports": { + "./*": { + "require": { + "types": "./dist/*/index.d.ts", + "default": "./dist/*/index.js" + }, + "import": { + "types": "./dist/*/index.d.ts", + "default": "./dist/*/index.js" + }, + "types": "./dist/*/index.d.ts", + "default": "./dist/*/index.js" + } + }, + "scripts": { + "build": "tsc --project tsconfig.lib.json", + "check": "npm run build -- --noEmit", + "dev": "npm run build -- --watch --preserveWatchOutput", + "test": "mocha" + }, + "mocha": { + "reporter": "spec", + "require": [ + "tsx" + ], + "spec": "src/**/*.test.ts" + }, + "dependencies": {}, + "devDependencies": { + "@electric-sql/pglite": "^0.2.15", + "@gouvfr-lasuite/proconnect.core": "^0.2.0", + "@tsconfig/node22": "^22.0.0", + "@types/mocha": "^10.0.10", + "@types/node": "^22.10.2", + "chai": "^5.1.2", + "mocha": "^11.0.1", + "node-pg-migrate": "^7.6.1", + "pg": "^8.13.0", + "tsx": "^4.19.2" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/packages/identite/src/types/index.ts b/packages/identite/src/types/index.ts new file mode 100644 index 000000000..57f2d37b1 --- /dev/null +++ b/packages/identite/src/types/index.ts @@ -0,0 +1,3 @@ +// + +export * from "./user.js"; diff --git a/src/types/user.d.ts b/packages/identite/src/types/user.ts similarity index 96% rename from src/types/user.d.ts rename to packages/identite/src/types/user.ts index f927ce2cd..683b91f19 100644 --- a/src/types/user.d.ts +++ b/packages/identite/src/types/user.ts @@ -1,27 +1,29 @@ -interface User { - id: number; - email: string; - encrypted_password: string | null; - reset_password_token: string | null; - reset_password_sent_at: Date | null; - sign_in_count: number; - last_sign_in_at: Date | null; +// + +export interface User { created_at: Date; - updated_at: Date; + current_challenge: string | null; + email_verified_at: Date | null; email_verified: boolean; - verify_email_token: string | null; - verify_email_sent_at: Date | null; - given_name: string | null; + email: string; + encrypted_password: string | null; + encrypted_totp_key: string | null; family_name: string | null; - phone_number: string | null; + force_2fa: boolean; + given_name: string | null; + id: number; job: string | null; - magic_link_token: string | null; + last_sign_in_at: Date | null; magic_link_sent_at: Date | null; - email_verified_at: Date | null; - current_challenge: string | null; - needs_inclusionconnect_welcome_page: boolean; + magic_link_token: string | null; needs_inclusionconnect_onboarding_help: boolean; - encrypted_totp_key: string | null; + needs_inclusionconnect_welcome_page: boolean; + phone_number: string | null; + reset_password_sent_at: Date | null; + reset_password_token: string | null; + sign_in_count: number; totp_key_verified_at: Date | null; - force_2fa: boolean; + updated_at: Date; + verify_email_sent_at: Date | null; + verify_email_token: string | null; } diff --git a/packages/identite/src/user/find-by-email.test.ts b/packages/identite/src/user/find-by-email.test.ts new file mode 100644 index 000000000..171db101c --- /dev/null +++ b/packages/identite/src/user/find-by-email.test.ts @@ -0,0 +1,69 @@ +// + +import { PGlite } from "@electric-sql/pglite"; +import { expect } from "chai"; +import { before, describe, it } from "mocha"; +import { runner } from "node-pg-migrate"; +import { join } from "path"; +import { FindByEmail } from "./find-by-email.js"; + +// + +const pg = new PGlite(); +const findByEmail = FindByEmail({ pg: pg as any }); + +before(async function migrate() { + await runner({ + dbClient: pg as any, + dir: join(import.meta.dirname, "../../../../migrations"), + direction: "up", + migrationsTable: "pg-migrate", + }); +}); + +describe("FindByEmail", () => { + it("should find a user by email", async () => { + await pg.sql`INSERT INTO users + (id, email, created_at, updated_at, given_name, family_name, phone_number, job) + VALUES + (1, 'lion.eljonson@darkangels.world', '4444-04-04', '4444-04-04', 'Lion', 'El''Jonson', 'I', 'Primarque'), + (2, 'perturabo@ironwarriors.world', '4444-04-04', '4444-04-04', 'Lion', 'El''Jonson', 'IV', 'Primarque'); + `; + + const user = await findByEmail("lion.eljonson@darkangels.world"); + + expect(user).to.deep.equal({ + created_at: new Date("4444-04-04T00:00:00.000Z"), + current_challenge: null, + email_verified_at: null, + email_verified: false, + email: "lion.eljonson@darkangels.world", + encrypted_password: "", + encrypted_totp_key: null, + family_name: "El'Jonson", + force_2fa: false, + given_name: "Lion", + id: 1, + job: "Primarque", + last_sign_in_at: null, + magic_link_sent_at: null, + magic_link_token: null, + needs_inclusionconnect_onboarding_help: false, + needs_inclusionconnect_welcome_page: false, + phone_number: "I", + reset_password_sent_at: null, + reset_password_token: null, + sign_in_count: 0, + totp_key_verified_at: null, + updated_at: new Date("4444-04-04T00:00:00.000Z"), + verify_email_sent_at: null, + verify_email_token: null, + }); + }); + + it("❎ fail to find the God-Emperor of Mankind", async () => { + const user = await findByEmail("the God-Emperor of Mankind"); + + expect(user).to.be.undefined; + }); +}); diff --git a/packages/identite/src/user/find-by-email.ts b/packages/identite/src/user/find-by-email.ts new file mode 100644 index 000000000..68510b567 --- /dev/null +++ b/packages/identite/src/user/find-by-email.ts @@ -0,0 +1,24 @@ +// + +import type { User } from "#src/types/user.js"; +import Pg, { type QueryResult } from "pg"; + +// + +type PostgresCradle = { + pg: Pg.Pool; +}; + +export function FindByEmail({ pg }: PostgresCradle) { + return async function findByEmail(email: string) { + const { rows }: QueryResult = await pg.query( + ` + SELECT * + FROM users WHERE email = $1 + `, + [email], + ); + + return rows.shift(); + }; +} diff --git a/packages/identite/src/user/index.ts b/packages/identite/src/user/index.ts new file mode 100644 index 000000000..843716a0d --- /dev/null +++ b/packages/identite/src/user/index.ts @@ -0,0 +1,3 @@ +// + +export * from "./find-by-email.js"; diff --git a/packages/identite/tsconfig.json b/packages/identite/tsconfig.json new file mode 100644 index 000000000..ec2a63458 --- /dev/null +++ b/packages/identite/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "src", + "types": ["node"], + "module": "NodeNext", + "moduleResolution": "nodenext", + "verbatimModuleSyntax": true, + "paths": { + "#src/*": ["./src/*"] + } + }, + "extends": "@tsconfig/node22/tsconfig.json", + "references": [] +} diff --git a/packages/identite/tsconfig.lib.json b/packages/identite/tsconfig.lib.json new file mode 100644 index 000000000..3ba354418 --- /dev/null +++ b/packages/identite/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "exclude": ["src/**/*.test.ts"], + "extends": "./tsconfig.json", + "include": ["src"] +} diff --git a/packages/insee/package.json b/packages/insee/package.json new file mode 100644 index 000000000..dc9dbf98d --- /dev/null +++ b/packages/insee/package.json @@ -0,0 +1,63 @@ +{ + "name": "@gouvfr-lasuite/proconnect.insee", + "version": "0.2.0", + "homepage": "https://github.com/numerique-gouv/moncomptepro/tree/master/packages/insee#readme", + "bugs": "https://github.com/numerique-gouv/moncomptepro/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/numerique-gouv/moncomptepro.git", + "directory": "packages/insee" + }, + "license": "MIT", + "sideEffects": false, + "type": "module", + "imports": { + "#src/*": { + "types": "./src/*", + "default": "./dist/*" + } + }, + "exports": { + "./*": { + "require": { + "types": "./dist/*/index.d.ts", + "default": "./dist/*/index.js" + }, + "import": { + "types": "./dist/*/index.d.ts", + "default": "./dist/*/index.js" + }, + "types": "./dist/*/index.d.ts", + "default": "./dist/*/index.js" + } + }, + "scripts": { + "build": "tsc --project tsconfig.lib.json", + "check": "npm run build -- --noEmit", + "dev": "npm run build -- --watch --preserveWatchOutput", + "test": "mocha" + }, + "mocha": { + "reporter": "spec", + "require": [ + "tsx" + ], + "spec": "src/**/*.test.ts" + }, + "dependencies": { + "axios": "^1.7.7" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.0", + "@types/mocha": "^10.0.10", + "@types/node": "^22.10.2", + "chai": "^5.1.2", + "mocha": "^11.0.1", + "nock": "^13.5.6", + "tsx": "^4.19.2" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/packages/insee/src/token/get-insee-access-token.test.ts b/packages/insee/src/token/get-insee-access-token.test.ts new file mode 100644 index 000000000..c1b42aa73 --- /dev/null +++ b/packages/insee/src/token/get-insee-access-token.test.ts @@ -0,0 +1,26 @@ +// + +import { expect } from "chai"; +import { describe, it } from "mocha"; +import nock from "nock"; +import { getInseeAccessTokenFactory } from "./get-insee-access-token.js"; + +// + +const getInseeAccessToken = getInseeAccessTokenFactory({ + consumerKey: "🔑", + consumerSecret: "㊙️", +}); + +describe("GetInseeAccessToken", () => { + it("should return 🛂 access token", async () => { + nock("https://api.insee.fr").post("/token").reply(200, { + access_token: "🛂", + scope: "am_application_scope default", + token_type: "Bearer", + expires_in: 123456, + }); + const access_token = await getInseeAccessToken(); + expect(access_token).to.be.equal("🛂"); + }); +}); diff --git a/packages/core/src/connectors/get-insee-access-token.test.ts b/packages/insee/src/token/get-insee-access-token.ts similarity index 55% rename from packages/core/src/connectors/get-insee-access-token.test.ts rename to packages/insee/src/token/get-insee-access-token.ts index 80727d027..6f4ec0ffa 100644 --- a/packages/core/src/connectors/get-insee-access-token.test.ts +++ b/packages/insee/src/token/get-insee-access-token.ts @@ -1,17 +1,14 @@ // -import axios, { type AxiosResponse } from "axios"; +import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"; // -type GetInseeAccessTokenCradle = { - fetch: typeof globalThis.fetch; - username: string; - password: string; - timeout: number; +export type InseeCredentials = { + consumerKey: string; + consumerSecret: string; }; - -type GetTokenReponse = { +export type GetTokenReponse = { access_token: string; scope: "am_application_scope default"; token_type: "Bearer"; @@ -20,8 +17,10 @@ type GetTokenReponse = { // -export function GetInseeAccessToken(cradle: GetInseeAccessTokenCradle) { - const { username, password, timeout } = cradle; +export function getInseeAccessTokenFactory( + credentials: InseeCredentials, + config?: AxiosRequestConfig, +) { return async function getInseeAccessToken() { const { data: { access_token }, @@ -31,10 +30,10 @@ export function GetInseeAccessToken(cradle: GetInseeAccessTokenCradle) { { headers: { "Content-Type": "application/x-www-form-urlencoded" }, auth: { - username, - password, + username: credentials.consumerKey, + password: credentials.consumerSecret, }, - timeout, + ...config, }, ); diff --git a/packages/core/src/connectors/index.ts b/packages/insee/src/token/index.ts similarity index 100% rename from packages/core/src/connectors/index.ts rename to packages/insee/src/token/index.ts diff --git a/packages/insee/tsconfig.json b/packages/insee/tsconfig.json new file mode 100644 index 000000000..ec2a63458 --- /dev/null +++ b/packages/insee/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "src", + "types": ["node"], + "module": "NodeNext", + "moduleResolution": "nodenext", + "verbatimModuleSyntax": true, + "paths": { + "#src/*": ["./src/*"] + } + }, + "extends": "@tsconfig/node22/tsconfig.json", + "references": [] +} diff --git a/packages/insee/tsconfig.lib.json b/packages/insee/tsconfig.lib.json new file mode 100644 index 000000000..3ba354418 --- /dev/null +++ b/packages/insee/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "exclude": ["src/**/*.test.ts"], + "extends": "./tsconfig.json", + "include": ["src"] +} diff --git a/scripts/import-accounts-coop.ts b/scripts/import-accounts-coop.ts index 9ae3f4386..50ec360cd 100644 --- a/scripts/import-accounts-coop.ts +++ b/scripts/import-accounts-coop.ts @@ -5,15 +5,18 @@ import { isPhoneNumberValid, isSiretValid, } from "@gouvfr-lasuite/proconnect.core/security"; +import { getInseeAccessTokenFactory } from "@gouvfr-lasuite/proconnect.insee/token"; import { AxiosError } from "axios"; import { parse, stringify, transform } from "csv"; import fs from "fs"; import { isEmpty, isString, some, toInteger } from "lodash-es"; import { z } from "zod"; import { - getInseeAccessToken, - getOrganizationInfo, -} from "../src/connectors/api-sirene"; + HTTP_CLIENT_TIMEOUT, + INSEE_CONSUMER_KEY, + INSEE_CONSUMER_SECRET, +} from "../src/config/env"; +import { getOrganizationInfo } from "../src/connectors/api-sirene"; import { findByUserId } from "../src/repositories/organization/getters"; import { linkUserToOrganization, @@ -29,6 +32,16 @@ import { throttleApiCall, } from "../src/services/script-helpers"; +export const getInseeAccessToken = getInseeAccessTokenFactory( + { + consumerKey: INSEE_CONSUMER_KEY, + consumerSecret: INSEE_CONSUMER_SECRET, + }, + { + timeout: HTTP_CLIENT_TIMEOUT, + }, +); + const { INPUT_FILE, OUTPUT_FILE } = z .object({ INPUT_FILE: z.string().default("./input.csv"), diff --git a/src/connectors/api-sirene/index.ts b/src/connectors/api-sirene/index.ts index a2539e058..1e357591d 100644 --- a/src/connectors/api-sirene/index.ts +++ b/src/connectors/api-sirene/index.ts @@ -1,3 +1,4 @@ +import { getInseeAccessTokenFactory } from "@gouvfr-lasuite/proconnect.insee/token"; import axios, { AxiosError, type AxiosResponse } from "axios"; import { cloneDeep, set } from "lodash-es"; import { @@ -168,13 +169,6 @@ type EtablissementSearchResponse = { etablissements: InseeEtablissement[]; }; -type GetTokenReponse = { - access_token: string; - scope: "am_application_scope default"; - token_type: "Bearer"; - expires_in: number; -}; - const hideNonDiffusibleData = ( etablissement: EtablissementSearchBySiretResponse["etablissement"], ): EtablissementSearchBySiretResponse["etablissement"] => { @@ -264,24 +258,15 @@ const hideNonDiffusibleData = ( return hiddenEtablissement; }; -export const getInseeAccessToken = async () => { - const { - data: { access_token }, - }: AxiosResponse = await axios.post( - "https://api.insee.fr/token", - "grant_type=client_credentials", - { - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - auth: { - username: INSEE_CONSUMER_KEY!, - password: INSEE_CONSUMER_SECRET!, - }, - timeout: HTTP_CLIENT_TIMEOUT, - }, - ); - - return access_token; -}; +export const getInseeAccessToken = getInseeAccessTokenFactory( + { + consumerKey: INSEE_CONSUMER_KEY, + consumerSecret: INSEE_CONSUMER_SECRET, + }, + { + timeout: HTTP_CLIENT_TIMEOUT, + }, +); export const getOrganizationInfo = async ( siretOrSiren: string, diff --git a/src/managers/moderation.ts b/src/managers/moderation.ts index 818b29dd6..cb1979788 100644 --- a/src/managers/moderation.ts +++ b/src/managers/moderation.ts @@ -1,4 +1,5 @@ import { ModerationProcessed } from "@gouvfr-lasuite/proconnect.email"; +import type { User } from "@gouvfr-lasuite/proconnect.identite/types"; import { isEmpty } from "lodash-es"; import { MONCOMPTEPRO_HOST } from "../config/env"; import { ForbiddenError, NotFoundError } from "../config/errors"; diff --git a/src/managers/session/authenticated.ts b/src/managers/session/authenticated.ts index 13062eb07..cac74906a 100644 --- a/src/managers/session/authenticated.ts +++ b/src/managers/session/authenticated.ts @@ -1,3 +1,4 @@ +import type { User } from "@gouvfr-lasuite/proconnect.identite/types"; import * as Sentry from "@sentry/node"; import type { Request, Response } from "express"; import { Session, type SessionData } from "express-session"; @@ -24,7 +25,6 @@ import { setBrowserAsTrustedForUser, setIsTrustedBrowserFromLoggedInSession, } from "../browser-authentication"; - export const isWithinAuthenticatedSession = ( session: Session & Partial, ): session is Session & Partial & AuthenticatedSessionData => { diff --git a/src/managers/user.ts b/src/managers/user.ts index c040d7cd5..2fad76262 100644 --- a/src/managers/user.ts +++ b/src/managers/user.ts @@ -16,6 +16,7 @@ import { UpdateTotpApplication, VerifyEmail, } from "@gouvfr-lasuite/proconnect.email"; +import type { User } from "@gouvfr-lasuite/proconnect.identite/types"; import { isEmpty } from "lodash-es"; import { MAGIC_LINK_TOKEN_EXPIRATION_DURATION_IN_MINUTES, diff --git a/src/repositories/organization/getters.ts b/src/repositories/organization/getters.ts index e7a9abb3f..fbb2c8df1 100644 --- a/src/repositories/organization/getters.ts +++ b/src/repositories/organization/getters.ts @@ -1,3 +1,4 @@ +import type { User } from "@gouvfr-lasuite/proconnect.identite/types"; import type { QueryResult } from "pg"; import { getDatabaseConnection } from "../../connectors/postgres"; diff --git a/src/repositories/organization/setters.ts b/src/repositories/organization/setters.ts index 9c5daf974..88b906312 100644 --- a/src/repositories/organization/setters.ts +++ b/src/repositories/organization/setters.ts @@ -1,3 +1,4 @@ +import type { User } from "@gouvfr-lasuite/proconnect.identite/types"; import type { QueryResult } from "pg"; import { getDatabaseConnection } from "../../connectors/postgres"; import { hashToPostgresParams } from "../../services/hash-to-postgres-params"; diff --git a/src/repositories/user.ts b/src/repositories/user.ts index 55254512a..78a6a8f87 100644 --- a/src/repositories/user.ts +++ b/src/repositories/user.ts @@ -1,7 +1,7 @@ +import type { User } from "@gouvfr-lasuite/proconnect.identite/types"; import type { QueryResult } from "pg"; import { getDatabaseConnection } from "../connectors/postgres"; import { hashToPostgresParams } from "../services/hash-to-postgres-params"; - export const findById = async (id: number) => { const connection = getDatabaseConnection();