From 16383b46d14a7430fe16473216c42457cffaad34 Mon Sep 17 00:00:00 2001 From: Douglas DUTEIL Date: Thu, 5 Dec 2024 11:12:23 +0100 Subject: [PATCH 01/38] fix(www): remove explicit catch of leaked password on change --- src/controllers/user/update-password.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/controllers/user/update-password.ts b/src/controllers/user/update-password.ts index 2961ae04f..a4274a3a6 100644 --- a/src/controllers/user/update-password.ts +++ b/src/controllers/user/update-password.ts @@ -1,4 +1,3 @@ -import * as Sentry from "@sentry/node"; import type { NextFunction, Request, Response } from "express"; import { z, ZodError } from "zod"; import { MONCOMPTEPRO_HOST } from "../../config/env"; @@ -144,7 +143,6 @@ export const postChangePasswordController = async ( if (error instanceof LeakedPasswordError) { const resetPasswordToken = req.body.reset_password_token; - Sentry.captureException(error); return res.redirect( `/users/change-password?reset_password_token=${resetPasswordToken}¬ification=leaked_password`, ); From ecb755e8656b8d3196125d0636e4ef0ec1f06876 Mon Sep 17 00:00:00 2001 From: Douglas DUTEIL Date: Thu, 5 Dec 2024 12:03:41 +0100 Subject: [PATCH 02/38] fix(www): ignore Oidc user InvalidRedirectUri --- src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index dc8697179..51159ff3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -170,9 +170,10 @@ let server: Server; async renderError(ctx, { error, error_description }, err) { if ( !( + err instanceof errors.InvalidClient || + err instanceof errors.InvalidRedirectUri || err instanceof errors.InvalidRequest || - err instanceof errors.InvalidRequestUri || - err instanceof errors.InvalidClient + err instanceof errors.InvalidRequestUri ) ) { logger.error(err); From 957837d68ef2c2d925dd66ab7e14a95a807cc41e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Dubigny?= Date: Fri, 13 Dec 2024 16:48:01 +0100 Subject: [PATCH 03/38] refactor: more agnostic naming --- .github/workflows/end-to-end.yml | 8 ++++---- src/config/env.ts | 6 +++--- src/config/env.zod.ts | 6 +++--- src/controllers/user/magic-link.ts | 4 ++-- src/controllers/user/update-password.ts | 4 ++-- src/index.ts | 6 +++--- src/managers/moderation.ts | 4 ++-- src/managers/organization/join.ts | 4 ++-- .../official-contact-email-verification.ts | 4 ++-- src/managers/totp.ts | 4 ++-- src/managers/user.ts | 20 +++++++++---------- src/managers/webauthn.ts | 10 +++------- src/services/security.ts | 8 ++++---- .../mails/unable-to-auto-join-organization.ts | 4 ++-- test/env.zod.test.ts | 6 +++--- test/security.test.ts | 4 ++-- 16 files changed, 49 insertions(+), 53 deletions(-) diff --git a/.github/workflows/end-to-end.yml b/.github/workflows/end-to-end.yml index e3e18c1e6..0bfaff325 100644 --- a/.github/workflows/end-to-end.yml +++ b/.github/workflows/end-to-end.yml @@ -20,7 +20,7 @@ env: FEATURE_SEND_MAIL: "True" INSEE_CONSUMER_KEY: ${{ secrets.INSEE_CONSUMER_KEY }} INSEE_CONSUMER_SECRET: ${{ secrets.INSEE_CONSUMER_SECRET }} - MONCOMPTEPRO_HOST: http://172.18.0.1:3000 + HOST: http://172.18.0.1:3000 ZAMMAD_TOKEN: ${{ secrets.ZAMMAD_TOKEN }} jobs: test: @@ -67,7 +67,7 @@ jobs: HOST: http://localhost:4000 PC_CLIENT_ID: standard_client_id PC_CLIENT_SECRET: standard_client_secret - PC_PROVIDER: ${{ env.MONCOMPTEPRO_HOST }} + PC_PROVIDER: ${{ env.HOST }} PC_SCOPES: openid email profile organization ACR_VALUE_FOR_2FA: https://proconnect.gouv.fr/assurance/consistency-checked-2fa STYLESHEET_URL: "" @@ -80,7 +80,7 @@ jobs: HOST: http://localhost:4001 PC_CLIENT_ID: proconnect_federation_client_id PC_CLIENT_SECRET: proconnect_federation_client_secret - PC_PROVIDER: ${{ env.MONCOMPTEPRO_HOST }} + PC_PROVIDER: ${{ env.HOST }} PC_SCOPES: openid uid given_name usual_name email siren siret organizational_unit belonging_population phone chorusdt is_service_public is_public_service PC_ID_TOKEN_SIGNED_RESPONSE_ALG: ES256 PC_USERINFO_SIGNED_RESPONSE_ALG: ES256 @@ -116,7 +116,7 @@ jobs: - name: Cypress run uses: cypress-io/github-action@v6.7.7 with: - wait-on: ${{ env.MONCOMPTEPRO_HOST }}/users/start-sign-in + wait-on: ${{ env.HOST }}/users/start-sign-in build: npm run build:assets start: npx dotenvx run -f cypress/e2e/${{ matrix.e2e_test }}/env.conf --overload -- npm start install: false diff --git a/src/config/env.ts b/src/config/env.ts index 33a75ec68..3750c2002 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -67,8 +67,8 @@ export const { MAX_DURATION_BETWEEN_TWO_EMAIL_ADDRESS_VERIFICATION_IN_MINUTES, MAX_SUGGESTED_ORGANIZATIONS, MIN_DURATION_BETWEEN_TWO_VERIFICATION_CODE_SENDING_IN_SECONDS, - MONCOMPTEPRO_HOST, - MONCOMPTEPRO_LABEL, + HOST, + APPLICATION_NAME, NODE_ENV, PORT, RECENT_LOGIN_INTERVAL_IN_SECONDS, @@ -84,4 +84,4 @@ export const { VERIFY_EMAIL_TOKEN_EXPIRATION_DURATION_IN_MINUTES, } = parsedEnv.data; -export const MONCOMPTEPRO_IDENTIFIER = new URL(MONCOMPTEPRO_HOST).hostname; +export const MONCOMPTEPRO_IDENTIFIER = new URL(HOST).hostname; diff --git a/src/config/env.zod.ts b/src/config/env.zod.ts index 518dd4e66..a147e97d7 100644 --- a/src/config/env.zod.ts +++ b/src/config/env.zod.ts @@ -47,7 +47,7 @@ export const secretEnvSchema = z.object({ "The SYMMETRIC_ENCRYPTION_KEY environment variable should be 32 bytes long! Use crypto.randomBytes(32).toString('base64') to generate one.", }) .default("aTrueRandom32BytesLongBase64EncodedStringAA="), - SESSION_COOKIE_SECRET: zCoerceArray(z.string()).default("moncompteprosecret"), + SESSION_COOKIE_SECRET: zCoerceArray(z.string()).default("proconnectsecret"), JWKS: zCoerceJson() .default(JSON.stringify(defaultJWKS)) .pipe(z.object({ keys: z.array(z.any()) })), @@ -102,8 +102,8 @@ export const paramsEnvSchema = z.object({ .int() .nonnegative() .default(20 * 60), // 20 minutes in seconds, - MONCOMPTEPRO_HOST: z.string().url().default("http://localhost:3000"), - MONCOMPTEPRO_LABEL: z.string().default("MonComptePro"), + HOST: z.string().url().default("http://localhost:3000"), + APPLICATION_NAME: z.string().default("MonComptePro"), NODE_ENV: z .enum(["production", "development", "test"]) .default("development"), diff --git a/src/controllers/user/magic-link.ts b/src/controllers/user/magic-link.ts index 0fdfebca0..ce40e09de 100644 --- a/src/controllers/user/magic-link.ts +++ b/src/controllers/user/magic-link.ts @@ -1,6 +1,6 @@ import type { NextFunction, Request, Response } from "express"; import { z, ZodError } from "zod"; -import { MONCOMPTEPRO_HOST } from "../../config/env"; +import { HOST } from "../../config/env"; import { InvalidEmailError, InvalidMagicLinkError } from "../../config/errors"; import { createAuthenticatedSession } from "../../managers/session/authenticated"; import { getEmailFromUnauthenticatedSession } from "../../managers/session/unauthenticated"; @@ -18,7 +18,7 @@ export const postSendMagicLinkController = async ( try { await sendSendMagicLinkEmail( getEmailFromUnauthenticatedSession(req)!, - MONCOMPTEPRO_HOST, + HOST, ); return res.redirect(`/users/magic-link-sent`); diff --git a/src/controllers/user/update-password.ts b/src/controllers/user/update-password.ts index 2961ae04f..b32292a48 100644 --- a/src/controllers/user/update-password.ts +++ b/src/controllers/user/update-password.ts @@ -1,7 +1,7 @@ import * as Sentry from "@sentry/node"; import type { NextFunction, Request, Response } from "express"; import { z, ZodError } from "zod"; -import { MONCOMPTEPRO_HOST } from "../../config/env"; +import { HOST } from "../../config/env"; import { InvalidTokenError, LeakedPasswordError, @@ -61,7 +61,7 @@ export const postResetPasswordController = async ( email = parsedBody.login; } - await sendResetPasswordEmail(email, MONCOMPTEPRO_HOST); + await sendResetPasswordEmail(email, HOST); return res.redirect( "/users/start-sign-in?notification=reset_password_email_sent", diff --git a/src/index.ts b/src/index.ts index dc8697179..fa4346dd4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,9 +17,9 @@ import { DEPLOY_ENV, FEATURE_USE_SECURE_COOKIES, FEATURE_USE_SECURITY_RESPONSE_HEADERS, + HOST, JWKS, LOG_LEVEL, - MONCOMPTEPRO_HOST, NODE_ENV, PORT, SENTRY_DSN, @@ -52,7 +52,7 @@ if (SENTRY_DSN) { debug: LOG_LEVEL === "debug", dsn: SENTRY_DSN, environment: DEPLOY_ENV, - initialScope: { tags: { NODE_ENV, DEPLOY_ENV, HOST: MONCOMPTEPRO_HOST } }, + initialScope: { tags: { NODE_ENV, DEPLOY_ENV, HOST } }, integrations: [ new Sentry.Integrations.Express({ app }), new Sentry.Integrations.Http({ tracing: true }), @@ -163,7 +163,7 @@ let server: Server; (oidcClient) => omitBy(oidcClient, isNull) as ClientMetadata, ); - const oidcProvider = new Provider(`${MONCOMPTEPRO_HOST}`, { + const oidcProvider = new Provider(`${HOST}`, { clients: clientsWithoutNullProperties, adapter: oidcProviderRepository, jwks: JWKS, diff --git a/src/managers/moderation.ts b/src/managers/moderation.ts index 818b29dd6..ff8984282 100644 --- a/src/managers/moderation.ts +++ b/src/managers/moderation.ts @@ -1,6 +1,6 @@ import { ModerationProcessed } from "@gouvfr-lasuite/proconnect.email"; import { isEmpty } from "lodash-es"; -import { MONCOMPTEPRO_HOST } from "../config/env"; +import { HOST } from "../config/env"; import { ForbiddenError, NotFoundError } from "../config/errors"; import { sendMail } from "../connectors/mail"; import { @@ -37,7 +37,7 @@ export const sendModerationProcessedEmail = async ({ to: [email], subject: `[ProConnect] Demande pour rejoindre ${cached_libelle || siret}`, html: ModerationProcessed({ - baseurl: MONCOMPTEPRO_HOST, + baseurl: HOST, libelle: cached_libelle || siret, }).toString(), tag: "moderation-processed", diff --git a/src/managers/organization/join.ts b/src/managers/organization/join.ts index 322a45dba..beb00441a 100644 --- a/src/managers/organization/join.ts +++ b/src/managers/organization/join.ts @@ -4,8 +4,8 @@ import { isEmpty, some } from "lodash-es"; import { CRISP_WEBSITE_ID, FEATURE_BYPASS_MODERATION, + HOST, MAX_SUGGESTED_ORGANIZATIONS, - MONCOMPTEPRO_HOST, } from "../../config/env"; import { InseeConnectionError, @@ -421,7 +421,7 @@ export const greetForJoiningOrganization = async ({ to: [email], subject: "Votre compte ProConnect a bien été créé", html: Welcome({ - baseurl: MONCOMPTEPRO_HOST, + baseurl: HOST, family_name: family_name ?? "", given_name: given_name ?? "", }).toString(), diff --git a/src/managers/organization/official-contact-email-verification.ts b/src/managers/organization/official-contact-email-verification.ts index 23124986a..dfc5faf60 100644 --- a/src/managers/organization/official-contact-email-verification.ts +++ b/src/managers/organization/official-contact-email-verification.ts @@ -1,6 +1,6 @@ import { OfficialContactEmailVerification } from "@gouvfr-lasuite/proconnect.email"; import { isEmpty } from "lodash-es"; -import { MONCOMPTEPRO_HOST } from "../../config/env"; +import { HOST } from "../../config/env"; import { ApiAnnuaireError, InvalidTokenError, @@ -108,7 +108,7 @@ export const sendOfficialContactEmailVerificationEmail = async ({ to: [contactEmail], subject: `[ProConnect] Authentifier un email sur ProConnect`, html: OfficialContactEmailVerification({ - baseurl: MONCOMPTEPRO_HOST, + baseurl: HOST, given_name: given_name ?? "", family_name: family_name ?? "", email, diff --git a/src/managers/totp.ts b/src/managers/totp.ts index 99e220fa6..ca6f0bd43 100644 --- a/src/managers/totp.ts +++ b/src/managers/totp.ts @@ -2,8 +2,8 @@ import { generateSecret, generateUri, validateToken } from "@sunknudsen/totp"; import { isEmpty } from "lodash-es"; import qrcode from "qrcode"; import { + APPLICATION_NAME, MONCOMPTEPRO_IDENTIFIER, - MONCOMPTEPRO_LABEL, SYMMETRIC_ENCRYPTION_KEY, } from "../config/env"; import { InvalidTotpTokenError, UserNotFoundError } from "../config/errors"; @@ -21,7 +21,7 @@ export const generateAuthenticatorAppRegistrationOptions = async ( let totpKey = existingTotpKey ?? generateSecret(32); const uri = generateUri( - MONCOMPTEPRO_LABEL, + APPLICATION_NAME, email, totpKey, MONCOMPTEPRO_IDENTIFIER, diff --git a/src/managers/user.ts b/src/managers/user.ts index b6bb4c0e6..f26d107ee 100644 --- a/src/managers/user.ts +++ b/src/managers/user.ts @@ -29,9 +29,9 @@ import { sendMail } from "../connectors/mail"; import { getDidYouMeanSuggestion } from "@gouvfr-lasuite/proconnect.core/services/suggestion/did-you-mean.js"; import { + HOST, MAGIC_LINK_TOKEN_EXPIRATION_DURATION_IN_MINUTES, MAX_DURATION_BETWEEN_TWO_EMAIL_ADDRESS_VERIFICATION_IN_MINUTES, - MONCOMPTEPRO_HOST, RESET_PASSWORD_TOKEN_EXPIRATION_DURATION_IN_MINUTES, VERIFY_EMAIL_TOKEN_EXPIRATION_DURATION_IN_MINUTES, } from "../config/env"; @@ -193,7 +193,7 @@ export const sendEmailAddressVerificationEmail = async ({ to: [user.email], subject: "Vérification de votre adresse email", html: VerifyEmail({ - baseurl: MONCOMPTEPRO_HOST, + baseurl: HOST, token: verify_email_token, }).toString(), tag: "verify-email", @@ -213,7 +213,7 @@ export const sendDeleteUserEmail = async ({ user_id }: { user_id: number }) => { to: [email], subject: "Suppression de compte", html: DeleteAccount({ - baseurl: MONCOMPTEPRO_HOST, + baseurl: HOST, family_name: family_name ?? "", given_name: given_name ?? "", support_email: "contact@moncomptepro.beta.gouv.fr", @@ -238,7 +238,7 @@ export const sendDeleteFreeTOTPApplicationEmail = async ({ subject: "Suppression d'une application d'authentification à double facteur", html: DeleteFreeTotpMail({ - baseurl: MONCOMPTEPRO_HOST, + baseurl: HOST, family_name: family_name ?? "", given_name: given_name ?? "", support_email: "contact@moncomptepro.beta.gouv.fr", @@ -258,7 +258,7 @@ export const sendDisable2faMail = async ({ user_id }: { user_id: number }) => { to: [email], subject: "Désactivation de la validation en deux étapes", html: Delete2faProtection({ - baseurl: MONCOMPTEPRO_HOST, + baseurl: HOST, family_name: family_name ?? "", given_name: given_name ?? "", }).toString(), @@ -280,7 +280,7 @@ export const sendChangeAppliTotpEmail = async ({ to: [email], subject: "Changement d'application d’authentification", html: UpdateTotpApplication({ - baseurl: MONCOMPTEPRO_HOST, + baseurl: HOST, family_name: family_name ?? "", given_name: given_name ?? "", support_email: "contact@moncomptepro.beta.gouv.fr", @@ -304,7 +304,7 @@ export const sendDeleteAccessKeyMail = async ({ to: [email], subject: "Alerte de sécurité", html: DeleteAccessKey({ - baseurl: MONCOMPTEPRO_HOST, + baseurl: HOST, family_name: family_name ?? "", given_name: given_name ?? "", support_email: "contact@moncomptepro.beta.gouv.fr", @@ -328,7 +328,7 @@ export const sendAddFreeTOTPEmail = async ({ to: [email], subject: "Validation en deux étapes activée", html: Add2fa({ - baseurl: MONCOMPTEPRO_HOST, + baseurl: HOST, email, family_name: family_name ?? "", given_name: given_name ?? "", @@ -352,7 +352,7 @@ export const sendActivateAccessKeyMail = async ({ to: [email], subject: "Alerte de sécurité", html: AddAccessKey({ - baseurl: MONCOMPTEPRO_HOST, + baseurl: HOST, family_name: family_name ?? "", given_name: given_name ?? "", support_email: "contact@moncomptepro.beta.gouv.fr", @@ -404,7 +404,7 @@ export const sendUpdatePersonalInformationEmail = async ({ to: [email], subject: "Mise à jour de vos données personnelles", html: UpdatePersonalDataMail({ - baseurl: MONCOMPTEPRO_HOST, + baseurl: HOST, family_name, given_name, updatedFields: updatedFields, diff --git a/src/managers/webauthn.ts b/src/managers/webauthn.ts index 72cad24c2..9bc5d74e7 100644 --- a/src/managers/webauthn.ts +++ b/src/managers/webauthn.ts @@ -13,11 +13,7 @@ import type { import { isEmpty } from "lodash-es"; import moment from "moment"; import "moment-timezone"; -import { - MONCOMPTEPRO_HOST, - MONCOMPTEPRO_IDENTIFIER, - MONCOMPTEPRO_LABEL, -} from "../config/env"; +import { APPLICATION_NAME, HOST, MONCOMPTEPRO_IDENTIFIER } from "../config/env"; import { NotFoundError, UserNotFoundError, @@ -42,11 +38,11 @@ import { logger } from "../services/log"; import { disableForce2fa, enableForce2fa, is2FACapable } from "./2fa"; // Human-readable title for your website -const rpName = MONCOMPTEPRO_LABEL; +const rpName = APPLICATION_NAME; // A unique identifier for your website const rpID = MONCOMPTEPRO_IDENTIFIER; // The URL at which registrations and authentications should occur -const origin = MONCOMPTEPRO_HOST; +const origin = HOST; export const isWebauthnConfiguredForUser = async (user_id: number) => { const user = await findById(user_id); diff --git a/src/services/security.ts b/src/services/security.ts index da1479045..3d604e9b4 100644 --- a/src/services/security.ts +++ b/src/services/security.ts @@ -2,7 +2,7 @@ import bcrypt from "bcryptjs"; import { hasIn, isEmpty, isString } from "lodash-es"; import { customAlphabet, nanoid } from "nanoid/async"; import { parse_host } from "tld-extract"; -import { MONCOMPTEPRO_HOST } from "../config/env"; +import { HOST } from "../config/env"; import notificationMessages from "../config/notification-messages"; import dicewareWordlistFrAlt from "../data/diceware-wordlist-fr-alt"; import type { AmrValue } from "../types/express-session"; @@ -171,18 +171,18 @@ export const getTrustedReferrerPath = (referrer: unknown): string | null => { } const isValidURL = URL.canParse(referrer); - const isValidRelativeURL = URL.canParse(referrer, MONCOMPTEPRO_HOST); + const isValidRelativeURL = URL.canParse(referrer, HOST); let parsedURL: URL; if (isValidURL) { parsedURL = new URL(referrer); } else if (isValidRelativeURL) { // referrer may be relative - parsedURL = new URL(referrer, MONCOMPTEPRO_HOST); + parsedURL = new URL(referrer, HOST); } else { return null; } - const moncompteproURL = new URL(MONCOMPTEPRO_HOST); + const moncompteproURL = new URL(HOST); if (moncompteproURL.origin !== parsedURL.origin) { return null; diff --git a/src/views/mails/unable-to-auto-join-organization.ts b/src/views/mails/unable-to-auto-join-organization.ts index ed11bee45..058f3df9c 100644 --- a/src/views/mails/unable-to-auto-join-organization.ts +++ b/src/views/mails/unable-to-auto-join-organization.ts @@ -1,6 +1,6 @@ // -import { MONCOMPTEPRO_HOST } from "../../config/env"; +import { HOST } from "../../config/env"; // @@ -10,7 +10,7 @@ export function unableToAutoJoinOrganizationMd({ libelle: string; }) { return ` -![MonComptePro](${MONCOMPTEPRO_HOST}/dist/mail-proconnect.png) +![MonComptePro](${HOST}/dist/mail-proconnect.png) Bonjour, diff --git a/test/env.zod.test.ts b/test/env.zod.test.ts index fcd8b0c1b..253ef02b8 100644 --- a/test/env.zod.test.ts +++ b/test/env.zod.test.ts @@ -63,15 +63,15 @@ test("default sample env with configured INSEE secrets", () => { MAX_DURATION_BETWEEN_TWO_EMAIL_ADDRESS_VERIFICATION_IN_MINUTES: 129600, MAX_SUGGESTED_ORGANIZATIONS: 3, MIN_DURATION_BETWEEN_TWO_VERIFICATION_CODE_SENDING_IN_SECONDS: 1200, - MONCOMPTEPRO_HOST: "http://localhost:3000", - MONCOMPTEPRO_LABEL: "MonComptePro", + HOST: "http://localhost:3000", + APPLICATION_NAME: "MonComptePro", NODE_ENV: "development", PORT: 3000, RECENT_LOGIN_INTERVAL_IN_SECONDS: 900, REDIS_URL: "redis://:@127.0.0.1:6379", RESET_PASSWORD_TOKEN_EXPIRATION_DURATION_IN_MINUTES: 60, SENTRY_DSN: "", - SESSION_COOKIE_SECRET: ["moncompteprosecret"], + SESSION_COOKIE_SECRET: ["proconnectsecret"], SESSION_MAX_AGE_IN_SECONDS: 86400, SMTP_URL: "smtp://localhost:1025", SYMMETRIC_ENCRYPTION_KEY: "aTrueRandom32BytesLongBase64EncodedStringAA=", diff --git a/test/security.test.ts b/test/security.test.ts index 94324c547..851c37497 100644 --- a/test/security.test.ts +++ b/test/security.test.ts @@ -1,5 +1,5 @@ import { assert } from "chai"; -import { MONCOMPTEPRO_HOST } from "../src/config/env"; +import { HOST } from "../src/config/env"; import { getTrustedReferrerPath, isEmailValid, @@ -206,7 +206,7 @@ describe("isUrlTrusted", () => { }); it("should trust absolute path on same domain", () => { assert.equal( - getTrustedReferrerPath(`${MONCOMPTEPRO_HOST}/users/join-organization`), + getTrustedReferrerPath(`${HOST}/users/join-organization`), "/users/join-organization", ); }); From 3f9b22e21cf7d664e0eb39d39031845c3e72e1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Dubigny?= Date: Fri, 13 Dec 2024 16:51:29 +0100 Subject: [PATCH 04/38] feat: show ProConnect when registering TOTP --- src/config/env.zod.ts | 4 ++-- test/env.zod.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/config/env.zod.ts b/src/config/env.zod.ts index a147e97d7..75667a0a9 100644 --- a/src/config/env.zod.ts +++ b/src/config/env.zod.ts @@ -10,7 +10,7 @@ export const connectorEnvSchema = z.object({ CRISP_KEY: z.string().default(""), CRISP_PLUGIN_URN: z.string().default(""), CRISP_RESOLVE_DELAY: z.coerce.number().int().nonnegative().default(1_000), // 1 second - CRISP_USER_NICKNAME: z.string().default("MonComptePro"), + CRISP_USER_NICKNAME: z.string().default("ProConnect"), CRISP_WEBSITE_ID: z.string().default(""), CRISP_MODERATION_TAG: zCoerceArray(z.string()).default("identite,moderation"), DATABASE_URL: z.string().url(), @@ -103,7 +103,7 @@ export const paramsEnvSchema = z.object({ .nonnegative() .default(20 * 60), // 20 minutes in seconds, HOST: z.string().url().default("http://localhost:3000"), - APPLICATION_NAME: z.string().default("MonComptePro"), + APPLICATION_NAME: z.string().default("ProConnect"), NODE_ENV: z .enum(["production", "development", "test"]) .default("development"), diff --git a/test/env.zod.test.ts b/test/env.zod.test.ts index 253ef02b8..ed44cec61 100644 --- a/test/env.zod.test.ts +++ b/test/env.zod.test.ts @@ -34,7 +34,7 @@ test("default sample env with configured INSEE secrets", () => { CRISP_MODERATION_TAG: ["identite", "moderation"], CRISP_PLUGIN_URN: "", CRISP_RESOLVE_DELAY: 1000, - CRISP_USER_NICKNAME: "MonComptePro", + CRISP_USER_NICKNAME: "ProConnect", CRISP_WEBSITE_ID: "", DATABASE_URL: "postgres://moncomptepro:moncomptepro@127.0.0.1:5432/moncomptepro", @@ -64,7 +64,7 @@ test("default sample env with configured INSEE secrets", () => { MAX_SUGGESTED_ORGANIZATIONS: 3, MIN_DURATION_BETWEEN_TWO_VERIFICATION_CODE_SENDING_IN_SECONDS: 1200, HOST: "http://localhost:3000", - APPLICATION_NAME: "MonComptePro", + APPLICATION_NAME: "ProConnect", NODE_ENV: "development", PORT: 3000, RECENT_LOGIN_INTERVAL_IN_SECONDS: 900, From 320360917bdca441c18c574707c0060a687313a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Dubigny?= Date: Fri, 13 Dec 2024 16:52:26 +0100 Subject: [PATCH 05/38] commit an old stash of mine --- scripts/import-accounts-coop.ts | 1 + scripts/import-accounts.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/import-accounts-coop.ts b/scripts/import-accounts-coop.ts index 0bbd94197..33f107ee7 100644 --- a/scripts/import-accounts-coop.ts +++ b/scripts/import-accounts-coop.ts @@ -54,6 +54,7 @@ const maxInseeCallRateInMs = rateInMsFromArgs !== 0 ? rateInMsFromArgs : 125; columns: true, trim: true, cast: false, + delimiter: ";", }); // csv Stream is a read and write stream : it reads raw text in CSV and output untransformed records const outputCsvStream = stringify({ quoted_empty: false, diff --git a/scripts/import-accounts.ts b/scripts/import-accounts.ts index 12789604b..04e02c62b 100644 --- a/scripts/import-accounts.ts +++ b/scripts/import-accounts.ts @@ -132,7 +132,7 @@ const maxInseeCallRateInMs = rateInMsFromArgs !== 0 ? rateInMsFromArgs : 125; logger.error(`invalid name ${first_name} ${last_name}`); return done(null); } - if (isEmpty(sub) && sub.length !== 36) { + if (isEmpty(sub)) { i++; rejected_invalid_sub_count++; logger.error(`invalid sub ${sub}`); From 709daf050d1c074bfcf3b5063a3b03f2262cbf72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Dubigny?= Date: Thu, 17 Oct 2024 14:41:10 +0200 Subject: [PATCH 06/38] feat: display errored input when totp code is invalid --- cypress/e2e/activate_totp/fixtures.sql | 6 ++++-- cypress/e2e/activate_totp/index.cy.ts | 19 +++++++++++++++++++ cypress/e2e/signin_with_totp/fixtures.sql | 10 +++++++++- cypress/e2e/signin_with_totp/index.cy.ts | 15 +++++++++++++-- src/controllers/totp.ts | 8 +++++++- src/controllers/user/2fa-sign-in.ts | 15 +++++++++++++-- src/views/authenticator-app-configuration.ejs | 18 +++++++++++++++++- src/views/user/2fa-sign-in.ejs | 17 ++++++++++++++++- 8 files changed, 98 insertions(+), 10 deletions(-) diff --git a/cypress/e2e/activate_totp/fixtures.sql b/cypress/e2e/activate_totp/fixtures.sql index 572ae188a..b0014f48b 100644 --- a/cypress/e2e/activate_totp/fixtures.sql +++ b/cypress/e2e/activate_totp/fixtures.sql @@ -1,7 +1,8 @@ INSERT INTO users (id, email, email_verified, email_verified_at, encrypted_password, created_at, updated_at, given_name, family_name, phone_number, job, force_2fa) VALUES - (1, 'lion.eljonson@darkangels.world', true, CURRENT_TIMESTAMP, '$2a$10$kzY3LINL6..50Fy9shWCcuNlRfYq0ft5lS.KCcJ5PzrhlWfKK4NIO', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'Lion', 'El''Jonson', 'I', 'Primarque', false); + (1, 'lion.eljonson@darkangels.world', true, CURRENT_TIMESTAMP, '$2a$10$kzY3LINL6..50Fy9shWCcuNlRfYq0ft5lS.KCcJ5PzrhlWfKK4NIO', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'Lion', 'El''Jonson', 'I', 'Primarque', false), + (2, 'unused1@yopmail.com', true, CURRENT_TIMESTAMP, '$2a$10$kzY3LINL6..50Fy9shWCcuNlRfYq0ft5lS.KCcJ5PzrhlWfKK4NIO', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'Raphapha', 'Dubibi', '0123456789', 'Sbire', false); INSERT INTO organizations (id, siret, created_at, updated_at) @@ -11,4 +12,5 @@ VALUES INSERT INTO users_organizations (user_id, organization_id, is_external, verification_type, has_been_greeted) VALUES - (1, 1, false, 'verified_email_domain', true); + (1, 1, false, 'verified_email_domain', true), + (2, 1, false, 'verified_email_domain', true); diff --git a/cypress/e2e/activate_totp/index.cy.ts b/cypress/e2e/activate_totp/index.cy.ts index 577a42661..346d6a1f0 100644 --- a/cypress/e2e/activate_totp/index.cy.ts +++ b/cypress/e2e/activate_totp/index.cy.ts @@ -12,6 +12,8 @@ describe("add 2fa authentication", () => { .contains("Configurer un code à usage unique") .click(); + cy.contains("Configurer une application d’authentification"); + // Extract the code from the front to generate the TOTP key cy.get("#humanReadableTotpKey") .invoke("text") @@ -36,4 +38,21 @@ describe("add 2fa authentication", () => { }, ); }); + + it("should see an help link on third failed attempt", function () { + cy.visit("/connection-and-account"); + + cy.login("unused1@yopmail.com"); + + cy.get('[href="/authenticator-app-configuration"]') + .contains("Configurer un code à usage unique") + .click(); + + cy.get("[name=totpToken]").type("123456"); + cy.get( + '[action="/authenticator-app-configuration"] [type="submit"]', + ).click(); + + cy.contains("Code invalide."); + }); }); diff --git a/cypress/e2e/signin_with_totp/fixtures.sql b/cypress/e2e/signin_with_totp/fixtures.sql index 6228c5f56..61e089e3d 100644 --- a/cypress/e2e/signin_with_totp/fixtures.sql +++ b/cypress/e2e/signin_with_totp/fixtures.sql @@ -19,6 +19,12 @@ VALUES 'Jean', 'Jean', '0123456789', 'Sbire', 'kuOSXGk68H2B3pYnph0uyXAHrmpbWaWyX/iX49xVaUc=.VMPBZSO+eAng7mjS.cI2kRY9rwhXchcKiiaMZIg==', CURRENT_TIMESTAMP, true + ), + (4, 'unused4@yopmail.com', true, CURRENT_TIMESTAMP, + '$2a$10$kzY3LINL6..50Fy9shWCcuNlRfYq0ft5lS.KCcJ5PzrhlWfKK4NIO', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, + 'Jean', 'Jean', '0123456789', 'Sbire', + 'kuOSXGk68H2B3pYnph0uyXAHrmpbWaWyX/iX49xVaUc=.VMPBZSO+eAng7mjS.cI2kRY9rwhXchcKiiaMZIg==', + CURRENT_TIMESTAMP, true ); INSERT INTO organizations @@ -30,7 +36,9 @@ INSERT INTO users_organizations (user_id, organization_id, is_external, verification_type, has_been_greeted) VALUES (1, 1, false, 'domain', true), - (2, 1, false, 'domain', true); + (2, 1, false, 'domain', true), + (3, 1, false, 'domain', true), + (4, 1, false, 'domain', true); INSERT INTO oidc_clients (client_name, client_id, client_secret, redirect_uris, diff --git a/cypress/e2e/signin_with_totp/index.cy.ts b/cypress/e2e/signin_with_totp/index.cy.ts index 839ae94d8..1e086a537 100644 --- a/cypress/e2e/signin_with_totp/index.cy.ts +++ b/cypress/e2e/signin_with_totp/index.cy.ts @@ -71,17 +71,28 @@ describe("sign-in with TOTP on untrusted browser", () => { cy.contains('"amr": [\n "pwd",\n "totp",\n "mfa"\n ],'); }); - it("should trigger totp rate limiting", function () { + it("should display error message", function () { cy.visit("/users/start-sign-in"); cy.login("unused3@yopmail.com"); + cy.get("[name=totpToken]").type("123456"); + cy.get( + '[action="/users/2fa-sign-in-with-authenticator-app"] [type="submit"]', + ).click(); + cy.contains("Code invalide."); + }); + + it("should trigger totp rate limiting", function () { + cy.visit("/users/start-sign-in"); + + cy.login("unused4@yopmail.com"); + for (let i = 0; i < 5; i++) { cy.get("[name=totpToken]").type("123456"); cy.get( '[action="/users/2fa-sign-in-with-authenticator-app"] [type="submit"]', ).click(); - cy.contains("le code que vous avez utilisé est invalide."); } cy.get("[name=totpToken]").type("123456"); diff --git a/src/controllers/totp.ts b/src/controllers/totp.ts index e8e313ff8..b116cc9e3 100644 --- a/src/controllers/totp.ts +++ b/src/controllers/totp.ts @@ -25,7 +25,9 @@ import { } from "../managers/user"; import { csrfToken } from "../middlewares/csrf-protection"; import { codeSchema } from "../services/custom-zod-schemas"; -import getNotificationsFromRequest from "../services/get-notifications-from-request"; +import getNotificationsFromRequest, { + getNotificationLabelFromRequest, +} from "../services/get-notifications-from-request"; export const getAuthenticatorAppConfigurationController = async ( req: Request, @@ -45,9 +47,13 @@ export const getAuthenticatorAppConfigurationController = async ( setTemporaryTotpKey(req, totpKey); + const notificationLabel = await getNotificationLabelFromRequest(req); + const hasCodeError = notificationLabel === "invalid_totp_token"; + return res.render("authenticator-app-configuration", { pageTitle: "Configuration TOTP", notifications: await getNotificationsFromRequest(req), + hasCodeError, csrfToken: csrfToken(req), isAuthenticatorAlreadyConfigured: await isAuthenticatorAppConfiguredForUser(user_id), diff --git a/src/controllers/user/2fa-sign-in.ts b/src/controllers/user/2fa-sign-in.ts index 986575f39..7225b0802 100644 --- a/src/controllers/user/2fa-sign-in.ts +++ b/src/controllers/user/2fa-sign-in.ts @@ -6,7 +6,9 @@ import { import { isAuthenticatorAppConfiguredForUser } from "../../managers/totp"; import { isWebauthnConfiguredForUser } from "../../managers/webauthn"; import { csrfToken } from "../../middlewares/csrf-protection"; -import getNotificationsFromRequest from "../../services/get-notifications-from-request"; +import getNotificationsFromRequest, { + getNotificationLabelFromRequest, +} from "../../services/get-notifications-from-request"; export const get2faSignInController = async ( req: Request, @@ -17,6 +19,14 @@ export const get2faSignInController = async ( const { id, email } = getUserFromAuthenticatedSession(req); const showsTotpSection = await isAuthenticatorAppConfiguredForUser(id); + let hasCodeError = false; + if (showsTotpSection) { + const notificationLabel = await getNotificationLabelFromRequest(req); + hasCodeError = notificationLabel === "invalid_totp_token"; + } + const notifications = hasCodeError + ? [] + : await getNotificationsFromRequest(req); // If a passkey has already been used for authentication in this session, // we cannot use another passkey, or even the same one, for a second factor. @@ -28,7 +38,8 @@ export const get2faSignInController = async ( return res.render("user/2fa-sign-in", { pageTitle: "Se connecter en deux étapes", - notifications: await getNotificationsFromRequest(req), + notifications, + hasCodeError, csrfToken: csrfToken(req), email, showsTotpSection, diff --git a/src/views/authenticator-app-configuration.ejs b/src/views/authenticator-app-configuration.ejs index 2789912e5..2275c322e 100644 --- a/src/views/authenticator-app-configuration.ejs +++ b/src/views/authenticator-app-configuration.ejs @@ -47,7 +47,7 @@
-
+
@@ -60,7 +60,23 @@ pattern="^(\s*\d){6}$" title="code composé de 6 chiffres" autocomplete="off" + <% if (locals.hasCodeError) { %> + autofocus + aria-describedby="email-error" + <% } %> > + <% if (locals.hasCodeError) { %> +

+ Code invalide.  + + Aide + +

+ <% } %>
diff --git a/src/views/user/2fa-sign-in.ejs b/src/views/user/2fa-sign-in.ejs index 1c121a714..cc66089c5 100644 --- a/src/views/user/2fa-sign-in.ejs +++ b/src/views/user/2fa-sign-in.ejs @@ -29,7 +29,7 @@ -
+
@@ -42,7 +42,22 @@ title="code composé de 6 chiffres" autocomplete="off" autofocus + <% if (locals.hasCodeError) { %> + aria-describedby="email-error" + <% } %> > + <% if (locals.hasCodeError) { %> +

+ Code invalide.  + + Aide + +

+ <% } %>
<% if (showsPasskeySection) { %>
diff --git a/src/views/user/verify-email.ejs b/src/views/user/verify-email.ejs index 2cedc059d..a13897f08 100644 --- a/src/views/user/verify-email.ejs +++ b/src/views/user/verify-email.ejs @@ -1,7 +1,7 @@
<%- include('../partials/notifications.ejs', {notifications: notifications, noWrapperDiv: true}) %>

- Vérifier votre email + Confirmer votre adresse

<% if (locals.needs_inclusionconnect_onboarding_help) { %>
@@ -11,24 +11,19 @@
<% } %>
- + <% if (codeSent || newCodeSent) { %> -

- Un <% if (newCodeSent) { %>nouveau <% } %>code de vérification a été envoyé à - - <%= email; %> - . -

+
+

Un <% if (newCodeSent) { %>nouveau <% } %>code de vérification a été envoyé à <%= email; %>. +

+
<% } %> - - - -
+
- <%- include('../partials/card-button-container.ejs', {label: 'Vérifier', showGoBackButton: true}) %> + <%- include('../partials/card-button-container.ejs', {label: 'Valider', showGoBackButton: false, button:'width: 100%; justify-content: center;', margin: "fr-2w"}) %> + +
+ +
- + + Vous ne recevez rien ? Consulter la page d'aide +
+ From e21f5b26eb95bceca74412e2067de27bbfe6e4de Mon Sep 17 00:00:00 2001 From: rebeccadumazert Date: Wed, 18 Dec 2024 11:10:38 +0100 Subject: [PATCH 20/38] refacto(go-back): create specific composant for go back button --- src/views/partials/go-back.ejs | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/views/partials/go-back.ejs diff --git a/src/views/partials/go-back.ejs b/src/views/partials/go-back.ejs new file mode 100644 index 000000000..bce2975a3 --- /dev/null +++ b/src/views/partials/go-back.ejs @@ -0,0 +1,11 @@ +
+ + + + +
+ From 5e2b43efdf3760a019668b026fbf7a25bd435b75 Mon Sep 17 00:00:00 2001 From: rebeccadumazert Date: Wed, 18 Dec 2024 11:11:04 +0100 Subject: [PATCH 21/38] chore: add go back componant in verify email page --- src/views/user/verify-email.ejs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/views/user/verify-email.ejs b/src/views/user/verify-email.ejs index a13897f08..bfcb299e8 100644 --- a/src/views/user/verify-email.ejs +++ b/src/views/user/verify-email.ejs @@ -54,5 +54,6 @@ > Vous ne recevez rien ? Consulter la page d'aide + <%- include('../partials/go-back.ejs') %>
From 092b065e2bf784175d0081b6c0318d80b34679fc Mon Sep 17 00:00:00 2001 From: rebeccadumazert Date: Wed, 18 Dec 2024 17:11:04 +0100 Subject: [PATCH 22/38] tests: update test --- .../signin_with_email_verification/index.cy.ts | 18 +++++------------- .../index.cy.ts | 4 +++- src/views/user/verify-email.ejs | 4 ++-- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/cypress/e2e/signin_with_email_verification/index.cy.ts b/cypress/e2e/signin_with_email_verification/index.cy.ts index 13b5c50fa..19d284e57 100644 --- a/cypress/e2e/signin_with_email_verification/index.cy.ts +++ b/cypress/e2e/signin_with_email_verification/index.cy.ts @@ -6,7 +6,7 @@ describe("sign-in with email verification renewal", () => { cy.login("lion.eljonson@darkangels.world"); - cy.contains("Vérifier votre email"); + cy.contains("Confirmer votre adresse"); cy.contains( "Information : pour garantir la sécurité de votre compte, nous avons besoin d’authentifier votre navigateur.", @@ -36,16 +36,8 @@ describe("sign-in with email verification renewal", () => { cy.login("rogal.dorn@imperialfists.world"); - cy.get('a[href="/users/verify-email-help"]') - .contains( - "J'ai attendu quelques secondes et je ne reçois pas de code de vérification", - ) - .click(); - - cy.contains("Vous ne recevez pas le code de vérification"); - - cy.get('[action="/users/send-email-verification"] [type="submit"]') - .contains("Cliquez ici pour recevoir un nouveau code") + cy.get('[action="/users/send-email-verification"]') + .contains("Recevoir un nouvel email") .should("be.disabled"); // Wait for countdown to last @@ -53,8 +45,8 @@ describe("sign-in with email verification renewal", () => { cy.maildevDeleteAllMessages(); - cy.get('[action="/users/send-email-verification"] [type="submit"]') - .contains("Cliquez ici pour recevoir un nouveau code") + cy.get('[action="/users/send-email-verification"]') + .contains("Recevoir un nouvel email") .click(); cy.contains( diff --git a/cypress/e2e/signup_entreprise_unipersonnelle/index.cy.ts b/cypress/e2e/signup_entreprise_unipersonnelle/index.cy.ts index 465eb0091..c85353da9 100644 --- a/cypress/e2e/signup_entreprise_unipersonnelle/index.cy.ts +++ b/cypress/e2e/signup_entreprise_unipersonnelle/index.cy.ts @@ -15,7 +15,9 @@ describe("Signup into new entreprise unipersonnelle", () => { cy.get('[action="/users/sign-up"] [type="submit"]').click(); // Check that the website is waiting for the user to verify their email - cy.get("#verify-email > p").contains("lion.eljonson@darkangels.world"); + cy.get("#verify-email > div > h3").contains( + "lion.eljonson@darkangels.world", + ); cy.maildevGetMessageBySubject("Vérification de votre adresse email") .then((email) => { diff --git a/src/views/user/verify-email.ejs b/src/views/user/verify-email.ejs index bfcb299e8..666ab5e30 100644 --- a/src/views/user/verify-email.ejs +++ b/src/views/user/verify-email.ejs @@ -41,8 +41,8 @@ method="post" > - From 2ad4a1c44d4f032f4365ab39f8dd05f5abc97684 Mon Sep 17 00:00:00 2001 From: rebeccadumazert Date: Thu, 19 Dec 2024 02:00:32 +0100 Subject: [PATCH 23/38] refacto: update disabled script for fix display bug --- assets/js/disabled-with-countdown.js | 45 +++++++++++++++++----------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/assets/js/disabled-with-countdown.js b/assets/js/disabled-with-countdown.js index 3076b320e..750a497ee 100644 --- a/assets/js/disabled-with-countdown.js +++ b/assets/js/disabled-with-countdown.js @@ -5,33 +5,44 @@ document.addEventListener( elements.forEach((element) => { const rawEndDate = element.getAttribute("data-countdown-end-date"); + try { const endDateInSeconds = new Date(rawEndDate).getTime() / 1000; const nowInSeconds = new Date().getTime() / 1000; let secondsToEndDate = Math.round(endDateInSeconds - nowInSeconds); let intervalId; - element.disabled = true; - - function updateButtonText() { - const minutes = Math.floor(secondsToEndDate / 60); - const seconds = String(secondsToEndDate % 60).padStart(2, "0"); - element.textContent = `Recevoir un nouvel email (disponible dans ${minutes}:${seconds})`; + if (isNaN(endDateInSeconds)) { + console.error("Invalid date provided in data-countdown-end-date."); + return; } - updateButtonText(); + if (secondsToEndDate > 0) { + element.disabled = true; - intervalId = setInterval(function () { - secondsToEndDate--; - - if (secondsToEndDate > 0) { - updateButtonText(); - } else { - element.disabled = false; - element.textContent = "Recevoir un nouvel email"; - clearInterval(intervalId); + function updateButtonText() { + const minutes = Math.floor(secondsToEndDate / 60); + const seconds = String(secondsToEndDate % 60).padStart(2, "0"); + element.textContent = `Recevoir un nouvel email (disponible dans ${minutes}:${seconds})`; } - }, 1000); + + updateButtonText(); + + intervalId = setInterval(function () { + secondsToEndDate--; + + if (secondsToEndDate > 0) { + updateButtonText(); + } else { + clearInterval(intervalId); + element.disabled = false; + element.textContent = "Recevoir un nouvel email"; + } + }, 1000); + } else { + element.disabled = false; + element.textContent = "Recevoir un nouvel email"; + } } catch (error) { // silently fails } From 99ce1fd7555695f912f5d370391d9ac7dcf12f1f Mon Sep 17 00:00:00 2001 From: rebeccadumazert Date: Fri, 20 Dec 2024 15:29:33 +0100 Subject: [PATCH 24/38] refacto: delete deprecate page --- src/views/user/verify-email-help.ejs | 55 ---------------------------- 1 file changed, 55 deletions(-) delete mode 100644 src/views/user/verify-email-help.ejs diff --git a/src/views/user/verify-email-help.ejs b/src/views/user/verify-email-help.ejs deleted file mode 100644 index 406696aea..000000000 --- a/src/views/user/verify-email-help.ejs +++ /dev/null @@ -1,55 +0,0 @@ -
-

Vous ne recevez pas le code de vérification

-

Vous êtes peut-être dans l’une de ces situations :

-
    -
  • Vous avez fait une erreur de saisie dans votre adresse - <% if (locals.email) { %> - - <%= email; %> - - <% } else { %> - email - <% } %> -
    - 💡  - - Recréez un compte avec la bonne adresse - - -
  • -
  • Le code est arrivé dans vos courriers indésirables
    - 💡 Vérifiez vos spams -
  • -
  • Votre organisation utilise une protection contre les spams (comme MailInBlack)
    - 💡 Contactez votre fournisseur de mail pour qu’il autorise les emails en provenance de - nepasrepondre@email.moncomptepro.beta.gouv.fr (adresse IP : 172.246.41.163) - -
  • -
  • Votre code a expiré ou vous avez perdu l’email qui contenait le code
    - 💡  -
    - - -
    -
    -
  • -
-

Vous n’êtes dans aucune de ces situations ? - - Contactez-nous - . -

-
- From e8b599884013b4581481b262fe26caad7992c416 Mon Sep 17 00:00:00 2001 From: rebeccadumazert Date: Fri, 20 Dec 2024 15:30:37 +0100 Subject: [PATCH 25/38] refacto(script): fix bugs on disabled with countdown script --- assets/js/disabled-with-countdown.js | 38 ++++++++-------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/assets/js/disabled-with-countdown.js b/assets/js/disabled-with-countdown.js index 750a497ee..f52e6cf21 100644 --- a/assets/js/disabled-with-countdown.js +++ b/assets/js/disabled-with-countdown.js @@ -5,44 +5,26 @@ document.addEventListener( elements.forEach((element) => { const rawEndDate = element.getAttribute("data-countdown-end-date"); - try { const endDateInSeconds = new Date(rawEndDate).getTime() / 1000; const nowInSeconds = new Date().getTime() / 1000; let secondsToEndDate = Math.round(endDateInSeconds - nowInSeconds); - let intervalId; - - if (isNaN(endDateInSeconds)) { - console.error("Invalid date provided in data-countdown-end-date."); - return; - } - - if (secondsToEndDate > 0) { - element.disabled = true; + element.disabled = true; + intervalId = setInterval(function () { + secondsToEndDate--; - function updateButtonText() { + if (secondsToEndDate > 0) { const minutes = Math.floor(secondsToEndDate / 60); const seconds = String(secondsToEndDate % 60).padStart(2, "0"); element.textContent = `Recevoir un nouvel email (disponible dans ${minutes}:${seconds})`; } - updateButtonText(); - - intervalId = setInterval(function () { - secondsToEndDate--; - - if (secondsToEndDate > 0) { - updateButtonText(); - } else { - clearInterval(intervalId); - element.disabled = false; - element.textContent = "Recevoir un nouvel email"; - } - }, 1000); - } else { - element.disabled = false; - element.textContent = "Recevoir un nouvel email"; - } + if (secondsToEndDate <= 0) { + element.textContent = `Recevoir un nouvel email`; + element.disabled = false; + return; + } + }, 1000); } catch (error) { // silently fails } From 337ac406199ceb5868734bdbbb9689b22d514d8b Mon Sep 17 00:00:00 2001 From: rebeccadumazert Date: Mon, 6 Jan 2025 18:54:25 +0100 Subject: [PATCH 26/38] fix: fix regex --- assets/js/disabled-with-countdown.js | 15 +++++++++++---- src/controllers/user/verify-email.ts | 11 ++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/assets/js/disabled-with-countdown.js b/assets/js/disabled-with-countdown.js index f52e6cf21..6a3b1d805 100644 --- a/assets/js/disabled-with-countdown.js +++ b/assets/js/disabled-with-countdown.js @@ -9,20 +9,27 @@ document.addEventListener( const endDateInSeconds = new Date(rawEndDate).getTime() / 1000; const nowInSeconds = new Date().getTime() / 1000; let secondsToEndDate = Math.round(endDateInSeconds - nowInSeconds); + let intervalId; element.disabled = true; intervalId = setInterval(function () { secondsToEndDate--; + const prefixText = + element.textContent.match( + /(.*)(\s+\(disponible dans \d+:\d+\))/, + )?.[1] || element.textContent; + let suffixText = ""; + if (secondsToEndDate > 0) { const minutes = Math.floor(secondsToEndDate / 60); const seconds = String(secondsToEndDate % 60).padStart(2, "0"); - element.textContent = `Recevoir un nouvel email (disponible dans ${minutes}:${seconds})`; + suffixText = ` (disponible dans ${minutes}:${seconds})`; } + element.textContent = prefixText + suffixText; - if (secondsToEndDate <= 0) { - element.textContent = `Recevoir un nouvel email`; + if (secondsToEndDate <= 0 || Number.isNaN(secondsToEndDate)) { element.disabled = false; - return; + clearInterval(intervalId); } }, 1000); } catch (error) { diff --git a/src/controllers/user/verify-email.ts b/src/controllers/user/verify-email.ts index 195db3199..6acd53e56 100644 --- a/src/controllers/user/verify-email.ts +++ b/src/controllers/user/verify-email.ts @@ -35,11 +35,8 @@ export const getVerifyEmailController = async ( const { new_code_sent } = await schema.parseAsync(req.query); - const { - email, - needs_inclusionconnect_onboarding_help, - verify_email_sent_at, - } = getUserFromAuthenticatedSession(req); + const { email, needs_inclusionconnect_onboarding_help } = + getUserFromAuthenticatedSession(req); const { codeSent, updatedUser } = await sendEmailAddressVerificationEmail({ email, @@ -52,12 +49,12 @@ export const getVerifyEmailController = async ( pageTitle: "Vérifier votre email", notifications: await getNotificationsFromRequest(req), email, - countdownEndDate: moment(verify_email_sent_at) + countdownEndDate: moment(updatedUser.verify_email_sent_at) .add(MIN_DURATION_BETWEEN_TWO_VERIFICATION_CODE_SENDING_IN_SECONDS, "s") .tz("Europe/Paris") .locale("fr") .format(), - csrfToken: email && csrfToken(req), + csrfToken: csrfToken(req), newCodeSent: new_code_sent, codeSent, needs_inclusionconnect_onboarding_help, From 3db11ed7c716d30230a511c8385d18d546338337 Mon Sep 17 00:00:00 2001 From: rebeccadumazert Date: Tue, 7 Jan 2025 11:29:39 +0100 Subject: [PATCH 27/38] refacto: delete go back template --- src/views/partials/go-back.ejs | 11 ----------- src/views/user/verify-email.ejs | 12 +++++++++++- 2 files changed, 11 insertions(+), 12 deletions(-) delete mode 100644 src/views/partials/go-back.ejs diff --git a/src/views/partials/go-back.ejs b/src/views/partials/go-back.ejs deleted file mode 100644 index bce2975a3..000000000 --- a/src/views/partials/go-back.ejs +++ /dev/null @@ -1,11 +0,0 @@ -
- - - - -
- diff --git a/src/views/user/verify-email.ejs b/src/views/user/verify-email.ejs index 666ab5e30..e0535fefb 100644 --- a/src/views/user/verify-email.ejs +++ b/src/views/user/verify-email.ejs @@ -54,6 +54,16 @@ > Vous ne recevez rien ? Consulter la page d'aide - <%- include('../partials/go-back.ejs') %> +
+ + + + +
+ From 9004e1b026094e5a675be9504d6ae862bb3ffdfd Mon Sep 17 00:00:00 2001 From: rebeccadumazert Date: Tue, 7 Jan 2025 11:53:03 +0100 Subject: [PATCH 28/38] refacto: delete inline style --- src/views/partials/card-button-container.ejs | 4 +++- src/views/user/verify-email.ejs | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/views/partials/card-button-container.ejs b/src/views/partials/card-button-container.ejs index a79471d7c..219cacaaf 100644 --- a/src/views/partials/card-button-container.ejs +++ b/src/views/partials/card-button-container.ejs @@ -10,7 +10,9 @@ <% } %> - diff --git a/src/views/user/verify-email.ejs b/src/views/user/verify-email.ejs index e0535fefb..7311a1668 100644 --- a/src/views/user/verify-email.ejs +++ b/src/views/user/verify-email.ejs @@ -33,7 +33,7 @@ name="verify_email_token" > - <%- include('../partials/card-button-container.ejs', {label: 'Valider', showGoBackButton: false, button:'width: 100%; justify-content: center;', margin: "fr-2w"}) %> + <%- include('../partials/card-button-container.ejs', {label: 'Valider', showGoBackButton: false, buttonFullWidth: true}) %>
From ebd23fbda12f054b5b07bf7a75fd838ba9a0638b Mon Sep 17 00:00:00 2001 From: Douglas Duteil Date: Tue, 7 Jan 2025 13:55:47 +0100 Subject: [PATCH 29/38] refactor(insee): extract insee connector to individual pkg (#912) --- .changeset/hip-houses-confess.md | 7 + .github/workflows/end-to-end.yml | 4 +- .github/workflows/fixtures.yml | 5 +- .github/workflows/release.yml | 5 +- .github/workflows/test.yml | 4 +- Dockerfile | 6 +- package-lock.json | 31 +- package.json | 6 +- packages/insee/package.json | 63 ++++ packages/insee/src/api/find-by-siren.test.ts | 25 ++ packages/insee/src/api/find-by-siren.ts | 49 ++++ packages/insee/src/api/find-by-siret.test.ts | 23 ++ packages/insee/src/api/find-by-siret.ts | 37 +++ .../src/api/get-insee-access-token.test.ts | 26 ++ .../insee/src/api/get-insee-access-token.ts | 46 +++ packages/insee/src/api/index.ts | 5 + .../insee/src/data}/categories-juridiques.ts | 3 + .../insee/src/data}/codes-effectifs.ts | 6 + .../insee/src/data}/codes-naf.ts | 4 + .../insee/src/data}/codes-voie.ts | 3 + packages/insee/src/data/index.ts | 6 + packages/insee/src/errors/index.ts | 8 + .../src/formatters/adresse-etablissement.ts | 75 +++++ packages/insee/src/formatters/enseigne.ts | 8 + packages/insee/src/formatters/index.ts | 8 + .../libelle-from-categories-juridiques.ts | 9 + .../formatters/libelle-from-code-effectif.ts | 25 ++ .../src/formatters/libelle-from-code-naf.ts | 10 + packages/insee/src/formatters/nom-complet.ts | 47 +++ packages/insee/src/types/establishment.ts | 137 +++++++++ packages/insee/src/types/index.ts | 4 + packages/insee/src/types/tranche-effectifs.ts | 36 +++ packages/insee/tsconfig.json | 18 ++ packages/insee/tsconfig.lib.json | 9 + scripts/import-accounts-coop.ts | 12 +- scripts/import-accounts.ts | 12 +- scripts/import-domains.ts | 13 +- src/connectors/api-sirene/formatters.js | 134 --------- src/connectors/api-sirene/index.ts | 269 ++++-------------- src/managers/organization/join.ts | 2 + src/managers/organization/main.ts | 1 + src/repositories/organization/getters.ts | 1 + src/repositories/organization/setters.ts | 2 + src/services/organization.ts | 1 + src/services/script-helpers.ts | 1 + src/types/organization-info.d.ts | 37 +-- src/types/organization.d.ts | 2 + test/api-sirene.test.ts | 2 +- test/organization.test.ts | 1 + 49 files changed, 816 insertions(+), 432 deletions(-) create mode 100644 .changeset/hip-houses-confess.md create mode 100644 packages/insee/package.json create mode 100644 packages/insee/src/api/find-by-siren.test.ts create mode 100644 packages/insee/src/api/find-by-siren.ts create mode 100644 packages/insee/src/api/find-by-siret.test.ts create mode 100644 packages/insee/src/api/find-by-siret.ts create mode 100644 packages/insee/src/api/get-insee-access-token.test.ts create mode 100644 packages/insee/src/api/get-insee-access-token.ts create mode 100644 packages/insee/src/api/index.ts rename {src/connectors/api-sirene => packages/insee/src/data}/categories-juridiques.ts (99%) rename {src/connectors/api-sirene => packages/insee/src/data}/codes-effectifs.ts (90%) rename {src/connectors/api-sirene => packages/insee/src/data}/codes-naf.ts (99%) rename {src/connectors/api-sirene => packages/insee/src/data}/codes-voie.ts (95%) create mode 100644 packages/insee/src/data/index.ts create mode 100644 packages/insee/src/errors/index.ts create mode 100644 packages/insee/src/formatters/adresse-etablissement.ts create mode 100644 packages/insee/src/formatters/enseigne.ts create mode 100644 packages/insee/src/formatters/index.ts create mode 100644 packages/insee/src/formatters/libelle-from-categories-juridiques.ts create mode 100644 packages/insee/src/formatters/libelle-from-code-effectif.ts create mode 100644 packages/insee/src/formatters/libelle-from-code-naf.ts create mode 100644 packages/insee/src/formatters/nom-complet.ts create mode 100644 packages/insee/src/types/establishment.ts create mode 100644 packages/insee/src/types/index.ts create mode 100644 packages/insee/src/types/tranche-effectifs.ts create mode 100644 packages/insee/tsconfig.json create mode 100644 packages/insee/tsconfig.lib.json delete mode 100644 src/connectors/api-sirene/formatters.js diff --git a/.changeset/hip-houses-confess.md b/.changeset/hip-houses-confess.md new file mode 100644 index 000000000..2719a9877 --- /dev/null +++ b/.changeset/hip-houses-confess.md @@ -0,0 +1,7 @@ +--- +"@gouvfr-lasuite/proconnect.insee": minor +--- + +♻️ Prélevement d'un partie du connecteur INSEE + +Dans le cadres la migration du script d'import de comptes coop, une partie de l'API INSEE est déplacées dans le package `@gouvfr-lasuite/proconnect.insee` pour permettre leur réutilisation dans Hyyypertool. diff --git a/.github/workflows/end-to-end.yml b/.github/workflows/end-to-end.yml index e6d4e0c18..581d23998 100644 --- a/.github/workflows/end-to-end.yml +++ b/.github/workflows/end-to-end.yml @@ -108,11 +108,11 @@ jobs: with: cache: "npm" node-version-file: package.json - - run: npm ci --include=dev + - run: npm ci + - run: npm run build:workspaces - run: npm run migrate up - run: npm run fixtures:load-ci -- cypress/e2e/${{ matrix.e2e_test }}/fixtures.sql - run: npm run update-organization-info -- 500 - - run: npm run build:workspaces - name: Cypress run uses: cypress-io/github-action@v6.7.7 with: diff --git a/.github/workflows/fixtures.yml b/.github/workflows/fixtures.yml index edd412c1a..35a70467c 100644 --- a/.github/workflows/fixtures.yml +++ b/.github/workflows/fixtures.yml @@ -37,7 +37,10 @@ jobs: with: cache: "npm" node-version-file: package.json - - run: npm ci --omit=dev # omit dev dependencies to simulate deployed environment + - run: npm ci + env: + CYPRESS_INSTALL_BINARY: 0 + - run: npm run build:workspaces - run: npm run migrate up - run: npm run fixtures:load-ci -- scripts/fixtures.sql - run: npm run update-organization-info -- 500 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bfc59cd00..276fe858f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,10 @@ jobs: cache: "npm" node-version-file: package.json - - run: npm ci --include=dev + - run: npm ci + env: + CYPRESS_INSTALL_BINARY: 0 + - run: npm run build:workspaces - name: Create Release Pull Request diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 608d3495e..dac9deafb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,9 @@ jobs: with: cache: "npm" node-version-file: package.json - - run: CYPRESS_INSTALL_BINARY=0 npm ci --include=dev + - run: npm ci + env: + CYPRESS_INSTALL_BINARY: 0 - run: npm run build:workspaces - run: npm run test:lint - run: npm run test:workspaces diff --git a/Dockerfile b/Dockerfile index 192efafe1..de2ffb24f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,9 @@ WORKDIR /app FROM base AS prod-deps RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=package-lock.json,target=package-lock.json \ - --mount=type=bind,source=packages/email/package.json,target=packages/email/package.json \ --mount=type=bind,source=packages/core/package.json,target=packages/core/package.json \ + --mount=type=bind,source=packages/email/package.json,target=packages/email/package.json \ + --mount=type=bind,source=packages/insee/package.json,target=packages/insee/package.json \ --mount=type=cache,target=/root/.npm \ npm ci --omit=dev @@ -14,8 +15,9 @@ FROM base AS build ENV CYPRESS_INSTALL_BINARY=0 RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=package-lock.json,target=package-lock.json \ - --mount=type=bind,source=packages/email/package.json,target=packages/email/package.json \ --mount=type=bind,source=packages/core/package.json,target=packages/core/package.json \ + --mount=type=bind,source=packages/email/package.json,target=packages/email/package.json \ + --mount=type=bind,source=packages/insee/package.json,target=packages/insee/package.json \ --mount=type=cache,target=/root/.npm \ npm ci COPY tsconfig.json vite.config.mjs ./ diff --git a/package-lock.json b/package-lock.json index 34cde89df..b9d2d815f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@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.insee": "workspace:*", "@gouvfr/dsfr": "^1.12.1", "@kitajs/html": "^4.2.4", "@kitajs/ts-html-plugin": "^4.1.0", @@ -96,7 +97,7 @@ "cypress-axe": "^1.5.0", "cypress-maildev": "^1.3.2", "mocha": "^11.0.1", - "nock": "^13.5.4", + "nock": "^13.5.6", "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.1.0" }, @@ -1286,6 +1287,10 @@ "resolved": "packages/email", "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", @@ -7149,10 +7154,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", @@ -10460,6 +10466,23 @@ "devDependencies": { "@tsconfig/node22": "^22.0.0" } + }, + "packages/insee": { + "name": "@gouvfr-lasuite/proconnect.insee", + "version": "0.2.0", + "license": "MIT", + "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" + } } } } diff --git a/package.json b/package.json index 964d957fc..fdbd26a21 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "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" }, "prettier": { "plugins": [ @@ -51,6 +52,7 @@ "@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.insee": "workspace:*", "@gouvfr/dsfr": "^1.12.1", "@kitajs/html": "^4.2.4", "@kitajs/ts-html-plugin": "^4.1.0", @@ -130,7 +132,7 @@ "cypress-axe": "^1.5.0", "cypress-maildev": "^1.3.2", "mocha": "^11.0.1", - "nock": "^13.5.4", + "nock": "^13.5.6", "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.1.0" }, diff --git a/packages/insee/package.json b/packages/insee/package.json new file mode 100644 index 000000000..1856b8930 --- /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": "./dist/*/index.d.ts", + "default": "./dist/*/index.js" + } + }, + "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/api/find-by-siren.test.ts b/packages/insee/src/api/find-by-siren.test.ts new file mode 100644 index 000000000..c066eda3e --- /dev/null +++ b/packages/insee/src/api/find-by-siren.test.ts @@ -0,0 +1,25 @@ +// + +import { expect } from "chai"; +import { describe, it } from "mocha"; +import nock from "nock"; +import { findBySirenFactory } from "./find-by-siren.js"; + +// + +const findBySiren = findBySirenFactory({ + getInseeAccessToken: async () => "SECRET_INSEE_TOKEN", +}); + +describe("findBySiren", () => { + it("should return an establishment", async () => { + nock("https://api.insee.fr") + .get( + "/entreprises/sirene/siret?q=siren:200071843 AND etablissementSiege:true", + ) + .reply(200, { etablissements: [{ siren: "🦄" }] }); + + const establishment = await findBySiren("200071843"); + expect(establishment).to.be.deep.equal({ siren: "🦄" }); + }); +}); diff --git a/packages/insee/src/api/find-by-siren.ts b/packages/insee/src/api/find-by-siren.ts new file mode 100644 index 000000000..96f470abd --- /dev/null +++ b/packages/insee/src/api/find-by-siren.ts @@ -0,0 +1,49 @@ +// + +import type { GetInseeAccessTokenHandler } from "#src/api"; +import { InvalidSirenError } from "#src/errors"; +import type { InseeEtablissement } from "#src/types"; +import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"; + +// + +type FactoryDependencies = { + getInseeAccessToken: GetInseeAccessTokenHandler; + config?: AxiosRequestConfig; +}; + +type EtablissementSearchResponse = { + header: { + total: number; + debut: number; + nombre: number; + }; + etablissements: InseeEtablissement[]; +}; + +export function findBySirenFactory({ + getInseeAccessToken, + config, +}: FactoryDependencies) { + return async function findBySiren(siren: string) { + const token = await getInseeAccessToken(); + const { data }: AxiosResponse = + await axios.get( + `https://api.insee.fr/entreprises/sirene/siret?q=siren:${siren} AND etablissementSiege:true`, + { + headers: { Authorization: `Bearer ${token}` }, + ...config, + }, + ); + + const establishment = data.etablissements.at(0); + + if (!establishment) { + throw new InvalidSirenError(); + } + + return establishment; + }; +} + +export type FindBySirenHandler = ReturnType; diff --git a/packages/insee/src/api/find-by-siret.test.ts b/packages/insee/src/api/find-by-siret.test.ts new file mode 100644 index 000000000..f052994b9 --- /dev/null +++ b/packages/insee/src/api/find-by-siret.test.ts @@ -0,0 +1,23 @@ +// + +import { expect } from "chai"; +import { describe, it } from "mocha"; +import nock from "nock"; +import { findBySiretFactory } from "./find-by-siret.js"; + +// + +const findBySiret = findBySiretFactory({ + getInseeAccessToken: async () => "SECRET_INSEE_TOKEN", +}); + +describe("findBySiret", () => { + it("should return an establishment", async () => { + nock("https://api.insee.fr") + .get("/entreprises/sirene/siret/20007184300060") + .reply(200, { etablissement: { siren: "🦄" } }); + + const establishment = await findBySiret("20007184300060"); + expect(establishment).to.be.deep.equal({ siren: "🦄" }); + }); +}); diff --git a/packages/insee/src/api/find-by-siret.ts b/packages/insee/src/api/find-by-siret.ts new file mode 100644 index 000000000..e99d388da --- /dev/null +++ b/packages/insee/src/api/find-by-siret.ts @@ -0,0 +1,37 @@ +// + +import type { GetInseeAccessTokenHandler } from "#src/api"; +import type { InseeEtablissement } from "#src/types"; +import type { AxiosRequestConfig, AxiosResponse } from "axios"; +import axios from "axios"; + +// + +type FactoryDependencies = { + getInseeAccessToken: GetInseeAccessTokenHandler; + config?: AxiosRequestConfig; +}; +type EtablissementSearchBySiretResponse = { + etablissement: InseeEtablissement; +}; + +export function findBySiretFactory({ + getInseeAccessToken, + config, +}: FactoryDependencies) { + return async function findBySiret(siret: string) { + const token = await getInseeAccessToken(); + const { data }: AxiosResponse = + await axios.get( + `https://api.insee.fr/entreprises/sirene/siret/${siret}`, + { + headers: { Authorization: `Bearer ${token}` }, + ...config, + }, + ); + + return data.etablissement; + }; +} + +export type FindBySiretHandler = ReturnType; diff --git a/packages/insee/src/api/get-insee-access-token.test.ts b/packages/insee/src/api/get-insee-access-token.test.ts new file mode 100644 index 000000000..0656ae40a --- /dev/null +++ b/packages/insee/src/api/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/insee/src/api/get-insee-access-token.ts b/packages/insee/src/api/get-insee-access-token.ts new file mode 100644 index 000000000..aade19c08 --- /dev/null +++ b/packages/insee/src/api/get-insee-access-token.ts @@ -0,0 +1,46 @@ +// + +import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"; + +// + +export type InseeCredentials = { + consumerKey: string; + consumerSecret: string; +}; +export type GetTokenResponse = { + access_token: string; + scope: "am_application_scope default"; + token_type: "Bearer"; + expires_in: number; +}; + +// + +export function getInseeAccessTokenFactory( + credentials: InseeCredentials, + config?: AxiosRequestConfig, +) { + 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: credentials.consumerKey, + password: credentials.consumerSecret, + }, + ...config, + }, + ); + + return access_token; + }; +} + +export type GetInseeAccessTokenHandler = ReturnType< + typeof getInseeAccessTokenFactory +>; diff --git a/packages/insee/src/api/index.ts b/packages/insee/src/api/index.ts new file mode 100644 index 000000000..250d27c31 --- /dev/null +++ b/packages/insee/src/api/index.ts @@ -0,0 +1,5 @@ +// + +export * from "./find-by-siren.js"; +export * from "./find-by-siret.js"; +export * from "./get-insee-access-token.js"; diff --git a/src/connectors/api-sirene/categories-juridiques.ts b/packages/insee/src/data/categories-juridiques.ts similarity index 99% rename from src/connectors/api-sirene/categories-juridiques.ts rename to packages/insee/src/data/categories-juridiques.ts index 0d25fce5c..5c7bc198f 100644 --- a/src/connectors/api-sirene/categories-juridiques.ts +++ b/packages/insee/src/data/categories-juridiques.ts @@ -1,3 +1,6 @@ +// + +export type CategoriesJuridique = keyof typeof categoriesJuridiques; export const categoriesJuridiques = { 1: "Entrepreneur individuel", 2: "Groupement de droit privé non doté de la personnalité morale", diff --git a/src/connectors/api-sirene/codes-effectifs.ts b/packages/insee/src/data/codes-effectifs.ts similarity index 90% rename from src/connectors/api-sirene/codes-effectifs.ts rename to packages/insee/src/data/codes-effectifs.ts index 6caa17197..a46976090 100644 --- a/src/connectors/api-sirene/codes-effectifs.ts +++ b/packages/insee/src/data/codes-effectifs.ts @@ -1,3 +1,9 @@ +// + +import type { TrancheEffectifs } from "#src/types/tranche-effectifs.js"; + +// + export const codesEffectifs: { [K in NonNullable]: string } = { NN: "Unité non employeuse (pas de salarié au cours de l'année de référence et pas d'effectif au 31/12)", diff --git a/src/connectors/api-sirene/codes-naf.ts b/packages/insee/src/data/codes-naf.ts similarity index 99% rename from src/connectors/api-sirene/codes-naf.ts rename to packages/insee/src/data/codes-naf.ts index 7d4a326a9..18229ee01 100644 --- a/src/connectors/api-sirene/codes-naf.ts +++ b/packages/insee/src/data/codes-naf.ts @@ -1,3 +1,7 @@ +// + +export type CodeNaf = keyof typeof codesNaf; + export const codesNaf = { "01.11Z": "Culture de céréales (à l’exception du riz), de légumineuses et de graines oléagineuses", diff --git a/src/connectors/api-sirene/codes-voie.ts b/packages/insee/src/data/codes-voie.ts similarity index 95% rename from src/connectors/api-sirene/codes-voie.ts rename to packages/insee/src/data/codes-voie.ts index 07a9a8a0e..013db1bfb 100644 --- a/src/connectors/api-sirene/codes-voie.ts +++ b/packages/insee/src/data/codes-voie.ts @@ -1,3 +1,6 @@ +// + +export type CodeVoie = keyof typeof codesVoies; export const codesVoies = { AIRE: "Aire", ALL: "Allée", diff --git a/packages/insee/src/data/index.ts b/packages/insee/src/data/index.ts new file mode 100644 index 000000000..a2b127a83 --- /dev/null +++ b/packages/insee/src/data/index.ts @@ -0,0 +1,6 @@ +// + +export * from "./categories-juridiques.js"; +export * from "./codes-effectifs.js"; +export * from "./codes-naf.js"; +export * from "./codes-voie.js"; diff --git a/packages/insee/src/errors/index.ts b/packages/insee/src/errors/index.ts new file mode 100644 index 000000000..6930b5e7b --- /dev/null +++ b/packages/insee/src/errors/index.ts @@ -0,0 +1,8 @@ +// + +export class InseeNotFoundError extends Error {} + +export class InvalidSiretError extends Error {} +export class InvalidSirenError extends Error {} + +export class InseeConnectionError extends Error {} diff --git a/packages/insee/src/formatters/adresse-etablissement.ts b/packages/insee/src/formatters/adresse-etablissement.ts new file mode 100644 index 000000000..94ae9f582 --- /dev/null +++ b/packages/insee/src/formatters/adresse-etablissement.ts @@ -0,0 +1,75 @@ +// + +import { codesVoies, type CodeVoie } from "#src/data"; +import type { InseeEtablissement } from "#src/types"; +import { capitalize } from "lodash-es"; + +// + +export const formatAdresseEtablissement = ({ + complementAdresseEtablissement, + numeroVoieEtablissement, + indiceRepetitionEtablissement, + typeVoieEtablissement, + libelleVoieEtablissement, + distributionSpecialeEtablissement, + codePostalEtablissement, + libelleCommuneEtablissement, + codeCedexEtablissement, + libelleCedexEtablissement, + libelleCommuneEtrangerEtablissement, + codePaysEtrangerEtablissement, + libellePaysEtrangerEtablissement, +}: InseeEtablissement["adresseEtablissement"]) => { + if ( + !complementAdresseEtablissement && + !numeroVoieEtablissement && + !typeVoieEtablissement && + !libelleCommuneEtablissement && + !distributionSpecialeEtablissement && + !codePostalEtablissement && + !codeCedexEtablissement && + !libelleVoieEtablissement && + !libelleCommuneEtrangerEtablissement && + !codePaysEtrangerEtablissement && + !libellePaysEtrangerEtablissement + ) { + return ""; + } + + const fullLibelleFromTypeVoie = libelleFromTypeVoie(typeVoieEtablissement); + + return [ + wrapWord(complementAdresseEtablissement, ", ", true), + wrapWord(numeroVoieEtablissement), + wrapWord(indiceRepetitionEtablissement), + wrapWord(fullLibelleFromTypeVoie), + wrapWord(libelleVoieEtablissement, ", "), + wrapWord(distributionSpecialeEtablissement, ", "), + wrapWord(codePostalEtablissement || codeCedexEtablissement), + wrapWord( + libelleCommuneEtablissement || + libelleCedexEtablissement || + libelleCommuneEtrangerEtablissement, + "", + true, + ), + libellePaysEtrangerEtablissement + ? `, ${wrapWord(libellePaysEtrangerEtablissement, "", true)}` + : "", + ].join(""); +}; + +const libelleFromTypeVoie = (codeVoie: CodeVoie) => { + return codesVoies[codeVoie] || codeVoie; +}; + +const wrapWord = (word: string | null, punct = " ", caps = false) => { + if (!word) { + return ""; + } + if (caps) { + return capitalize(word) + punct; + } + return word.toString().toLowerCase() + punct; +}; diff --git a/packages/insee/src/formatters/enseigne.ts b/packages/insee/src/formatters/enseigne.ts new file mode 100644 index 000000000..3f8619523 --- /dev/null +++ b/packages/insee/src/formatters/enseigne.ts @@ -0,0 +1,8 @@ +// + +import { capitalize, isEmpty } from "lodash-es"; + +// + +export const formatEnseigne = (...args: (string | null)[]) => + capitalize(args.filter((e) => !isEmpty(e)).join(" ")) || ""; diff --git a/packages/insee/src/formatters/index.ts b/packages/insee/src/formatters/index.ts new file mode 100644 index 000000000..bc58830c2 --- /dev/null +++ b/packages/insee/src/formatters/index.ts @@ -0,0 +1,8 @@ +// + +export * from "./adresse-etablissement.js"; +export * from "./enseigne.js"; +export * from "./libelle-from-categories-juridiques.js"; +export * from "./libelle-from-code-effectif.js"; +export * from "./libelle-from-code-naf.js"; +export * from "./nom-complet.js"; diff --git a/packages/insee/src/formatters/libelle-from-categories-juridiques.ts b/packages/insee/src/formatters/libelle-from-categories-juridiques.ts new file mode 100644 index 000000000..10fec25c8 --- /dev/null +++ b/packages/insee/src/formatters/libelle-from-categories-juridiques.ts @@ -0,0 +1,9 @@ +// + +import { categoriesJuridiques, type CategoriesJuridique } from "#src/data"; + +// + +export const libelleFromCategoriesJuridiques = ( + categorie: CategoriesJuridique, +) => categoriesJuridiques[categorie] || null; diff --git a/packages/insee/src/formatters/libelle-from-code-effectif.ts b/packages/insee/src/formatters/libelle-from-code-effectif.ts new file mode 100644 index 000000000..b16a92f6f --- /dev/null +++ b/packages/insee/src/formatters/libelle-from-code-effectif.ts @@ -0,0 +1,25 @@ +// + +import { codesEffectifs } from "#src/data"; +import type { TrancheEffectifs } from "#src/types/tranche-effectifs.js"; + +// + +export const libelleFromCodeEffectif = ( + codeEffectif: NonNullable, + anneeEffectif: string, + characterEmployeurUniteLegale?: string, +) => { + const libelle = codesEffectifs[codeEffectif]; + + if (libelle && anneeEffectif) { + return `${libelle}, en ${anneeEffectif}`; + } + if (libelle) { + return libelle; + } + if (characterEmployeurUniteLegale === "N") { + return "Unité non employeuse"; + } + return null; +}; diff --git a/packages/insee/src/formatters/libelle-from-code-naf.ts b/packages/insee/src/formatters/libelle-from-code-naf.ts new file mode 100644 index 000000000..f445530ec --- /dev/null +++ b/packages/insee/src/formatters/libelle-from-code-naf.ts @@ -0,0 +1,10 @@ +// + +import { codesNaf, type CodeNaf } from "#src/data"; + +// + +export const libelleFromCodeNaf = (codeNaf: CodeNaf, addCode = true) => { + const label = codesNaf[codeNaf] || "Activité inconnue"; + return addCode && codeNaf ? `${codeNaf} - ${label}` : label; +}; diff --git a/packages/insee/src/formatters/nom-complet.ts b/packages/insee/src/formatters/nom-complet.ts new file mode 100644 index 000000000..8a0cc8e0d --- /dev/null +++ b/packages/insee/src/formatters/nom-complet.ts @@ -0,0 +1,47 @@ +// + +import { capitalize } from "lodash-es"; + +// + +type FormatNomCompletArgs = { + denominationUniteLegale: string; + prenomUsuelUniteLegale: string | null; + nomUniteLegale: string | null; + nomUsageUniteLegale: string | null; + sigleUniteLegale: string | null; +}; + +export const formatNomComplet = ({ + denominationUniteLegale, + prenomUsuelUniteLegale, + nomUniteLegale, + nomUsageUniteLegale, + sigleUniteLegale, +}: FormatNomCompletArgs) => { + const formattedFirstName = formatFirstNames([prenomUsuelUniteLegale ?? ""]); + const formattedName = formatNameFull( + nomUniteLegale ?? "", + nomUsageUniteLegale ?? "", + ); + return `${ + capitalize(denominationUniteLegale) || + [formattedFirstName, formattedName].filter((e) => !!e).join(" ") || + "Nom inconnu" + }${sigleUniteLegale ? ` (${sigleUniteLegale})` : ""}`; +}; + +const formatFirstNames = (firstNames: string[], nameCount = 0) => { + const formatted = firstNames.map(capitalize).filter((name) => !!name); + if (nameCount > 0 && nameCount < firstNames.length) { + return formatted.slice(0, nameCount).join(", "); + } + return formatted.join(", "); +}; + +const formatNameFull = (nomPatronymique = "", nomUsage = "") => { + if (nomUsage && nomPatronymique) { + return `${capitalize(nomUsage)} (${capitalize(nomPatronymique)})`; + } + return capitalize(nomUsage || nomPatronymique || ""); +}; diff --git a/packages/insee/src/types/establishment.ts b/packages/insee/src/types/establishment.ts new file mode 100644 index 000000000..0dd8575b2 --- /dev/null +++ b/packages/insee/src/types/establishment.ts @@ -0,0 +1,137 @@ +import type { CategoriesJuridique, CodeNaf, CodeVoie } from "#src/data"; +import type { TrancheEffectifs } from "./tranche-effectifs.js"; +export type InseeEtablissement = { + // ex: '217400563' + siren: string; + // ex: '00011' + nic: string; + // ex: '21740056300011' + siret: string; + // ex: 'O' + statutDiffusionEtablissement: "O" | "P" | "N"; + // ex: '1983-03-01' + dateCreationEtablissement: string; + // ex: '32' + trancheEffectifsEtablissement: TrancheEffectifs; + // ex: '2020' + anneeEffectifsEtablissement: string; + activitePrincipaleRegistreMetiersEtablissement: string | null; + // ex: '2022-08-29T09:08:45' + dateDernierTraitementEtablissement: string; + // ex: true + etablissementSiege: boolean; + // ex: 4 + nombrePeriodesEtablissement: number; + uniteLegale: { + // ex: 'A' + etatAdministratifUniteLegale: string; + // ex: 'O' + statutDiffusionUniteLegale: "O" | "P" | "N"; + // ex: '1982-01-01' + dateCreationUniteLegale: string; + // ex: '7210' + categorieJuridiqueUniteLegale: CategoriesJuridique; + // ex: 'COMMUNE DE CHAMONIX MONT BLANC' + denominationUniteLegale: string; + sigleUniteLegale: string | null; + denominationUsuelle1UniteLegale: string | null; + denominationUsuelle2UniteLegale: string | null; + denominationUsuelle3UniteLegale: string | null; + sexeUniteLegale: string | null; + nomUniteLegale: string | null; + nomUsageUniteLegale: string | null; + prenom1UniteLegale: string | null; + prenom2UniteLegale: string | null; + prenom3UniteLegale: string | null; + prenom4UniteLegale: string | null; + prenomUsuelUniteLegale: string | null; + pseudonymeUniteLegale: string | null; + // ex: '84.11Z' + activitePrincipaleUniteLegale: string; + nomenclatureActivitePrincipaleUniteLegale: "NAFRev2"; + identifiantAssociationUniteLegale: string | null; + // ex: 'N' + economieSocialeSolidaireUniteLegale: string; + // ex: 'N' + societeMissionUniteLegale: string; + // ex: 'O' + caractereEmployeurUniteLegale: string; + // ex: '32' + trancheEffectifsUniteLegale: TrancheEffectifs; + // ex: '2020' + anneeEffectifsUniteLegale: string; + // ex: '00011' + nicSiegeUniteLegale: string; + // ex: '2023-03-01T20:13:11' + dateDernierTraitementUniteLegale: string; + // ex: 'ETI' + categorieEntreprise: string; + // ex: '2020' + anneeCategorieEntreprise: string; + }; + adresseEtablissement: { + complementAdresseEtablissement: string | null; + // ex: '38' + numeroVoieEtablissement: string; + indiceRepetitionEtablissement: string | null; + // ex: 'PL' + typeVoieEtablissement: CodeVoie; + // ex: 'DE L EGLISE' + libelleVoieEtablissement: string; + // ex: '74400' + codePostalEtablissement: string; + // ex: 'CHAMONIX-MONT-BLANC' + libelleCommuneEtablissement: string; + libelleCommuneEtrangerEtablissement: string | null; + distributionSpecialeEtablissement: string | null; + // ex: '74056' + codeCommuneEtablissement: string; + codeCedexEtablissement: string | null; + libelleCedexEtablissement: string | null; + codePaysEtrangerEtablissement: string | null; + libellePaysEtrangerEtablissement: string | null; + }; + adresse2Etablissement: { + complementAdresse2Etablissement: null; + numeroVoie2Etablissement: null; + indiceRepetition2Etablissement: null; + typeVoie2Etablissement: null; + libelleVoie2Etablissement: null; + codePostal2Etablissement: null; + libelleCommune2Etablissement: null; + libelleCommuneEtranger2Etablissement: null; + distributionSpeciale2Etablissement: null; + codeCommune2Etablissement: null; + codeCedex2Etablissement: null; + libelleCedex2Etablissement: null; + codePaysEtranger2Etablissement: null; + libellePaysEtranger2Etablissement: null; + }; + periodesEtablissement: { + dateFin: string | null; + // ex: '2008-01-01' + dateDebut: string; + // ex: 'A' + etatAdministratifEtablissement: string; + // ex: false + changementEtatAdministratifEtablissement: boolean; + // ex: 'MAIRIE CHAMONIX - ARGENTIERE' + enseigne1Etablissement: string; + enseigne2Etablissement: null; + enseigne3Etablissement: null; + // ex: false + changementEnseigneEtablissement: boolean; + denominationUsuelleEtablissement: null; + // ex: false + changementDenominationUsuelleEtablissement: boolean; + // ex: '84.11Z' + activitePrincipaleEtablissement: CodeNaf; + nomenclatureActivitePrincipaleEtablissement: "NAFRev2"; + // ex: true + changementActivitePrincipaleEtablissement: boolean; + // ex: 'O' + caractereEmployeurEtablissement: string; + // ex: false + changementCaractereEmployeurEtablissement: boolean; + }[]; +}; diff --git a/packages/insee/src/types/index.ts b/packages/insee/src/types/index.ts new file mode 100644 index 000000000..5652f6ab1 --- /dev/null +++ b/packages/insee/src/types/index.ts @@ -0,0 +1,4 @@ +// + +export * from "./establishment.js"; +export * from "./tranche-effectifs.js"; diff --git a/packages/insee/src/types/tranche-effectifs.ts b/packages/insee/src/types/tranche-effectifs.ts new file mode 100644 index 000000000..5994996a0 --- /dev/null +++ b/packages/insee/src/types/tranche-effectifs.ts @@ -0,0 +1,36 @@ +// source : https://www.sirene.fr/sirene/public/variable/trancheEffectifsEtablissement +export type TrancheEffectifs = + // le champ peut être null bien que la documentation ne spécifie pas à quoi correspond ce cas + | null + // Etablissement non employeur (pas de salarié au cours de l'année de référence et pas d'effectif au 31/12)NN + | "NN" + // 0 salarié (n'ayant pas d'effectif au 31/12 mais ayant employé des salariés au cours de l'année de référence)'' + | "00" + // 1 ou 2 salariés + | "01" + // 3 à 5 salariés + | "02" + // 6 à 9 salariés + | "03" + // 10 à 19 salariés + | "11" + // 20 à 49 salariés + | "12" + // 50 à 99 salariés + | "21" + // 100 à 199 salariés + | "22" + // 200 à 249 salariés + | "31" + // 250 à 499 salariés + | "32" + // 500 à 999 salariés + | "41" + // 1 000 à 1 999 salariés + | "42" + // 2 000 à 4 999 salariés + | "51" + // 5 000 à 9 999 salariés + | "52" + // 10 000 salariés et plus + | "53"; 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 ab284b796..ab671286a 100644 --- a/scripts/import-accounts-coop.ts +++ b/scripts/import-accounts-coop.ts @@ -10,10 +10,7 @@ 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"; +import { getOrganizationInfo } from "../src/connectors/api-sirene"; import { findByUserId } from "../src/repositories/organization/getters"; import { linkUserToOrganization, @@ -45,8 +42,6 @@ const rateInMsFromArgs = toInteger(process.argv[2]); const maxInseeCallRateInMs = rateInMsFromArgs !== 0 ? rateInMsFromArgs : 125; (async () => { - const access_token = await getInseeAccessToken(); - const readStream = fs.createReadStream(INPUT_FILE); // readStream is a read-only stream wit raw text content of the CSV file const writeStream = fs.createWriteStream(OUTPUT_FILE); // writeStream is a write-only stream to write on the disk @@ -175,10 +170,7 @@ const maxInseeCallRateInMs = rateInMsFromArgs !== 0 ? rateInMsFromArgs : 125; // 2. get organizationInfo try { - const organizationInfo = await getOrganizationInfo( - siret, - access_token, - ); + const organizationInfo = await getOrganizationInfo(siret); if (!isOrganizationInfo(organizationInfo)) { throw Error("empty organization info"); } diff --git a/scripts/import-accounts.ts b/scripts/import-accounts.ts index ebb913b17..6146b9e90 100644 --- a/scripts/import-accounts.ts +++ b/scripts/import-accounts.ts @@ -9,10 +9,7 @@ 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"; +import { getOrganizationInfo } from "../src/connectors/api-sirene"; import { findByUserId } from "../src/repositories/organization/getters"; import { linkUserToOrganization, @@ -44,8 +41,6 @@ const rateInMsFromArgs = toInteger(process.argv[2]); const maxInseeCallRateInMs = rateInMsFromArgs !== 0 ? rateInMsFromArgs : 125; (async () => { - const access_token = await getInseeAccessToken(); - const readStream = fs.createReadStream(INPUT_FILE); // readStream is a read-only stream wit raw text content of the CSV file const writeStream = fs.createWriteStream(OUTPUT_FILE); // writeStream is a write-only stream to write on the disk @@ -165,10 +160,7 @@ const maxInseeCallRateInMs = rateInMsFromArgs !== 0 ? rateInMsFromArgs : 125; for (let siret of sirets) { // 2. get organizationInfo try { - const organizationInfo = await getOrganizationInfo( - siret, - access_token, - ); + const organizationInfo = await getOrganizationInfo(siret); if (!isOrganizationInfo(organizationInfo)) { throw Error("empty organization info"); } diff --git a/scripts/import-domains.ts b/scripts/import-domains.ts index e967b79f4..9094eca03 100644 --- a/scripts/import-domains.ts +++ b/scripts/import-domains.ts @@ -9,10 +9,7 @@ import fs from "fs"; import { isEmpty, some, toInteger } from "lodash-es"; import { z } from "zod"; import { InseeNotFoundError } from "../src/config/errors"; -import { - getInseeAccessToken, - getOrganizationInfo, -} from "../src/connectors/api-sirene"; +import { getOrganizationInfo } from "../src/connectors/api-sirene"; import { addDomain, findEmailDomainsByOrganizationId, @@ -27,6 +24,7 @@ import { startDurationMesure, throttleApiCall, } from "../src/services/script-helpers"; +import type { Organization } from "../src/types/organization"; const { INPUT_FILE, OUTPUT_FILE } = z .object({ @@ -44,8 +42,6 @@ const rateInMsFromArgs = toInteger(process.argv[2]); const maxInseeCallRateInMs = rateInMsFromArgs !== 0 ? rateInMsFromArgs : 125; (async () => { - const access_token = await getInseeAccessToken(); - const readStream = fs.createReadStream(INPUT_FILE); // readStream is a read-only stream wit raw text content of the CSV file const writeStream = fs.createWriteStream(OUTPUT_FILE); // writeStream is a write-only stream to write on the disk @@ -131,10 +127,7 @@ const maxInseeCallRateInMs = rateInMsFromArgs !== 0 ? rateInMsFromArgs : 125; const start = startDurationMesure(); try { // 2. get organizationInfo - const organizationInfo = await getOrganizationInfo( - siret, - access_token, - ); + const organizationInfo = await getOrganizationInfo(siret); await throttleApiCall(start, maxInseeCallRateInMs); // 3. check organization status diff --git a/src/connectors/api-sirene/formatters.js b/src/connectors/api-sirene/formatters.js deleted file mode 100644 index b10cec8a8..000000000 --- a/src/connectors/api-sirene/formatters.js +++ /dev/null @@ -1,134 +0,0 @@ -import { capitalize, isEmpty } from "lodash-es"; -import { categoriesJuridiques } from "./categories-juridiques"; -import { codesEffectifs } from "./codes-effectifs"; -import { codesNaf } from "./codes-naf"; -import { codesVoies } from "./codes-voie"; - -export const formatEnseigne = (...args) => - capitalize(args.filter((e) => !isEmpty(e)).join(" ")) || ""; - -export const formatNomComplet = ({ - denominationUniteLegale, - prenomUsuelUniteLegale, - nomUniteLegale, - nomUsageUniteLegale, - sigleUniteLegale, -}) => { - const formattedFirstName = formatFirstNames([prenomUsuelUniteLegale]); - const formattedName = formatNameFull(nomUniteLegale, nomUsageUniteLegale); - return `${ - capitalize(denominationUniteLegale) || - [formattedFirstName, formattedName].filter((e) => !!e).join(" ") || - "Nom inconnu" - }${sigleUniteLegale ? ` (${sigleUniteLegale})` : ""}`; -}; - -export const formatNameFull = (nomPatronymique = "", nomUsage = "") => { - if (nomUsage && nomPatronymique) { - return `${capitalize(nomUsage)} (${capitalize(nomPatronymique)})`; - } - return capitalize(nomUsage || nomPatronymique || ""); -}; - -export const formatFirstNames = (firstNames, nameCount = 0) => { - const formatted = firstNames.map(capitalize).filter((name) => !!name); - if (nameCount > 0 && nameCount < firstNames.length) { - return formatted.slice(0, nameCount).join(", "); - } - return formatted.join(", "); -}; - -const wrapWord = (word, punct = " ", caps = false) => { - if (!word) { - return ""; - } - if (caps) { - return capitalize(word) + punct; - } - return word.toString().toLowerCase() + punct; -}; - -const libelleFromTypeVoie = (codeVoie) => { - return codesVoies[codeVoie] || codeVoie; -}; - -export const formatAdresseEtablissement = ({ - complementAdresseEtablissement, - numeroVoieEtablissement, - indiceRepetitionEtablissement, - typeVoieEtablissement, - libelleVoieEtablissement, - distributionSpecialeEtablissement, - codePostalEtablissement, - libelleCommuneEtablissement, - codeCedexEtablissement, - libelleCedexEtablissement, - libelleCommuneEtrangerEtablissement, - codePaysEtrangerEtablissement, - libellePaysEtrangerEtablissement, -}) => { - if ( - !complementAdresseEtablissement && - !numeroVoieEtablissement && - !typeVoieEtablissement && - !libelleCommuneEtablissement && - !distributionSpecialeEtablissement && - !codePostalEtablissement && - !codeCedexEtablissement && - !libelleVoieEtablissement && - !libelleCommuneEtrangerEtablissement && - !codePaysEtrangerEtablissement && - !libellePaysEtrangerEtablissement - ) { - return ""; - } - - const fullLibelleFromTypeVoie = libelleFromTypeVoie(typeVoieEtablissement); - - return [ - wrapWord(complementAdresseEtablissement, ", ", true), - wrapWord(numeroVoieEtablissement), - wrapWord(indiceRepetitionEtablissement), - wrapWord(fullLibelleFromTypeVoie), - wrapWord(libelleVoieEtablissement, ", "), - wrapWord(distributionSpecialeEtablissement, ", "), - wrapWord(codePostalEtablissement || codeCedexEtablissement), - wrapWord( - libelleCommuneEtablissement || - libelleCedexEtablissement || - libelleCommuneEtrangerEtablissement, - "", - true, - ), - libellePaysEtrangerEtablissement - ? `, ${wrapWord(libellePaysEtrangerEtablissement, "", true)}` - : "", - ].join(""); -}; - -export const libelleFromCodeNaf = (codeNaf = "", addCode = true) => { - const label = codesNaf[codeNaf] || "Activité inconnue"; - return addCode && codeNaf ? `${codeNaf} - ${label}` : label; -}; - -export const libelleFromCategoriesJuridiques = (categorie) => - categoriesJuridiques[categorie] || null; - -export const libelleFromCodeEffectif = ( - codeEffectif, - anneeEffectif, - characterEmployeurUniteLegale, -) => { - const libelle = codesEffectifs[codeEffectif]; - - if (libelle && anneeEffectif) { - return `${libelle}, en ${anneeEffectif}`; - } - if (libelle) { - return libelle; - } - if (characterEmployeurUniteLegale === "N") { - return "Unité non employeuse"; - } - return null; -}; diff --git a/src/connectors/api-sirene/index.ts b/src/connectors/api-sirene/index.ts index a2539e058..a24fe8973 100644 --- a/src/connectors/api-sirene/index.ts +++ b/src/connectors/api-sirene/index.ts @@ -1,4 +1,18 @@ -import axios, { AxiosError, type AxiosResponse } from "axios"; +import { + findBySirenFactory, + findBySiretFactory, + getInseeAccessTokenFactory, +} from "@gouvfr-lasuite/proconnect.insee/api"; +import { + formatAdresseEtablissement, + formatEnseigne, + formatNomComplet, + libelleFromCategoriesJuridiques, + libelleFromCodeEffectif, + libelleFromCodeNaf, +} from "@gouvfr-lasuite/proconnect.insee/formatters"; +import type { InseeEtablissement } from "@gouvfr-lasuite/proconnect.insee/types"; +import { AxiosError } from "axios"; import { cloneDeep, set } from "lodash-es"; import { HTTP_CLIENT_TIMEOUT, @@ -10,174 +24,11 @@ import { InseeNotFoundError, InvalidSiretError, } from "../../config/errors"; -import { - formatAdresseEtablissement, - formatEnseigne, - formatNomComplet, - libelleFromCategoriesJuridiques, - libelleFromCodeEffectif, - libelleFromCodeNaf, -} from "./formatters"; - -type InseeEtablissement = { - // ex: '217400563' - siren: string; - // ex: '00011' - nic: string; - // ex: '21740056300011' - siret: string; - // ex: 'O' - statutDiffusionEtablissement: "O" | "P" | "N"; - // ex: '1983-03-01' - dateCreationEtablissement: string; - // ex: '32' - trancheEffectifsEtablissement: TrancheEffectifs; - // ex: '2020' - anneeEffectifsEtablissement: string; - activitePrincipaleRegistreMetiersEtablissement: string | null; - // ex: '2022-08-29T09:08:45' - dateDernierTraitementEtablissement: string; - // ex: true - etablissementSiege: boolean; - // ex: 4 - nombrePeriodesEtablissement: number; - uniteLegale: { - // ex: 'A' - etatAdministratifUniteLegale: string; - // ex: 'O' - statutDiffusionUniteLegale: "O" | "P" | "N"; - // ex: '1982-01-01' - dateCreationUniteLegale: string; - // ex: '7210' - categorieJuridiqueUniteLegale: string; - // ex: 'COMMUNE DE CHAMONIX MONT BLANC' - denominationUniteLegale: string; - sigleUniteLegale: string | null; - denominationUsuelle1UniteLegale: string | null; - denominationUsuelle2UniteLegale: string | null; - denominationUsuelle3UniteLegale: string | null; - sexeUniteLegale: string | null; - nomUniteLegale: string | null; - nomUsageUniteLegale: string | null; - prenom1UniteLegale: string | null; - prenom2UniteLegale: string | null; - prenom3UniteLegale: string | null; - prenom4UniteLegale: string | null; - prenomUsuelUniteLegale: string | null; - pseudonymeUniteLegale: string | null; - // ex: '84.11Z' - activitePrincipaleUniteLegale: string; - nomenclatureActivitePrincipaleUniteLegale: "NAFRev2"; - identifiantAssociationUniteLegale: string | null; - // ex: 'N' - economieSocialeSolidaireUniteLegale: string; - // ex: 'N' - societeMissionUniteLegale: string; - // ex: 'O' - caractereEmployeurUniteLegale: string; - // ex: '32' - trancheEffectifsUniteLegale: TrancheEffectifs; - // ex: '2020' - anneeEffectifsUniteLegale: string; - // ex: '00011' - nicSiegeUniteLegale: string; - // ex: '2023-03-01T20:13:11' - dateDernierTraitementUniteLegale: string; - // ex: 'ETI' - categorieEntreprise: string; - // ex: '2020' - anneeCategorieEntreprise: string; - }; - adresseEtablissement: { - complementAdresseEtablissement: string | null; - // ex: '38' - numeroVoieEtablissement: string; - indiceRepetitionEtablissement: string | null; - // ex: 'PL' - typeVoieEtablissement: string; - // ex: 'DE L EGLISE' - libelleVoieEtablissement: string; - // ex: '74400' - codePostalEtablissement: string; - // ex: 'CHAMONIX-MONT-BLANC' - libelleCommuneEtablissement: string; - libelleCommuneEtrangerEtablissement: string | null; - distributionSpecialeEtablissement: string | null; - // ex: '74056' - codeCommuneEtablissement: string; - codeCedexEtablissement: string | null; - libelleCedexEtablissement: string | null; - codePaysEtrangerEtablissement: string | null; - libellePaysEtrangerEtablissement: string | null; - }; - adresse2Etablissement: { - complementAdresse2Etablissement: null; - numeroVoie2Etablissement: null; - indiceRepetition2Etablissement: null; - typeVoie2Etablissement: null; - libelleVoie2Etablissement: null; - codePostal2Etablissement: null; - libelleCommune2Etablissement: null; - libelleCommuneEtranger2Etablissement: null; - distributionSpeciale2Etablissement: null; - codeCommune2Etablissement: null; - codeCedex2Etablissement: null; - libelleCedex2Etablissement: null; - codePaysEtranger2Etablissement: null; - libellePaysEtranger2Etablissement: null; - }; - periodesEtablissement: { - dateFin: string | null; - // ex: '2008-01-01' - dateDebut: string; - // ex: 'A' - etatAdministratifEtablissement: string; - // ex: false - changementEtatAdministratifEtablissement: boolean; - // ex: 'MAIRIE CHAMONIX - ARGENTIERE' - enseigne1Etablissement: string; - enseigne2Etablissement: null; - enseigne3Etablissement: null; - // ex: false - changementEnseigneEtablissement: boolean; - denominationUsuelleEtablissement: null; - // ex: false - changementDenominationUsuelleEtablissement: boolean; - // ex: '84.11Z' - activitePrincipaleEtablissement: string; - nomenclatureActivitePrincipaleEtablissement: "NAFRev2"; - // ex: true - changementActivitePrincipaleEtablissement: boolean; - // ex: 'O' - caractereEmployeurEtablissement: string; - // ex: false - changementCaractereEmployeurEtablissement: boolean; - }[]; -}; - -type EtablissementSearchBySiretResponse = { - etablissement: InseeEtablissement; -}; - -type EtablissementSearchResponse = { - header: { - total: number; - debut: number; - nombre: number; - }; - etablissements: InseeEtablissement[]; -}; - -type GetTokenReponse = { - access_token: string; - scope: "am_application_scope default"; - token_type: "Bearer"; - expires_in: number; -}; +import type { OrganizationInfo } from "../../types/organization-info"; const hideNonDiffusibleData = ( - etablissement: EtablissementSearchBySiretResponse["etablissement"], -): EtablissementSearchBySiretResponse["etablissement"] => { + etablissement: InseeEtablissement, +): InseeEtablissement => { const hiddenEtablissement = cloneDeep(etablissement); set(hiddenEtablissement, "uniteLegale.denominationUniteLegale", null); set(hiddenEtablissement, "uniteLegale.sigleUniteLegale", null); @@ -264,60 +115,40 @@ 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, - }, - ); +export const getInseeAccessToken = getInseeAccessTokenFactory( + { + consumerKey: INSEE_CONSUMER_KEY, + consumerSecret: INSEE_CONSUMER_SECRET, + }, + { + timeout: HTTP_CLIENT_TIMEOUT, + }, +); - return access_token; -}; +const findBySiret = findBySiretFactory({ + getInseeAccessToken, + config: { + timeout: HTTP_CLIENT_TIMEOUT, + }, +}); + +const findBySiren = findBySirenFactory({ + getInseeAccessToken, + config: { + timeout: HTTP_CLIENT_TIMEOUT, + }, +}); export const getOrganizationInfo = async ( siretOrSiren: string, - provided_access_token?: string, ): Promise => { try { - let access_token = provided_access_token; - if (!access_token) { - access_token = await getInseeAccessToken(); - } - let etablissement: InseeEtablissement; if (siretOrSiren.match(/^\d{14}$/)) { - let { data }: AxiosResponse = - await axios.get( - `https://api.insee.fr/entreprises/sirene/siret/${siretOrSiren}`, - { - headers: { Authorization: `Bearer ${access_token}` }, - timeout: HTTP_CLIENT_TIMEOUT, - }, - ); - - etablissement = data.etablissement; + etablissement = await findBySiret(siretOrSiren); } else if (siretOrSiren.match(/^\d{9}$/)) { - // siretOrSiren is a siren, we fetch the data of the siege social - let { data }: AxiosResponse = - await axios.get( - `https://api.insee.fr/entreprises/sirene/siret?q=siren:${siretOrSiren} AND etablissementSiege:true`, - { - headers: { Authorization: `Bearer ${access_token}` }, - timeout: HTTP_CLIENT_TIMEOUT, - }, - ); - - etablissement = data.etablissements[0]; + etablissement = await findBySiren(siretOrSiren); } else { throw new InvalidSiretError(); } @@ -388,10 +219,11 @@ export const getOrganizationInfo = async ( enseigne, trancheEffectifs: trancheEffectifsEtablissement, trancheEffectifsUniteLegale, - libelleTrancheEffectif: libelleFromCodeEffectif( - trancheEffectifsEtablissement, - anneeEffectifsEtablissement, - ), + libelleTrancheEffectif: + libelleFromCodeEffectif( + trancheEffectifsEtablissement, + anneeEffectifsEtablissement, + ) ?? "", etatAdministratif: etatAdministratifEtablissement, estActive: etatAdministratifEtablissement === "A", statutDiffusion: statutDiffusionEtablissement, @@ -403,10 +235,9 @@ export const getOrganizationInfo = async ( libelleActivitePrincipale: libelleFromCodeNaf( activitePrincipaleEtablissement, ), - categorieJuridique: categorieJuridiqueUniteLegale, - libelleCategorieJuridique: libelleFromCategoriesJuridiques( - categorieJuridiqueUniteLegale, - ), + categorieJuridique: String(categorieJuridiqueUniteLegale), + libelleCategorieJuridique: + libelleFromCategoriesJuridiques(categorieJuridiqueUniteLegale) ?? "", }; } catch (e) { if ( diff --git a/src/managers/organization/join.ts b/src/managers/organization/join.ts index 6a41394f8..9d2857c3b 100644 --- a/src/managers/organization/join.ts +++ b/src/managers/organization/join.ts @@ -54,6 +54,8 @@ import { isEtablissementScolaireDuPremierEtSecondDegre, isSmallAssociation, } from "../../services/organization"; +import type { Organization } from "../../types/organization"; +import type { OrganizationInfo } from "../../types/organization-info"; import { unableToAutoJoinOrganizationMd } from "../../views/mails/unable-to-auto-join-organization"; import { getOrganizationsByUserId, markDomainAsVerified } from "./main"; diff --git a/src/managers/organization/main.ts b/src/managers/organization/main.ts index a3ae80dae..4082d0532 100644 --- a/src/managers/organization/main.ts +++ b/src/managers/organization/main.ts @@ -16,6 +16,7 @@ import { } from "../../repositories/organization/setters"; import { setSelectedOrganizationId } from "../../repositories/redis/selected-organization"; import { getEmailDomain } from "../../services/email"; +import type { Organization } from "../../types/organization"; export const getOrganizationsByUserId = findByUserId; export const getOrganizationById = findOrganizationById; diff --git a/src/repositories/organization/getters.ts b/src/repositories/organization/getters.ts index e7a9abb3f..1107cf957 100644 --- a/src/repositories/organization/getters.ts +++ b/src/repositories/organization/getters.ts @@ -1,5 +1,6 @@ import type { QueryResult } from "pg"; import { getDatabaseConnection } from "../../connectors/postgres"; +import type { Organization } from "../../types/organization"; export const findById = async (id: number) => { const connection = getDatabaseConnection(); diff --git a/src/repositories/organization/setters.ts b/src/repositories/organization/setters.ts index 9c5daf974..3a0adfe2f 100644 --- a/src/repositories/organization/setters.ts +++ b/src/repositories/organization/setters.ts @@ -1,6 +1,8 @@ import type { QueryResult } from "pg"; import { getDatabaseConnection } from "../../connectors/postgres"; import { hashToPostgresParams } from "../../services/hash-to-postgres-params"; +import type { Organization } from "../../types/organization"; +import type { OrganizationInfo } from "../../types/organization-info"; export const upsert = async ({ siret, diff --git a/src/services/organization.ts b/src/services/organization.ts index 668ecacb8..db2aaccd1 100644 --- a/src/services/organization.ts +++ b/src/services/organization.ts @@ -1,4 +1,5 @@ import { isDomainValid } from "@gouvfr-lasuite/proconnect.core/security"; +import type { Organization } from "../types/organization"; /** * These fonctions return approximate results. As the data tranche effectifs is diff --git a/src/services/script-helpers.ts b/src/services/script-helpers.ts index a532b106a..9a954cc4c 100644 --- a/src/services/script-helpers.ts +++ b/src/services/script-helpers.ts @@ -1,6 +1,7 @@ // from https://ipirozhenko.com/blog/measuring-requests-duration-nodejs-express/ import fs from "fs"; import { isEmpty } from "lodash-es"; +import type { OrganizationInfo } from "../types/organization-info"; export const startDurationMesure = () => { return process.hrtime(); diff --git a/src/types/organization-info.d.ts b/src/types/organization-info.d.ts index 83eadaa57..7745efae0 100644 --- a/src/types/organization-info.d.ts +++ b/src/types/organization-info.d.ts @@ -1,39 +1,6 @@ // source : https://www.sirene.fr/sirene/public/variable/trancheEffectifsEtablissement -type TrancheEffectifs = - // le champ peut être null bien que la documentation ne spécifie pas à quoi correspond ce cas - | null - // Etablissement non employeur (pas de salarié au cours de l'année de référence et pas d'effectif au 31/12)NN - | "NN" - // 0 salarié (n'ayant pas d'effectif au 31/12 mais ayant employé des salariés au cours de l'année de référence)'' - | "00" - // 1 ou 2 salariés - | "01" - // 3 à 5 salariés - | "02" - // 6 à 9 salariés - | "03" - // 10 à 19 salariés - | "11" - // 20 à 49 salariés - | "12" - // 50 à 99 salariés - | "21" - // 100 à 199 salariés - | "22" - // 200 à 249 salariés - | "31" - // 250 à 499 salariés - | "32" - // 500 à 999 salariés - | "41" - // 1 000 à 1 999 salariés - | "42" - // 2 000 à 4 999 salariés - | "51" - // 5 000 à 9 999 salariés - | "52" - // 10 000 salariés et plus - | "53"; + +import { TrancheEffectifs } from "@gouvfr-lasuite/proconnect.insee/types"; interface OrganizationInfo { siret: string; diff --git a/src/types/organization.d.ts b/src/types/organization.d.ts index 2f779a1fb..a966b5db8 100644 --- a/src/types/organization.d.ts +++ b/src/types/organization.d.ts @@ -1,3 +1,5 @@ +import { TrancheEffectifs } from "@gouvfr-lasuite/proconnect.insee/types"; + interface Organization { id: number; siret: string; diff --git a/test/api-sirene.test.ts b/test/api-sirene.test.ts index 4189e18b4..1eb2b83e8 100644 --- a/test/api-sirene.test.ts +++ b/test/api-sirene.test.ts @@ -75,7 +75,7 @@ describe("getOrganizationInfo", () => { enseigne: "", trancheEffectifs: null, trancheEffectifsUniteLegale: null, - libelleTrancheEffectif: null, + libelleTrancheEffectif: "", etatAdministratif: "A", estActive: true, statutDiffusion: "P", diff --git a/test/organization.test.ts b/test/organization.test.ts index 67eb9f157..596e54822 100644 --- a/test/organization.test.ts +++ b/test/organization.test.ts @@ -8,6 +8,7 @@ import { isSmallAssociation, isWasteManagementOrganization, } from "../src/services/organization"; +import type { Organization } from "../src/types/organization"; const association_org_info = { siret: "83511518900010", From 7e528cb9bfdcebad52a10397f0cce94682de7f32 Mon Sep 17 00:00:00 2001 From: Douglas Duteil Date: Tue, 7 Jan 2025 15:51:16 +0100 Subject: [PATCH 30/38] refactor(insee): extract insee connector to individual pkg (#899) --- Dockerfile | 2 + package-lock.json | 62 ++++- package.json | 7 +- packages/core/package.json | 2 +- packages/email/package.json | 2 +- packages/identite/package.json | 69 +++++ .../organization/__mocks__}/diffusible.json | 0 .../__mocks__}/partially-non-diffusible.json | 0 .../__mocks__}/search-by-siren.json | 0 .../get-organization-info.test.ts | 56 ++-- .../src/organization/get-organization-info.ts | 241 ++++++++++++++++ packages/identite/src/organization/index.ts | 4 + packages/identite/src/organization/upsert.ts | 140 ++++++++++ .../identite/src/organization/upset.test.ts | 54 ++++ .../src/services/hash-to-postgres-params.ts | 34 +++ packages/identite/src/services/index.ts | 3 + packages/identite/src/types/contexts.ts | 9 + packages/identite/src/types/index.ts | 6 + .../identite/src/types/organization-info.ts | 6 +- .../identite/src/types/organization.ts | 8 +- .../identite/src/types/user.ts | 40 +-- packages/identite/src/user/create.test.ts | 31 +++ packages/identite/src/user/create.ts | 42 +++ .../identite/src/user/find-by-email.test.ts | 45 +++ packages/identite/src/user/find-by-email.ts | 20 ++ packages/identite/src/user/index.ts | 5 + packages/identite/src/user/update.test.ts | 36 +++ packages/identite/src/user/update.ts | 29 ++ packages/identite/tsconfig.json | 24 ++ packages/identite/tsconfig.lib.json | 9 + packages/insee/package.json | 2 +- packages/insee/src/data/codes-effectifs.ts | 6 +- scripts/import-accounts-coop.ts | 18 +- scripts/import-domains.ts | 2 +- src/connectors/api-sirene.ts | 44 +++ src/connectors/api-sirene/index.ts | 262 ------------------ src/managers/moderation.ts | 1 + src/managers/organization/join.ts | 6 +- src/managers/organization/main.ts | 2 +- src/managers/session/authenticated.ts | 1 + src/managers/user.ts | 1 + src/repositories/organization/getters.ts | 5 +- src/repositories/organization/setters.ts | 134 +-------- src/repositories/user.ts | 80 +----- src/services/organization.ts | 2 +- src/services/script-helpers.ts | 2 +- test/organization.test.ts | 2 +- tsconfig.json | 6 +- 48 files changed, 1023 insertions(+), 539 deletions(-) create mode 100644 packages/identite/package.json rename {test/api-sirene-data => packages/identite/src/organization/__mocks__}/diffusible.json (100%) rename {test/api-sirene-data => packages/identite/src/organization/__mocks__}/partially-non-diffusible.json (100%) rename {test/api-sirene-data => packages/identite/src/organization/__mocks__}/search-by-siren.json (100%) rename test/api-sirene.test.ts => packages/identite/src/organization/get-organization-info.test.ts (64%) create mode 100644 packages/identite/src/organization/get-organization-info.ts create mode 100644 packages/identite/src/organization/index.ts create mode 100644 packages/identite/src/organization/upsert.ts create mode 100644 packages/identite/src/organization/upset.test.ts create mode 100644 packages/identite/src/services/hash-to-postgres-params.ts create mode 100644 packages/identite/src/services/index.ts create mode 100644 packages/identite/src/types/contexts.ts create mode 100644 packages/identite/src/types/index.ts rename src/types/organization-info.d.ts => packages/identite/src/types/organization-info.ts (83%) rename src/types/organization.d.ts => packages/identite/src/types/organization.ts (87%) rename src/types/user.d.ts => packages/identite/src/types/user.ts (96%) create mode 100644 packages/identite/src/user/create.test.ts create mode 100644 packages/identite/src/user/create.ts 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/src/user/update.test.ts create mode 100644 packages/identite/src/user/update.ts create mode 100644 packages/identite/tsconfig.json create mode 100644 packages/identite/tsconfig.lib.json create mode 100644 src/connectors/api-sirene.ts delete mode 100644 src/connectors/api-sirene/index.ts diff --git a/Dockerfile b/Dockerfile index de2ffb24f..b77e295df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=package-lock.json,target=package-lock.json \ --mount=type=bind,source=packages/core/package.json,target=packages/core/package.json \ --mount=type=bind,source=packages/email/package.json,target=packages/email/package.json \ + --mount=type=bind,source=packages/identite/package.json,target=packages/identite/package.json \ --mount=type=bind,source=packages/insee/package.json,target=packages/insee/package.json \ --mount=type=cache,target=/root/.npm \ npm ci --omit=dev @@ -17,6 +18,7 @@ RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=package-lock.json,target=package-lock.json \ --mount=type=bind,source=packages/core/package.json,target=packages/core/package.json \ --mount=type=bind,source=packages/email/package.json,target=packages/email/package.json \ + --mount=type=bind,source=packages/identite/package.json,target=packages/identite/package.json \ --mount=type=bind,source=packages/insee/package.json,target=packages/insee/package.json \ --mount=type=cache,target=/root/.npm \ npm ci diff --git a/package-lock.json b/package-lock.json index b9d2d815f..00a2a3fc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@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", @@ -876,6 +877,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", @@ -1287,6 +1295,10 @@ "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 @@ -9153,6 +9165,15 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/sql-template-tag": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/sql-template-tag/-/sql-template-tag-5.2.1.tgz", + "integrity": "sha512-lFdvXCOqWhV40A7w4oQVDyuaNFb5yO+dhsHStZzOdtDJWCBWYv4+hhATK5nPpY5v/T1OMVcLMPeN4519qIyb9Q==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -10405,6 +10426,7 @@ "version": "0.2.0", "license": "MIT", "dependencies": { + "@types/lodash-es": "^4.17.12", "@zootools/email-spell-checker": "^1.12.0", "bcryptjs": "^2.4.3", "is-disposable-email-domain": "^1.0.7", @@ -10456,15 +10478,43 @@ "vite": "^5.4.8" } }, - "packages/free-email": { - "name": "@gouvfr-lasuite/moncomptepro.free-email", - "version": "0.0.0", - "extraneous": true, + "packages/identite": { + "name": "@gouvfr-lasuite/proconnect.identite", + "version": "0.2.0", + "license": "MIT", "dependencies": { - "is-disposable-email-domain": "^1.0.7" + "sql-template-tag": "^5.2.1" }, "devDependencies": { - "@tsconfig/node22": "^22.0.0" + "@electric-sql/pglite": "^0.2.15", + "@gouvfr-lasuite/proconnect.core": "^0.2.0", + "@gouvfr-lasuite/proconnect.insee": "^0.2.0", + "@tsconfig/node22": "^22.0.0", + "@types/mocha": "^10.0.10", + "@types/node": "^22.10.2", + "await-to-js": "^3.0.0", + "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": { diff --git a/package.json b/package.json index fdbd26a21..d36addbed 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,10 @@ }, "main": "src/index.js", "workspaces": [ - "packages/*" + "packages/core", + "packages/email", + "packages/insee", + "packages/identite" ], "scripts": { "build": "run-s build:**", @@ -39,6 +42,7 @@ "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:identite": "npm run dev --if-present --workspace=@gouvfr-lasuite/proconnect.identite", "watch:workspaces:insee": "npm run dev --if-present --workspace=@gouvfr-lasuite/proconnect.insee" }, "prettier": { @@ -52,6 +56,7 @@ "@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", diff --git a/packages/core/package.json b/packages/core/package.json index fe191c1a2..85028bf7f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -32,7 +32,7 @@ } }, "scripts": { - "build": "tsc --project tsconfig.lib.json", + "build": "tsc --build tsconfig.lib.json", "check": "npm run build -- --noEmit", "dev": "npm run build -- --watch --preserveWatchOutput", "test": "mocha" diff --git a/packages/email/package.json b/packages/email/package.json index f44656d53..9356c3190 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -27,7 +27,7 @@ } }, "scripts": { - "build": "tsc --project ./tsconfig.lib.json", + "build": "tsc --build ./tsconfig.lib.json", "dev": "npm run build -- --watch --preserveWatchOutput", "storybook": "vite", "test": "tsc --noEmit" diff --git a/packages/identite/package.json b/packages/identite/package.json new file mode 100644 index 000000000..75465a207 --- /dev/null +++ b/packages/identite/package.json @@ -0,0 +1,69 @@ +{ + "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": "./dist/*/index.d.ts", + "default": "./dist/*/index.js" + } + }, + "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 --build tsconfig.lib.json", + "clean": "rm -rf dist tsconfig*.tsbuildinfo", + "check": "npm run build -- --noEmit", + "dev": "npm run build -- --watch --preserveWatchOutput", + "test": "mocha" + }, + "mocha": { + "reporter": "spec", + "require": [ + "tsx" + ], + "spec": "src/**/*.test.ts" + }, + "dependencies": { + "sql-template-tag": "^5.2.1" + }, + "devDependencies": { + "@electric-sql/pglite": "^0.2.15", + "@gouvfr-lasuite/proconnect.core": "^0.2.0", + "@gouvfr-lasuite/proconnect.insee": "^0.2.0", + "@tsconfig/node22": "^22.0.0", + "@types/mocha": "^10.0.10", + "@types/node": "^22.10.2", + "await-to-js": "^3.0.0", + "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/test/api-sirene-data/diffusible.json b/packages/identite/src/organization/__mocks__/diffusible.json similarity index 100% rename from test/api-sirene-data/diffusible.json rename to packages/identite/src/organization/__mocks__/diffusible.json diff --git a/test/api-sirene-data/partially-non-diffusible.json b/packages/identite/src/organization/__mocks__/partially-non-diffusible.json similarity index 100% rename from test/api-sirene-data/partially-non-diffusible.json rename to packages/identite/src/organization/__mocks__/partially-non-diffusible.json diff --git a/test/api-sirene-data/search-by-siren.json b/packages/identite/src/organization/__mocks__/search-by-siren.json similarity index 100% rename from test/api-sirene-data/search-by-siren.json rename to packages/identite/src/organization/__mocks__/search-by-siren.json diff --git a/test/api-sirene.test.ts b/packages/identite/src/organization/get-organization-info.test.ts similarity index 64% rename from test/api-sirene.test.ts rename to packages/identite/src/organization/get-organization-info.test.ts index 1eb2b83e8..2f24b0111 100644 --- a/test/api-sirene.test.ts +++ b/packages/identite/src/organization/get-organization-info.test.ts @@ -1,11 +1,12 @@ +import { InseeNotFoundError } from "@gouvfr-lasuite/proconnect.insee/errors"; +import type { InseeEtablissement } from "@gouvfr-lasuite/proconnect.insee/types"; import * as chai from "chai"; import chaiAsPromised from "chai-as-promised"; import nock from "nock"; -import { InseeNotFoundError } from "../src/config/errors"; -import { getOrganizationInfo } from "../src/connectors/api-sirene"; -import diffusible from "./api-sirene-data/diffusible.json"; -import partiallyNonDiffusible from "./api-sirene-data/partially-non-diffusible.json"; -import searchBySiren from "./api-sirene-data/search-by-siren.json"; +import diffusible from "./__mocks__/diffusible.json" with { type: "json" }; +import partiallyNonDiffusible from "./__mocks__/partially-non-diffusible.json" with { type: "json" }; +import searchBySiren from "./__mocks__/search-by-siren.json" with { type: "json" }; +import { getOrganizationInfoFactory } from "./get-organization-info.js"; chai.use(chaiAsPromised); const assert = chai.assert; @@ -42,9 +43,11 @@ describe("getOrganizationInfo", () => { }; it("should return valid payload for diffusible établissement", async () => { - nock("https://api.insee.fr") - .get("/entreprises/sirene/siret/20007184300060") - .reply(200, diffusible); + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => Promise.reject(), + findBySiret: () => + Promise.resolve(diffusible.etablissement as any as InseeEtablissement), + }); await assert.eventually.deepEqual( getOrganizationInfo("20007184300060"), diffusibleOrganizationInfo, @@ -52,11 +55,13 @@ describe("getOrganizationInfo", () => { }); it("should return valid payload for diffusible établissement", async () => { - nock("https://api.insee.fr") - .get( - "/entreprises/sirene/siret?q=siren:200071843 AND etablissementSiege:true", - ) - .reply(200, searchBySiren); + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => + Promise.resolve( + searchBySiren.etablissements[0] as any as InseeEtablissement, + ), + findBySiret: () => Promise.reject(), + }); await assert.eventually.deepEqual( getOrganizationInfo("200071843"), diffusibleOrganizationInfo, @@ -64,9 +69,13 @@ describe("getOrganizationInfo", () => { }); it("should show partial data for partially non diffusible établissement", async () => { - nock("https://api.insee.fr") - .get("/entreprises/sirene/siret/94957325700019") - .reply(200, partiallyNonDiffusible); + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => Promise.reject(), + findBySiret: () => + Promise.resolve( + partiallyNonDiffusible.etablissement as any as InseeEtablissement, + ), + }); await assert.eventually.deepEqual(getOrganizationInfo("94957325700019"), { siret: "94957325700019", @@ -92,14 +101,13 @@ describe("getOrganizationInfo", () => { }); it("should throw for totally non diffusible établissement", async () => { - nock("https://api.insee.fr") - .get("/entreprises/sirene/siret/53512638700013") - .reply(403, { - header: { - statut: 403, - message: "Établissement non diffusable (53512638700013)", - }, - }); + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => Promise.reject(), + findBySiret: () => + Promise.resolve({ + statutDiffusionEtablissement: "N", + } as InseeEtablissement), + }); await assert.isRejected( getOrganizationInfo("53512638700013"), InseeNotFoundError, diff --git a/packages/identite/src/organization/get-organization-info.ts b/packages/identite/src/organization/get-organization-info.ts new file mode 100644 index 000000000..0090e3175 --- /dev/null +++ b/packages/identite/src/organization/get-organization-info.ts @@ -0,0 +1,241 @@ +import type { OrganizationInfo } from "@gouvfr-lasuite/proconnect.identite/types"; +import { + type FindBySirenHandler, + type FindBySiretHandler, +} from "@gouvfr-lasuite/proconnect.insee/api"; +import { + InseeConnectionError, + InseeNotFoundError, + InvalidSiretError, +} from "@gouvfr-lasuite/proconnect.insee/errors"; +import { + formatAdresseEtablissement, + formatEnseigne, + formatNomComplet, + libelleFromCategoriesJuridiques, + libelleFromCodeEffectif, + libelleFromCodeNaf, +} from "@gouvfr-lasuite/proconnect.insee/formatters"; +import type { InseeEtablissement } from "@gouvfr-lasuite/proconnect.insee/types"; +import { AxiosError } from "axios"; +import { cloneDeep, set } from "lodash-es"; + +const hideNonDiffusibleData = ( + etablissement: InseeEtablissement, +): InseeEtablissement => { + const hiddenEtablissement = cloneDeep(etablissement); + set(hiddenEtablissement, "uniteLegale.denominationUniteLegale", null); + set(hiddenEtablissement, "uniteLegale.sigleUniteLegale", null); + set(hiddenEtablissement, "uniteLegale.denominationUsuelle1UniteLegale", null); + set(hiddenEtablissement, "uniteLegale.denominationUsuelle2UniteLegale", null); + set(hiddenEtablissement, "uniteLegale.denominationUsuelle3UniteLegale", null); + set(hiddenEtablissement, "uniteLegale.sexeUniteLegale", null); + set(hiddenEtablissement, "uniteLegale.nomUniteLegale", null); + set(hiddenEtablissement, "uniteLegale.nomUsageUniteLegale", null); + set(hiddenEtablissement, "uniteLegale.prenom1UniteLegale", null); + set(hiddenEtablissement, "uniteLegale.prenom2UniteLegale", null); + set(hiddenEtablissement, "uniteLegale.prenom3UniteLegale", null); + set(hiddenEtablissement, "uniteLegale.prenom4UniteLegale", null); + set(hiddenEtablissement, "uniteLegale.prenomUsuelUniteLegale", null); + set(hiddenEtablissement, "uniteLegale.pseudonymeUniteLegale", null); + set( + hiddenEtablissement, + "adresseEtablissement.complementAdresseEtablissement", + null, + ); + set( + hiddenEtablissement, + "adresseEtablissement.numeroVoieEtablissement", + null, + ); + set( + hiddenEtablissement, + "adresseEtablissement.indiceRepetitionEtablissement", + null, + ); + set(hiddenEtablissement, "adresseEtablissement.typeVoieEtablissement", null); + set( + hiddenEtablissement, + "adresseEtablissement.libelleVoieEtablissement", + null, + ); + set( + hiddenEtablissement, + "adresse2Etablissement.complementAdresse2Etablissement", + null, + ); + set( + hiddenEtablissement, + "adresse2Etablissement.numeroVoie2Etablissement", + null, + ); + set( + hiddenEtablissement, + "adresse2Etablissement.indiceRepetition2Etablissement", + null, + ); + set( + hiddenEtablissement, + "adresse2Etablissement.typeVoie2Etablissement", + null, + ); + set( + hiddenEtablissement, + "adresse2Etablissement.libelleVoie2Etablissement", + null, + ); + + set( + hiddenEtablissement, + "periodesEtablissement.0.enseigne1Etablissement", + null, + ); + set( + hiddenEtablissement, + "periodesEtablissement.0.enseigne2Etablissement", + null, + ); + set( + hiddenEtablissement, + "periodesEtablissement.0.enseigne3Etablissement", + null, + ); + set( + hiddenEtablissement, + "periodesEtablissement.0.denominationUsuelleEtablissement", + null, + ); + + return hiddenEtablissement; +}; + +type FactoryDependencies = { + findBySiret: FindBySiretHandler; + findBySiren: FindBySirenHandler; +}; + +export function getOrganizationInfoFactory(dependencies: FactoryDependencies) { + const { findBySiren, findBySiret } = dependencies; + return async function getOrganizationInfo( + siretOrSiren: string, + ): Promise { + try { + let etablissement: InseeEtablissement; + + if (siretOrSiren.match(/^\d{14}$/)) { + etablissement = await findBySiret(siretOrSiren); + } else if (siretOrSiren.match(/^\d{9}$/)) { + etablissement = await findBySiren(siretOrSiren); + } else { + throw new InvalidSiretError(); + } + + const { statutDiffusionEtablissement } = etablissement; + + if (statutDiffusionEtablissement === "N") { + throw new InseeNotFoundError(); + } + + if (statutDiffusionEtablissement === "P") { + etablissement = hideNonDiffusibleData(etablissement); + } + + const { + siret: siretFromInseeApi, + trancheEffectifsEtablissement, + anneeEffectifsEtablissement, + adresseEtablissement, + periodesEtablissement, + uniteLegale, + } = etablissement; + + const { + categorieJuridiqueUniteLegale, + denominationUniteLegale, + sigleUniteLegale, + nomUniteLegale, + nomUsageUniteLegale, + prenomUsuelUniteLegale, + trancheEffectifsUniteLegale, + } = uniteLegale; + + // get last period to obtain most recent data + const { + activitePrincipaleEtablissement, + enseigne1Etablissement, + enseigne2Etablissement, + enseigne3Etablissement, + etatAdministratifEtablissement, + } = periodesEtablissement[0]; + + const { codePostalEtablissement, codeCommuneEtablissement } = + adresseEtablissement; + + const enseigne = formatEnseigne( + enseigne1Etablissement, + enseigne2Etablissement, + enseigne3Etablissement, + ); + + const nomComplet = formatNomComplet({ + denominationUniteLegale, + prenomUsuelUniteLegale, + nomUniteLegale, + nomUsageUniteLegale, + sigleUniteLegale, + }); + + const organizationLabel = `${nomComplet}${ + enseigne ? ` - ${enseigne}` : "" + }`; + + return { + siret: siretFromInseeApi, + libelle: organizationLabel, + nomComplet, + enseigne, + trancheEffectifs: trancheEffectifsEtablissement, + trancheEffectifsUniteLegale, + libelleTrancheEffectif: trancheEffectifsEtablissement + ? (libelleFromCodeEffectif( + trancheEffectifsEtablissement, + anneeEffectifsEtablissement, + ) ?? "") + : "", + etatAdministratif: etatAdministratifEtablissement, + estActive: etatAdministratifEtablissement === "A", + statutDiffusion: statutDiffusionEtablissement, + estDiffusible: statutDiffusionEtablissement === "O", + adresse: formatAdresseEtablissement(adresseEtablissement), + codePostal: codePostalEtablissement, + codeOfficielGeographique: codeCommuneEtablissement, + activitePrincipale: activitePrincipaleEtablissement, + libelleActivitePrincipale: libelleFromCodeNaf( + activitePrincipaleEtablissement, + ), + categorieJuridique: String(categorieJuridiqueUniteLegale), + libelleCategorieJuridique: + libelleFromCategoriesJuridiques(categorieJuridiqueUniteLegale) ?? "", + }; + } catch (e) { + if ( + e instanceof AxiosError && + e.response && + [403, 404].includes(e.response.status) + ) { + throw new InseeNotFoundError(); + } + + if ( + e instanceof AxiosError && + (e.code === "ECONNABORTED" || + e.code === "ERR_BAD_RESPONSE" || + e.code === "EAI_AGAIN") + ) { + throw new InseeConnectionError(); + } + + throw e; + } + }; +} diff --git a/packages/identite/src/organization/index.ts b/packages/identite/src/organization/index.ts new file mode 100644 index 000000000..4ee2827f7 --- /dev/null +++ b/packages/identite/src/organization/index.ts @@ -0,0 +1,4 @@ +// + +export * from "./get-organization-info.js"; +export * from "./upsert.js"; diff --git a/packages/identite/src/organization/upsert.ts b/packages/identite/src/organization/upsert.ts new file mode 100644 index 000000000..a42c8054b --- /dev/null +++ b/packages/identite/src/organization/upsert.ts @@ -0,0 +1,140 @@ +// + +import type { + DatabaseContext, + Organization, + OrganizationInfo, +} from "#src/types"; +import type { QueryResult } from "pg"; + +// + +export function upsertFactory({ pg }: DatabaseContext) { + return async function upsert({ + siret, + organizationInfo: { + libelle: cached_libelle, + nomComplet: cached_nom_complet, + enseigne: cached_enseigne, + trancheEffectifs: cached_tranche_effectifs, + trancheEffectifsUniteLegale: cached_tranche_effectifs_unite_legale, + libelleTrancheEffectif: cached_libelle_tranche_effectif, + etatAdministratif: cached_etat_administratif, + estActive: cached_est_active, + statutDiffusion: cached_statut_diffusion, + estDiffusible: cached_est_diffusible, + adresse: cached_adresse, + codePostal: cached_code_postal, + codeOfficielGeographique: cached_code_officiel_geographique, + activitePrincipale: cached_activite_principale, + libelleActivitePrincipale: cached_libelle_activite_principale, + categorieJuridique: cached_categorie_juridique, + libelleCategorieJuridique: cached_libelle_categorie_juridique, + }, + }: { + siret: string; + organizationInfo: OrganizationInfo; + }) { + const { rows }: QueryResult = await pg.query( + ` + INSERT INTO organizations + ( + siret, + cached_libelle, + cached_nom_complet, + cached_enseigne, + cached_tranche_effectifs, + cached_tranche_effectifs_unite_legale, + cached_libelle_tranche_effectif, + cached_etat_administratif, + cached_est_active, + cached_statut_diffusion, + cached_est_diffusible, + cached_adresse, + cached_code_postal, + cached_code_officiel_geographique, + cached_activite_principale, + cached_libelle_activite_principale, + cached_categorie_juridique, + cached_libelle_categorie_juridique, + organization_info_fetched_at, + updated_at, + created_at + ) + VALUES + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) + ON CONFLICT (siret) + DO UPDATE + SET ( + siret, + cached_libelle, + cached_nom_complet, + cached_enseigne, + cached_tranche_effectifs, + cached_tranche_effectifs_unite_legale, + cached_libelle_tranche_effectif, + cached_etat_administratif, + cached_est_active, + cached_statut_diffusion, + cached_est_diffusible, + cached_adresse, + cached_code_postal, + cached_code_officiel_geographique, + cached_activite_principale, + cached_libelle_activite_principale, + cached_categorie_juridique, + cached_libelle_categorie_juridique, + organization_info_fetched_at, + updated_at + ) = ( + EXCLUDED.siret, + EXCLUDED.cached_libelle, + EXCLUDED.cached_nom_complet, + EXCLUDED.cached_enseigne, + EXCLUDED.cached_tranche_effectifs, + EXCLUDED.cached_tranche_effectifs_unite_legale, + EXCLUDED.cached_libelle_tranche_effectif, + EXCLUDED.cached_etat_administratif, + EXCLUDED.cached_est_active, + EXCLUDED.cached_statut_diffusion, + EXCLUDED.cached_est_diffusible, + EXCLUDED.cached_adresse, + EXCLUDED.cached_code_postal, + EXCLUDED.cached_code_officiel_geographique, + EXCLUDED.cached_activite_principale, + EXCLUDED.cached_libelle_activite_principale, + EXCLUDED.cached_categorie_juridique, + EXCLUDED.cached_libelle_categorie_juridique, + EXCLUDED.organization_info_fetched_at, + EXCLUDED.updated_at + ) + RETURNING * + `, + [ + siret, + cached_libelle, + cached_nom_complet, + cached_enseigne, + cached_tranche_effectifs, + cached_tranche_effectifs_unite_legale, + cached_libelle_tranche_effectif, + cached_etat_administratif, + cached_est_active, + cached_statut_diffusion, + cached_est_diffusible, + cached_adresse, + cached_code_postal, + cached_code_officiel_geographique, + cached_activite_principale, + cached_libelle_activite_principale, + cached_categorie_juridique, + cached_libelle_categorie_juridique, + new Date(), + new Date(), + new Date(), + ], + ); + + return rows.shift()!; + }; +} diff --git a/packages/identite/src/organization/upset.test.ts b/packages/identite/src/organization/upset.test.ts new file mode 100644 index 000000000..c440652b8 --- /dev/null +++ b/packages/identite/src/organization/upset.test.ts @@ -0,0 +1,54 @@ +// + +import { PGlite } from "@electric-sql/pglite"; +import { expect } from "chai"; +import { noop } from "lodash-es"; +import { before, describe, it } from "mocha"; +import { runner } from "node-pg-migrate"; +import { join } from "path"; +import { upsertFactory } from "./upsert.js"; + +// + +const pg = new PGlite(); +const upset = upsertFactory({ 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", + log: noop, + }); +}); + +describe("upset", () => { + it("should create the Tau Empire organization", async () => { + const organization = await upset({ + organizationInfo: { + libelle: "Tau Empire", + nomComplet: "Tau Empire", + } as any, + siret: "👽️", + }); + expect(organization.created_at).to.deep.equal(organization.updated_at); + }); + + it("should update the Necron organization", async () => { + await pg.sql`insert into organizations + (siret, created_at, updated_at) + VALUES + ('⚰️', '1967-12-19', '1967-12-19'); + `; + const organization = await upset({ + organizationInfo: { + libelle: "Necron", + nomComplet: "Necrontyr", + } as any, + siret: "⚰️", + }); + expect(organization.created_at).to.not.deep.equal(organization.updated_at); + expect(organization.cached_libelle).to.equal("Necron"); + }); +}); diff --git a/packages/identite/src/services/hash-to-postgres-params.ts b/packages/identite/src/services/hash-to-postgres-params.ts new file mode 100644 index 000000000..d3f6d570b --- /dev/null +++ b/packages/identite/src/services/hash-to-postgres-params.ts @@ -0,0 +1,34 @@ +// + +import { chain } from "lodash-es"; + +// + +export function hashToPostgresParams(fieldsToUpdate: Partial): { + // postgres column-list syntax + paramsString: string; + // postgres column-list syntax for prepared statement + valuesString: string; + values: any[]; +} { + const paramsString = "(" + Object.keys(fieldsToUpdate).join(", ") + ")"; + // 'email, encrypted_password' + + const valuesString = + "(" + + chain(fieldsToUpdate) + // { email: 'email@xy.z', encrypted_password: 'hash' } + .toPairs() + // [[ 'email', 'email@xy.z'], ['encrypted_password', 'hash' ]] + .map((_value, index) => `$${index + 1}`) + // [ '$1', '$2' ] + .join(", ") + // '$1, $2' + .value() + + ")"; + + const values = Object.values(fieldsToUpdate); + // [ 'email@xy.z', 'hash' ] + + return { paramsString, valuesString, values }; +} diff --git a/packages/identite/src/services/index.ts b/packages/identite/src/services/index.ts new file mode 100644 index 000000000..8d8320223 --- /dev/null +++ b/packages/identite/src/services/index.ts @@ -0,0 +1,3 @@ +// + +export * from "./hash-to-postgres-params.js"; diff --git a/packages/identite/src/types/contexts.ts b/packages/identite/src/types/contexts.ts new file mode 100644 index 000000000..c55b4067d --- /dev/null +++ b/packages/identite/src/types/contexts.ts @@ -0,0 +1,9 @@ +// + +import Pg from "pg"; + +// + +export type DatabaseContext = { + pg: Pg.Pool; +}; diff --git a/packages/identite/src/types/index.ts b/packages/identite/src/types/index.ts new file mode 100644 index 000000000..1c97d28bb --- /dev/null +++ b/packages/identite/src/types/index.ts @@ -0,0 +1,6 @@ +// + +export * from "./contexts.js"; +export * from "./organization-info.js"; +export * from "./organization.js"; +export * from "./user.js"; diff --git a/src/types/organization-info.d.ts b/packages/identite/src/types/organization-info.ts similarity index 83% rename from src/types/organization-info.d.ts rename to packages/identite/src/types/organization-info.ts index 7745efae0..2bf6ac3f6 100644 --- a/src/types/organization-info.d.ts +++ b/packages/identite/src/types/organization-info.ts @@ -1,8 +1,10 @@ // source : https://www.sirene.fr/sirene/public/variable/trancheEffectifsEtablissement -import { TrancheEffectifs } from "@gouvfr-lasuite/proconnect.insee/types"; +import type { TrancheEffectifs } from "@gouvfr-lasuite/proconnect.insee/types"; -interface OrganizationInfo { +// + +export interface OrganizationInfo { siret: string; libelle: string; nomComplet: string; diff --git a/src/types/organization.d.ts b/packages/identite/src/types/organization.ts similarity index 87% rename from src/types/organization.d.ts rename to packages/identite/src/types/organization.ts index a966b5db8..ded1fce86 100644 --- a/src/types/organization.d.ts +++ b/packages/identite/src/types/organization.ts @@ -1,6 +1,10 @@ -import { TrancheEffectifs } from "@gouvfr-lasuite/proconnect.insee/types"; +// -interface Organization { +import type { TrancheEffectifs } from "@gouvfr-lasuite/proconnect.insee/types"; + +// + +export interface Organization { id: number; siret: string; created_at: Date; 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/create.test.ts b/packages/identite/src/user/create.test.ts new file mode 100644 index 000000000..0396e2b26 --- /dev/null +++ b/packages/identite/src/user/create.test.ts @@ -0,0 +1,31 @@ +// + +import { PGlite } from "@electric-sql/pglite"; +import { expect } from "chai"; +import { noop } from "lodash-es"; +import { before, describe, it } from "mocha"; +import { runner } from "node-pg-migrate"; +import { join } from "path"; +import { createUserFactory } from "./create.js"; + +// + +const pg = new PGlite(); +const createUser = createUserFactory({ pg: pg as any }); + +before(async function migrate() { + await runner({ + dbClient: pg as any, + dir: join(import.meta.dirname, "../../../../migrations"), + direction: "up", + log: noop, + migrationsTable: "pg-migrate", + }); +}); + +describe("CreateUser", () => { + it("should create the god-emperor of mankind", async () => { + const user = await createUser({ email: "god-emperor@mankind" }); + expect(user.email).to.equal("god-emperor@mankind"); + }); +}); diff --git a/packages/identite/src/user/create.ts b/packages/identite/src/user/create.ts new file mode 100644 index 000000000..d7a6ea6b3 --- /dev/null +++ b/packages/identite/src/user/create.ts @@ -0,0 +1,42 @@ +// + +import { hashToPostgresParams } from "#src/services"; +import type { DatabaseContext, User } from "#src/types"; +import type { QueryResult } from "pg"; + +// + +export function createUserFactory({ pg }: DatabaseContext) { + return async function createUser({ + email, + encrypted_password = null, + }: { + email: string; + encrypted_password?: string | null; + }) { + const userWithTimestamps = { + email, + email_verified: false, + verify_email_token: null, + verify_email_sent_at: null, + encrypted_password, + magic_link_token: null, + magic_link_sent_at: null, + reset_password_token: null, + reset_password_sent_at: null, + sign_in_count: 0, + last_sign_in_at: null, + created_at: new Date(), + updated_at: new Date(), + }; + + const { paramsString, valuesString, values } = + hashToPostgresParams(userWithTimestamps); + + const { rows }: QueryResult = await pg.query( + `INSERT INTO users ${paramsString} VALUES ${valuesString} RETURNING *;`, + values, + ); + return rows.shift()!; + }; +} 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..03872c226 --- /dev/null +++ b/packages/identite/src/user/find-by-email.test.ts @@ -0,0 +1,45 @@ +// + +import { PGlite } from "@electric-sql/pglite"; +import { expect } from "chai"; +import { noop } from "lodash-es"; +import { before, describe, it } from "mocha"; +import { runner } from "node-pg-migrate"; +import { join } from "path"; +import { findByEmailFactory } from "./find-by-email.js"; + +// + +const pg = new PGlite(); +const findByEmail = findByEmailFactory({ 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", + log: noop, + }); +}); + +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?.email).to.equal("lion.eljonson@darkangels.world"); + }); + + 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..75ba3e85a --- /dev/null +++ b/packages/identite/src/user/find-by-email.ts @@ -0,0 +1,20 @@ +// + +import type { DatabaseContext, User } from "#src/types"; +import { type QueryResult } from "pg"; + +// + +export function findByEmailFactory({ pg }: DatabaseContext) { + 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..466f4bcea --- /dev/null +++ b/packages/identite/src/user/index.ts @@ -0,0 +1,5 @@ +// + +export * from "./create.js"; +export * from "./find-by-email.js"; +export * from "./update.js"; diff --git a/packages/identite/src/user/update.test.ts b/packages/identite/src/user/update.test.ts new file mode 100644 index 000000000..5bba1028a --- /dev/null +++ b/packages/identite/src/user/update.test.ts @@ -0,0 +1,36 @@ +// + +import { PGlite } from "@electric-sql/pglite"; +import { expect } from "chai"; +import { noop } from "lodash-es"; +import { before, describe, it } from "mocha"; +import { runner } from "node-pg-migrate"; +import { join } from "path"; +import { updateUserFactory } from "./update.js"; + +// + +const pg = new PGlite(); +const updateUser = updateUserFactory({ pg: pg as any }); + +before(async function migrate() { + await runner({ + dbClient: pg as any, + dir: join(import.meta.dirname, "../../../../migrations"), + direction: "up", + log: noop, + migrationsTable: "pg-migrate", + }); +}); + +describe("UpdateUser", () => { + it("should update the user job", 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'); + `; + const user = await updateUser(1, { job: "Chevalier de l'Ordre" }); + expect(user.job).to.equal("Chevalier de l'Ordre"); + }); +}); diff --git a/packages/identite/src/user/update.ts b/packages/identite/src/user/update.ts new file mode 100644 index 000000000..e1299fbe5 --- /dev/null +++ b/packages/identite/src/user/update.ts @@ -0,0 +1,29 @@ +// + +import { hashToPostgresParams } from "#src/services"; +import type { DatabaseContext, User } from "#src/types"; +import type { QueryResult } from "pg"; + +// + +export function updateUserFactory({ pg }: DatabaseContext) { + return async function updateUser(id: number, fieldsToUpdate: Partial) { + const fieldsToUpdateWithTimestamps = { + ...fieldsToUpdate, + updated_at: new Date(), + }; + + const { paramsString, valuesString, values } = hashToPostgresParams( + fieldsToUpdateWithTimestamps, + ); + + const { rows }: QueryResult = await pg.query( + `UPDATE users SET ${paramsString} = ${valuesString} WHERE id = $${ + values.length + 1 + } RETURNING *`, + [...values, id], + ); + + return rows.shift()!; + }; +} diff --git a/packages/identite/tsconfig.json b/packages/identite/tsconfig.json new file mode 100644 index 000000000..c66deae2b --- /dev/null +++ b/packages/identite/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "module": "NodeNext", + "moduleResolution": "nodenext", + "outDir": "./dist", + "resolveJsonModule": true, + "rootDir": "src", + "types": ["node"], + "verbatimModuleSyntax": true, + "paths": { + "#src/*": ["./src/*"] + } + }, + "extends": "@tsconfig/node22/tsconfig.json", + "references": [ + { "path": "../core/tsconfig.lib.json" }, + { "path": "../insee/tsconfig.lib.json" } + ], + "include": ["./src/**/*", "./src/**/*.json"] +} 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 index 1856b8930..c9e4a9ff2 100644 --- a/packages/insee/package.json +++ b/packages/insee/package.json @@ -32,7 +32,7 @@ } }, "scripts": { - "build": "tsc --project tsconfig.lib.json", + "build": "tsc --build tsconfig.lib.json", "check": "npm run build -- --noEmit", "dev": "npm run build -- --watch --preserveWatchOutput", "test": "mocha" diff --git a/packages/insee/src/data/codes-effectifs.ts b/packages/insee/src/data/codes-effectifs.ts index a46976090..f860a11a9 100644 --- a/packages/insee/src/data/codes-effectifs.ts +++ b/packages/insee/src/data/codes-effectifs.ts @@ -1,8 +1,4 @@ -// - -import type { TrancheEffectifs } from "#src/types/tranche-effectifs.js"; - -// +import type { TrancheEffectifs } from "@gouvfr-lasuite/proconnect.insee/types"; export const codesEffectifs: { [K in NonNullable]: string } = { diff --git a/scripts/import-accounts-coop.ts b/scripts/import-accounts-coop.ts index ab671286a..47dcf631d 100644 --- a/scripts/import-accounts-coop.ts +++ b/scripts/import-accounts-coop.ts @@ -5,18 +5,23 @@ import { isPhoneNumberValid, isSiretValid, } from "@gouvfr-lasuite/proconnect.core/security"; +import { + createUserFactory, + findByEmailFactory, + updateUserFactory, +} from "@gouvfr-lasuite/proconnect.identite/user"; 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 { getOrganizationInfo } from "../src/connectors/api-sirene"; +import { getDatabaseConnection } from "../src/connectors/postgres"; import { findByUserId } from "../src/repositories/organization/getters"; import { linkUserToOrganization, upsert, } from "../src/repositories/organization/setters"; -import { create, findByEmail, update } from "../src/repositories/user"; import { logger } from "../src/services/log"; import { getNumberOfLineInFile, @@ -26,6 +31,15 @@ import { throttleApiCall, } from "../src/services/script-helpers"; +// + +const pg = getDatabaseConnection(); +const findByEmail = findByEmailFactory({ pg }); +const create = createUserFactory({ pg }); +const update = updateUserFactory({ pg }); + +// + const { INPUT_FILE, OUTPUT_FILE } = z .object({ INPUT_FILE: z.string().default("./input.csv"), @@ -102,10 +116,10 @@ const maxInseeCallRateInMs = rateInMsFromArgs !== 0 ? rateInMsFromArgs : 125; const start = startDurationMesure(); try { const { + coordinateur, prenom: given_name, nom: family_name, téléphone: phone_number, - coordinateur, "email professionnel secondaire": professional_email, "SIRET structure": siret, } = data; diff --git a/scripts/import-domains.ts b/scripts/import-domains.ts index 9094eca03..a67973005 100644 --- a/scripts/import-domains.ts +++ b/scripts/import-domains.ts @@ -3,6 +3,7 @@ import { isDomainValid, isSiretValid, } from "@gouvfr-lasuite/proconnect.core/security"; +import type { Organization } from "@gouvfr-lasuite/proconnect.identite/types"; import { AxiosError } from "axios"; import { parse, stringify, transform } from "csv"; import fs from "fs"; @@ -24,7 +25,6 @@ import { startDurationMesure, throttleApiCall, } from "../src/services/script-helpers"; -import type { Organization } from "../src/types/organization"; const { INPUT_FILE, OUTPUT_FILE } = z .object({ diff --git a/src/connectors/api-sirene.ts b/src/connectors/api-sirene.ts new file mode 100644 index 000000000..17076d0f8 --- /dev/null +++ b/src/connectors/api-sirene.ts @@ -0,0 +1,44 @@ +// + +import { getOrganizationInfoFactory } from "@gouvfr-lasuite/proconnect.identite/organization"; +import { + findBySirenFactory, + findBySiretFactory, + getInseeAccessTokenFactory, +} from "@gouvfr-lasuite/proconnect.insee/api"; +import { + HTTP_CLIENT_TIMEOUT, + INSEE_CONSUMER_KEY, + INSEE_CONSUMER_SECRET, +} from "../config/env"; + +// + +export const getInseeAccessToken = getInseeAccessTokenFactory( + { + consumerKey: INSEE_CONSUMER_KEY, + consumerSecret: INSEE_CONSUMER_SECRET, + }, + { + timeout: HTTP_CLIENT_TIMEOUT, + }, +); + +export const findBySiret = findBySiretFactory({ + getInseeAccessToken, + config: { + timeout: HTTP_CLIENT_TIMEOUT, + }, +}); + +export const findBySiren = findBySirenFactory({ + getInseeAccessToken, + config: { + timeout: HTTP_CLIENT_TIMEOUT, + }, +}); + +export const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren, + findBySiret, +}); diff --git a/src/connectors/api-sirene/index.ts b/src/connectors/api-sirene/index.ts deleted file mode 100644 index a24fe8973..000000000 --- a/src/connectors/api-sirene/index.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { - findBySirenFactory, - findBySiretFactory, - getInseeAccessTokenFactory, -} from "@gouvfr-lasuite/proconnect.insee/api"; -import { - formatAdresseEtablissement, - formatEnseigne, - formatNomComplet, - libelleFromCategoriesJuridiques, - libelleFromCodeEffectif, - libelleFromCodeNaf, -} from "@gouvfr-lasuite/proconnect.insee/formatters"; -import type { InseeEtablissement } from "@gouvfr-lasuite/proconnect.insee/types"; -import { AxiosError } from "axios"; -import { cloneDeep, set } from "lodash-es"; -import { - HTTP_CLIENT_TIMEOUT, - INSEE_CONSUMER_KEY, - INSEE_CONSUMER_SECRET, -} from "../../config/env"; -import { - InseeConnectionError, - InseeNotFoundError, - InvalidSiretError, -} from "../../config/errors"; -import type { OrganizationInfo } from "../../types/organization-info"; - -const hideNonDiffusibleData = ( - etablissement: InseeEtablissement, -): InseeEtablissement => { - const hiddenEtablissement = cloneDeep(etablissement); - set(hiddenEtablissement, "uniteLegale.denominationUniteLegale", null); - set(hiddenEtablissement, "uniteLegale.sigleUniteLegale", null); - set(hiddenEtablissement, "uniteLegale.denominationUsuelle1UniteLegale", null); - set(hiddenEtablissement, "uniteLegale.denominationUsuelle2UniteLegale", null); - set(hiddenEtablissement, "uniteLegale.denominationUsuelle3UniteLegale", null); - set(hiddenEtablissement, "uniteLegale.sexeUniteLegale", null); - set(hiddenEtablissement, "uniteLegale.nomUniteLegale", null); - set(hiddenEtablissement, "uniteLegale.nomUsageUniteLegale", null); - set(hiddenEtablissement, "uniteLegale.prenom1UniteLegale", null); - set(hiddenEtablissement, "uniteLegale.prenom2UniteLegale", null); - set(hiddenEtablissement, "uniteLegale.prenom3UniteLegale", null); - set(hiddenEtablissement, "uniteLegale.prenom4UniteLegale", null); - set(hiddenEtablissement, "uniteLegale.prenomUsuelUniteLegale", null); - set(hiddenEtablissement, "uniteLegale.pseudonymeUniteLegale", null); - set( - hiddenEtablissement, - "adresseEtablissement.complementAdresseEtablissement", - null, - ); - set( - hiddenEtablissement, - "adresseEtablissement.numeroVoieEtablissement", - null, - ); - set( - hiddenEtablissement, - "adresseEtablissement.indiceRepetitionEtablissement", - null, - ); - set(hiddenEtablissement, "adresseEtablissement.typeVoieEtablissement", null); - set( - hiddenEtablissement, - "adresseEtablissement.libelleVoieEtablissement", - null, - ); - set( - hiddenEtablissement, - "adresse2Etablissement.complementAdresse2Etablissement", - null, - ); - set( - hiddenEtablissement, - "adresse2Etablissement.numeroVoie2Etablissement", - null, - ); - set( - hiddenEtablissement, - "adresse2Etablissement.indiceRepetition2Etablissement", - null, - ); - set( - hiddenEtablissement, - "adresse2Etablissement.typeVoie2Etablissement", - null, - ); - set( - hiddenEtablissement, - "adresse2Etablissement.libelleVoie2Etablissement", - null, - ); - - set( - hiddenEtablissement, - "periodesEtablissement.0.enseigne1Etablissement", - null, - ); - set( - hiddenEtablissement, - "periodesEtablissement.0.enseigne2Etablissement", - null, - ); - set( - hiddenEtablissement, - "periodesEtablissement.0.enseigne3Etablissement", - null, - ); - set( - hiddenEtablissement, - "periodesEtablissement.0.denominationUsuelleEtablissement", - null, - ); - - return hiddenEtablissement; -}; - -export const getInseeAccessToken = getInseeAccessTokenFactory( - { - consumerKey: INSEE_CONSUMER_KEY, - consumerSecret: INSEE_CONSUMER_SECRET, - }, - { - timeout: HTTP_CLIENT_TIMEOUT, - }, -); - -const findBySiret = findBySiretFactory({ - getInseeAccessToken, - config: { - timeout: HTTP_CLIENT_TIMEOUT, - }, -}); - -const findBySiren = findBySirenFactory({ - getInseeAccessToken, - config: { - timeout: HTTP_CLIENT_TIMEOUT, - }, -}); - -export const getOrganizationInfo = async ( - siretOrSiren: string, -): Promise => { - try { - let etablissement: InseeEtablissement; - - if (siretOrSiren.match(/^\d{14}$/)) { - etablissement = await findBySiret(siretOrSiren); - } else if (siretOrSiren.match(/^\d{9}$/)) { - etablissement = await findBySiren(siretOrSiren); - } else { - throw new InvalidSiretError(); - } - - const { statutDiffusionEtablissement } = etablissement; - - if (statutDiffusionEtablissement === "N") { - throw new InseeNotFoundError(); - } - - if (statutDiffusionEtablissement === "P") { - etablissement = hideNonDiffusibleData(etablissement); - } - - const { - siret: siretFromInseeApi, - trancheEffectifsEtablissement, - anneeEffectifsEtablissement, - adresseEtablissement, - periodesEtablissement, - uniteLegale, - } = etablissement; - - const { - categorieJuridiqueUniteLegale, - denominationUniteLegale, - sigleUniteLegale, - nomUniteLegale, - nomUsageUniteLegale, - prenomUsuelUniteLegale, - trancheEffectifsUniteLegale, - } = uniteLegale; - - // get last period to obtain most recent data - const { - activitePrincipaleEtablissement, - enseigne1Etablissement, - enseigne2Etablissement, - enseigne3Etablissement, - etatAdministratifEtablissement, - } = periodesEtablissement[0]; - - const { codePostalEtablissement, codeCommuneEtablissement } = - adresseEtablissement; - - const enseigne = formatEnseigne( - enseigne1Etablissement, - enseigne2Etablissement, - enseigne3Etablissement, - ); - - const nomComplet = formatNomComplet({ - denominationUniteLegale, - prenomUsuelUniteLegale, - nomUniteLegale, - nomUsageUniteLegale, - sigleUniteLegale, - }); - - const organizationLabel = `${nomComplet}${ - enseigne ? ` - ${enseigne}` : "" - }`; - - return { - siret: siretFromInseeApi, - libelle: organizationLabel, - nomComplet, - enseigne, - trancheEffectifs: trancheEffectifsEtablissement, - trancheEffectifsUniteLegale, - libelleTrancheEffectif: - libelleFromCodeEffectif( - trancheEffectifsEtablissement, - anneeEffectifsEtablissement, - ) ?? "", - etatAdministratif: etatAdministratifEtablissement, - estActive: etatAdministratifEtablissement === "A", - statutDiffusion: statutDiffusionEtablissement, - estDiffusible: statutDiffusionEtablissement === "O", - adresse: formatAdresseEtablissement(adresseEtablissement), - codePostal: codePostalEtablissement, - codeOfficielGeographique: codeCommuneEtablissement, - activitePrincipale: activitePrincipaleEtablissement, - libelleActivitePrincipale: libelleFromCodeNaf( - activitePrincipaleEtablissement, - ), - categorieJuridique: String(categorieJuridiqueUniteLegale), - libelleCategorieJuridique: - libelleFromCategoriesJuridiques(categorieJuridiqueUniteLegale) ?? "", - }; - } catch (e) { - if ( - e instanceof AxiosError && - e.response && - [403, 404].includes(e.response.status) - ) { - throw new InseeNotFoundError(); - } - - if ( - e instanceof AxiosError && - (e.code === "ECONNABORTED" || - e.code === "ERR_BAD_RESPONSE" || - e.code === "EAI_AGAIN") - ) { - throw new InseeConnectionError(); - } - - throw e; - } -}; diff --git a/src/managers/moderation.ts b/src/managers/moderation.ts index ff8984282..239add3e6 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 { HOST } from "../config/env"; import { ForbiddenError, NotFoundError } from "../config/errors"; diff --git a/src/managers/organization/join.ts b/src/managers/organization/join.ts index 9d2857c3b..2c76ffa54 100644 --- a/src/managers/organization/join.ts +++ b/src/managers/organization/join.ts @@ -1,5 +1,9 @@ import { isEmailValid } from "@gouvfr-lasuite/proconnect.core/security"; import { Welcome } from "@gouvfr-lasuite/proconnect.email"; +import type { + Organization, + OrganizationInfo, +} from "@gouvfr-lasuite/proconnect.identite/types"; import * as Sentry from "@sentry/node"; import { isEmpty, some } from "lodash-es"; import { @@ -54,8 +58,6 @@ import { isEtablissementScolaireDuPremierEtSecondDegre, isSmallAssociation, } from "../../services/organization"; -import type { Organization } from "../../types/organization"; -import type { OrganizationInfo } from "../../types/organization-info"; import { unableToAutoJoinOrganizationMd } from "../../views/mails/unable-to-auto-join-organization"; import { getOrganizationsByUserId, markDomainAsVerified } from "./main"; diff --git a/src/managers/organization/main.ts b/src/managers/organization/main.ts index 4082d0532..6a676aa70 100644 --- a/src/managers/organization/main.ts +++ b/src/managers/organization/main.ts @@ -1,3 +1,4 @@ +import type { Organization } from "@gouvfr-lasuite/proconnect.identite/types"; import { isEmpty, some } from "lodash-es"; import { NotFoundError } from "../../config/errors"; import { @@ -16,7 +17,6 @@ import { } from "../../repositories/organization/setters"; import { setSelectedOrganizationId } from "../../repositories/redis/selected-organization"; import { getEmailDomain } from "../../services/email"; -import type { Organization } from "../../types/organization"; export const getOrganizationsByUserId = findByUserId; export const getOrganizationById = findOrganizationById; diff --git a/src/managers/session/authenticated.ts b/src/managers/session/authenticated.ts index 13062eb07..fca525e2b 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"; diff --git a/src/managers/user.ts b/src/managers/user.ts index e79be9d17..4d52ba5a9 100644 --- a/src/managers/user.ts +++ b/src/managers/user.ts @@ -19,6 +19,7 @@ import { UpdateTotpApplication, VerifyEmail, } from "@gouvfr-lasuite/proconnect.email"; +import type { User } from "@gouvfr-lasuite/proconnect.identite/types"; import { isEmpty } from "lodash-es"; import { HOST, diff --git a/src/repositories/organization/getters.ts b/src/repositories/organization/getters.ts index 1107cf957..7d66fa2eb 100644 --- a/src/repositories/organization/getters.ts +++ b/src/repositories/organization/getters.ts @@ -1,6 +1,9 @@ +import type { + Organization, + User, +} from "@gouvfr-lasuite/proconnect.identite/types"; import type { QueryResult } from "pg"; import { getDatabaseConnection } from "../../connectors/postgres"; -import type { Organization } from "../../types/organization"; export const findById = async (id: number) => { const connection = getDatabaseConnection(); diff --git a/src/repositories/organization/setters.ts b/src/repositories/organization/setters.ts index 3a0adfe2f..3429b39d7 100644 --- a/src/repositories/organization/setters.ts +++ b/src/repositories/organization/setters.ts @@ -1,138 +1,10 @@ +import { upsertFactory } from "@gouvfr-lasuite/proconnect.identite/organization"; +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"; -import type { Organization } from "../../types/organization"; -import type { OrganizationInfo } from "../../types/organization-info"; -export const upsert = async ({ - siret, - organizationInfo: { - libelle: cached_libelle, - nomComplet: cached_nom_complet, - enseigne: cached_enseigne, - trancheEffectifs: cached_tranche_effectifs, - trancheEffectifsUniteLegale: cached_tranche_effectifs_unite_legale, - libelleTrancheEffectif: cached_libelle_tranche_effectif, - etatAdministratif: cached_etat_administratif, - estActive: cached_est_active, - statutDiffusion: cached_statut_diffusion, - estDiffusible: cached_est_diffusible, - adresse: cached_adresse, - codePostal: cached_code_postal, - codeOfficielGeographique: cached_code_officiel_geographique, - activitePrincipale: cached_activite_principale, - libelleActivitePrincipale: cached_libelle_activite_principale, - categorieJuridique: cached_categorie_juridique, - libelleCategorieJuridique: cached_libelle_categorie_juridique, - }, -}: { - siret: string; - organizationInfo: OrganizationInfo; -}) => { - const connection = getDatabaseConnection(); - - const { rows }: QueryResult = await connection.query( - ` -INSERT INTO organizations - ( - siret, - cached_libelle, - cached_nom_complet, - cached_enseigne, - cached_tranche_effectifs, - cached_tranche_effectifs_unite_legale, - cached_libelle_tranche_effectif, - cached_etat_administratif, - cached_est_active, - cached_statut_diffusion, - cached_est_diffusible, - cached_adresse, - cached_code_postal, - cached_code_officiel_geographique, - cached_activite_principale, - cached_libelle_activite_principale, - cached_categorie_juridique, - cached_libelle_categorie_juridique, - organization_info_fetched_at, - updated_at, - created_at - ) -VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) -ON CONFLICT (siret) -DO UPDATE - SET ( - siret, - cached_libelle, - cached_nom_complet, - cached_enseigne, - cached_tranche_effectifs, - cached_tranche_effectifs_unite_legale, - cached_libelle_tranche_effectif, - cached_etat_administratif, - cached_est_active, - cached_statut_diffusion, - cached_est_diffusible, - cached_adresse, - cached_code_postal, - cached_code_officiel_geographique, - cached_activite_principale, - cached_libelle_activite_principale, - cached_categorie_juridique, - cached_libelle_categorie_juridique, - organization_info_fetched_at, - updated_at - ) = ( - EXCLUDED.siret, - EXCLUDED.cached_libelle, - EXCLUDED.cached_nom_complet, - EXCLUDED.cached_enseigne, - EXCLUDED.cached_tranche_effectifs, - EXCLUDED.cached_tranche_effectifs_unite_legale, - EXCLUDED.cached_libelle_tranche_effectif, - EXCLUDED.cached_etat_administratif, - EXCLUDED.cached_est_active, - EXCLUDED.cached_statut_diffusion, - EXCLUDED.cached_est_diffusible, - EXCLUDED.cached_adresse, - EXCLUDED.cached_code_postal, - EXCLUDED.cached_code_officiel_geographique, - EXCLUDED.cached_activite_principale, - EXCLUDED.cached_libelle_activite_principale, - EXCLUDED.cached_categorie_juridique, - EXCLUDED.cached_libelle_categorie_juridique, - EXCLUDED.organization_info_fetched_at, - EXCLUDED.updated_at - ) -RETURNING * -`, - [ - siret, - cached_libelle, - cached_nom_complet, - cached_enseigne, - cached_tranche_effectifs, - cached_tranche_effectifs_unite_legale, - cached_libelle_tranche_effectif, - cached_etat_administratif, - cached_est_active, - cached_statut_diffusion, - cached_est_diffusible, - cached_adresse, - cached_code_postal, - cached_code_officiel_geographique, - cached_activite_principale, - cached_libelle_activite_principale, - cached_categorie_juridique, - cached_libelle_categorie_juridique, - new Date(), - new Date(), - new Date(), - ], - ); - - return rows.shift()!; -}; +export const upsert = upsertFactory({ pg: getDatabaseConnection() }); export const linkUserToOrganization = async ({ organization_id, diff --git a/src/repositories/user.ts b/src/repositories/user.ts index 55254512a..75783d273 100644 --- a/src/repositories/user.ts +++ b/src/repositories/user.ts @@ -1,7 +1,11 @@ +import type { User } from "@gouvfr-lasuite/proconnect.identite/types"; +import { + createUserFactory, + findByEmailFactory, + updateUserFactory, +} from "@gouvfr-lasuite/proconnect.identite/user"; 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(); @@ -16,19 +20,7 @@ FROM users WHERE id = $1 return rows.shift(); }; -export const findByEmail = async (email: string) => { - const connection = getDatabaseConnection(); - - const { rows }: QueryResult = await connection.query( - ` -SELECT * -FROM users WHERE email = $1 -`, - [email], - ); - - return rows.shift(); -}; +export const findByEmail = findByEmailFactory({ pg: getDatabaseConnection() }); export const findByMagicLinkToken = async (magic_link_token: string) => { const connection = getDatabaseConnection(); @@ -60,63 +52,9 @@ FROM users WHERE reset_password_token = $1 return rows.shift(); }; -export const update = async (id: number, fieldsToUpdate: Partial) => { - const connection = getDatabaseConnection(); - - const fieldsToUpdateWithTimestamps = { - ...fieldsToUpdate, - updated_at: new Date(), - }; - - const { paramsString, valuesString, values } = hashToPostgresParams( - fieldsToUpdateWithTimestamps, - ); - - const { rows }: QueryResult = await connection.query( - `UPDATE users SET ${paramsString} = ${valuesString} WHERE id = $${ - values.length + 1 - } RETURNING *`, - [...values, id], - ); - - return rows.shift()!; -}; - -export const create = async ({ - email, - encrypted_password = null, -}: { - email: string; - encrypted_password?: string | null; -}) => { - const connection = getDatabaseConnection(); - - const userWithTimestamps = { - email, - email_verified: false, - verify_email_token: null, - verify_email_sent_at: null, - encrypted_password, - magic_link_token: null, - magic_link_sent_at: null, - reset_password_token: null, - reset_password_sent_at: null, - sign_in_count: 0, - last_sign_in_at: null, - created_at: new Date(), - updated_at: new Date(), - }; +export const update = updateUserFactory({ pg: getDatabaseConnection() }); - const { paramsString, valuesString, values } = - hashToPostgresParams(userWithTimestamps); - - const { rows }: QueryResult = await connection.query( - `INSERT INTO users ${paramsString} VALUES ${valuesString} RETURNING *;`, - values, - ); - - return rows.shift()!; -}; +export const create = createUserFactory({ pg: getDatabaseConnection() }); export const deleteUser = async (id: number) => { const connection = getDatabaseConnection(); diff --git a/src/services/organization.ts b/src/services/organization.ts index db2aaccd1..2eeea3f25 100644 --- a/src/services/organization.ts +++ b/src/services/organization.ts @@ -1,5 +1,5 @@ import { isDomainValid } from "@gouvfr-lasuite/proconnect.core/security"; -import type { Organization } from "../types/organization"; +import type { Organization } from "@gouvfr-lasuite/proconnect.identite/types"; /** * These fonctions return approximate results. As the data tranche effectifs is diff --git a/src/services/script-helpers.ts b/src/services/script-helpers.ts index 9a954cc4c..994bd0f85 100644 --- a/src/services/script-helpers.ts +++ b/src/services/script-helpers.ts @@ -1,7 +1,7 @@ // from https://ipirozhenko.com/blog/measuring-requests-duration-nodejs-express/ +import type { OrganizationInfo } from "@gouvfr-lasuite/proconnect.identite/types"; import fs from "fs"; import { isEmpty } from "lodash-es"; -import type { OrganizationInfo } from "../types/organization-info"; export const startDurationMesure = () => { return process.hrtime(); diff --git a/test/organization.test.ts b/test/organization.test.ts index 596e54822..44d73f4b5 100644 --- a/test/organization.test.ts +++ b/test/organization.test.ts @@ -1,3 +1,4 @@ +import type { Organization } from "@gouvfr-lasuite/proconnect.identite/types"; import { assert } from "chai"; import { isCommune, @@ -8,7 +9,6 @@ import { isSmallAssociation, isWasteManagementOrganization, } from "../src/services/organization"; -import type { Organization } from "../src/types/organization"; const association_org_info = { siret: "83511518900010", diff --git a/tsconfig.json b/tsconfig.json index 8a16caacc..e5aae435e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,9 +21,9 @@ "exclude": ["packages/*", "build/*"], "references": [ { "path": "./packages/core/tsconfig.lib.json" }, - { - "path": "./packages/email/tsconfig.lib.json" - } + { "path": "./packages/email/tsconfig.lib.json" }, + { "path": "./packages/identite/tsconfig.lib.json" }, + { "path": "./packages/insee/tsconfig.lib.json" } ], "extends": "@tsconfig/node22/tsconfig.json" } From 21d47b6c00670b7bbea1ce1f59b96a91c59bbe7a Mon Sep 17 00:00:00 2001 From: Douglas Duteil Date: Tue, 7 Jan 2025 15:57:46 +0100 Subject: [PATCH 31/38] docs(changeset): identity minor bump (#913) --- .changeset/eighty-news-cry.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/eighty-news-cry.md diff --git a/.changeset/eighty-news-cry.md b/.changeset/eighty-news-cry.md new file mode 100644 index 000000000..ecee21e03 --- /dev/null +++ b/.changeset/eighty-news-cry.md @@ -0,0 +1,7 @@ +--- +"@gouvfr-lasuite/proconnect.identite": minor +--- + +♻️ Prélevement d'un partie du buisness proconnect identité + +Dans le cadres la migration du script d'import de comptes coop, une partie de l'API INSEE est déplacées dans le package `@gouvfr-lasuite/proconnect.identite` pour permettre leur réutilisation dans Hyyypertool. From 9fbb0e89d17c1652c2446f8bfc6e07ccab7b80c1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:05:47 +0100 Subject: [PATCH 32/38] =?UTF-8?q?=F0=9F=94=96=20Version=20Packages=20(#885?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- .changeset/eighty-news-cry.md | 7 ------- .changeset/fast-peas-sin.md | 7 ------- .changeset/hip-houses-confess.md | 7 ------- .changeset/selfish-apes-push.md | 5 ----- .changeset/smooth-waves-complain.md | 7 ------- package-lock.json | 16 +++++++++------- packages/core/CHANGELOG.md | 16 ++++++++++++++++ packages/core/package.json | 2 +- packages/identite/CHANGELOG.md | 9 +++++++++ packages/identite/package.json | 6 +++--- packages/insee/CHANGELOG.md | 9 +++++++++ packages/insee/package.json | 2 +- 12 files changed, 48 insertions(+), 45 deletions(-) delete mode 100644 .changeset/eighty-news-cry.md delete mode 100644 .changeset/fast-peas-sin.md delete mode 100644 .changeset/hip-houses-confess.md delete mode 100644 .changeset/selfish-apes-push.md delete mode 100644 .changeset/smooth-waves-complain.md create mode 100644 packages/identite/CHANGELOG.md create mode 100644 packages/insee/CHANGELOG.md diff --git a/.changeset/eighty-news-cry.md b/.changeset/eighty-news-cry.md deleted file mode 100644 index ecee21e03..000000000 --- a/.changeset/eighty-news-cry.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@gouvfr-lasuite/proconnect.identite": minor ---- - -♻️ Prélevement d'un partie du buisness proconnect identité - -Dans le cadres la migration du script d'import de comptes coop, une partie de l'API INSEE est déplacées dans le package `@gouvfr-lasuite/proconnect.identite` pour permettre leur réutilisation dans Hyyypertool. diff --git a/.changeset/fast-peas-sin.md b/.changeset/fast-peas-sin.md deleted file mode 100644 index 581a4e4b6..000000000 --- a/.changeset/fast-peas-sin.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@gouvfr-lasuite/proconnect.core": patch ---- - -♻️ restriction des exports de fichier - -Seul les index peuvent être importé. Cela permet de réduire les confusions d'auto-import dans la majorité des IDEs. diff --git a/.changeset/hip-houses-confess.md b/.changeset/hip-houses-confess.md deleted file mode 100644 index 2719a9877..000000000 --- a/.changeset/hip-houses-confess.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@gouvfr-lasuite/proconnect.insee": minor ---- - -♻️ Prélevement d'un partie du connecteur INSEE - -Dans le cadres la migration du script d'import de comptes coop, une partie de l'API INSEE est déplacées dans le package `@gouvfr-lasuite/proconnect.insee` pour permettre leur réutilisation dans Hyyypertool. diff --git a/.changeset/selfish-apes-push.md b/.changeset/selfish-apes-push.md deleted file mode 100644 index c9700ad26..000000000 --- a/.changeset/selfish-apes-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@gouvfr-lasuite/proconnect.core": patch ---- - -:recycle: force la compatibilité avec Node.js diff --git a/.changeset/smooth-waves-complain.md b/.changeset/smooth-waves-complain.md deleted file mode 100644 index 100acbb0f..000000000 --- a/.changeset/smooth-waves-complain.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@gouvfr-lasuite/proconnect.core": minor ---- - -👮 Accueillons l'équipe de sécurité de ProConnect - -Dans le cadres la migration du script d'import de comptes coop, une partie des fonctions de validation sont déplacées dans le package `@gouvfr-lasuite/proconnect.core/security` pour permettre leur réutilisation dans Hyyypertool. diff --git a/package-lock.json b/package-lock.json index 00a2a3fc1..ce0e4894b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,10 @@ "version": "1.0.0", "license": "MIT", "workspaces": [ - "packages/*" + "packages/core", + "packages/email", + "packages/insee", + "packages/identite" ], "dependencies": { "@dotenvx/dotenvx": "^1.19.2", @@ -10423,10 +10426,9 @@ }, "packages/core": { "name": "@gouvfr-lasuite/proconnect.core", - "version": "0.2.0", + "version": "0.3.0", "license": "MIT", "dependencies": { - "@types/lodash-es": "^4.17.12", "@zootools/email-spell-checker": "^1.12.0", "bcryptjs": "^2.4.3", "is-disposable-email-domain": "^1.0.7", @@ -10480,15 +10482,15 @@ }, "packages/identite": { "name": "@gouvfr-lasuite/proconnect.identite", - "version": "0.2.0", + "version": "0.3.0", "license": "MIT", "dependencies": { "sql-template-tag": "^5.2.1" }, "devDependencies": { "@electric-sql/pglite": "^0.2.15", - "@gouvfr-lasuite/proconnect.core": "^0.2.0", - "@gouvfr-lasuite/proconnect.insee": "^0.2.0", + "@gouvfr-lasuite/proconnect.core": "^0.3.0", + "@gouvfr-lasuite/proconnect.insee": "^0.3.0", "@tsconfig/node22": "^22.0.0", "@types/mocha": "^10.0.10", "@types/node": "^22.10.2", @@ -10519,7 +10521,7 @@ }, "packages/insee": { "name": "@gouvfr-lasuite/proconnect.insee", - "version": "0.2.0", + "version": "0.3.0", "license": "MIT", "dependencies": { "axios": "^1.7.7" diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 72bd36ebe..164884df6 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,21 @@ # @gouvfr-lasuite/moncomptepro.core +## 0.3.0 + +### Minor Changes + +- [#911](https://github.com/numerique-gouv/moncomptepro/pull/911) [`c406e75`](https://github.com/numerique-gouv/moncomptepro/commit/c406e7528fd74ee7efc49fb3dca7ddfa7cf32ddd) Thanks [@douglasduteil](https://github.com/douglasduteil)! - 👮 Accueillons l'équipe de sécurité de ProConnect + + Dans le cadres la migration du script d'import de comptes coop, une partie des fonctions de validation sont déplacées dans le package `@gouvfr-lasuite/proconnect.core/security` pour permettre leur réutilisation dans Hyyypertool. + +### Patch Changes + +- [#909](https://github.com/numerique-gouv/moncomptepro/pull/909) [`eaa069d`](https://github.com/numerique-gouv/moncomptepro/commit/eaa069dc8a19134bd2b30ba1a4c451dc6d13f2ec) Thanks [@douglasduteil](https://github.com/douglasduteil)! - ♻️ restriction des exports de fichier + + Seul les index peuvent être importé. Cela permet de réduire les confusions d'auto-import dans la majorité des IDEs. + +- [#879](https://github.com/numerique-gouv/moncomptepro/pull/879) [`7a1aca3`](https://github.com/numerique-gouv/moncomptepro/commit/7a1aca395ed260ad77bd764e160eda48a66c54f9) Thanks [@douglasduteil](https://github.com/douglasduteil)! - :recycle: force la compatibilité avec Node.js + ## 0.2.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index 85028bf7f..8679329ca 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@gouvfr-lasuite/proconnect.core", - "version": "0.2.0", + "version": "0.3.0", "homepage": "https://github.com/numerique-gouv/moncomptepro/tree/master/packages/core#readme", "bugs": "https://github.com/numerique-gouv/moncomptepro/issues", "repository": { diff --git a/packages/identite/CHANGELOG.md b/packages/identite/CHANGELOG.md new file mode 100644 index 000000000..cd6fd84d2 --- /dev/null +++ b/packages/identite/CHANGELOG.md @@ -0,0 +1,9 @@ +# @gouvfr-lasuite/proconnect.identite + +## 0.3.0 + +### Minor Changes + +- [#913](https://github.com/numerique-gouv/moncomptepro/pull/913) [`21d47b6`](https://github.com/numerique-gouv/moncomptepro/commit/21d47b6c00670b7bbea1ce1f59b96a91c59bbe7a) Thanks [@douglasduteil](https://github.com/douglasduteil)! - ♻️ Prélevement d'un partie du buisness proconnect identité + + Dans le cadres la migration du script d'import de comptes coop, une partie de l'API INSEE est déplacées dans le package `@gouvfr-lasuite/proconnect.identite` pour permettre leur réutilisation dans Hyyypertool. diff --git a/packages/identite/package.json b/packages/identite/package.json index 75465a207..9fe38a61e 100644 --- a/packages/identite/package.json +++ b/packages/identite/package.json @@ -1,6 +1,6 @@ { "name": "@gouvfr-lasuite/proconnect.identite", - "version": "0.2.0", + "version": "0.3.0", "homepage": "https://github.com/numerique-gouv/moncomptepro/tree/master/packages/identite#readme", "bugs": "https://github.com/numerique-gouv/moncomptepro/issues", "repository": { @@ -50,8 +50,8 @@ }, "devDependencies": { "@electric-sql/pglite": "^0.2.15", - "@gouvfr-lasuite/proconnect.core": "^0.2.0", - "@gouvfr-lasuite/proconnect.insee": "^0.2.0", + "@gouvfr-lasuite/proconnect.core": "^0.3.0", + "@gouvfr-lasuite/proconnect.insee": "^0.3.0", "@tsconfig/node22": "^22.0.0", "@types/mocha": "^10.0.10", "@types/node": "^22.10.2", diff --git a/packages/insee/CHANGELOG.md b/packages/insee/CHANGELOG.md new file mode 100644 index 000000000..6431a0d90 --- /dev/null +++ b/packages/insee/CHANGELOG.md @@ -0,0 +1,9 @@ +# @gouvfr-lasuite/proconnect.insee + +## 0.3.0 + +### Minor Changes + +- [#912](https://github.com/numerique-gouv/moncomptepro/pull/912) [`ebd23fb`](https://github.com/numerique-gouv/moncomptepro/commit/ebd23fbda12f054b5b07bf7a75fd838ba9a0638b) Thanks [@douglasduteil](https://github.com/douglasduteil)! - ♻️ Prélevement d'un partie du connecteur INSEE + + Dans le cadres la migration du script d'import de comptes coop, une partie de l'API INSEE est déplacées dans le package `@gouvfr-lasuite/proconnect.insee` pour permettre leur réutilisation dans Hyyypertool. diff --git a/packages/insee/package.json b/packages/insee/package.json index c9e4a9ff2..5808617bc 100644 --- a/packages/insee/package.json +++ b/packages/insee/package.json @@ -1,6 +1,6 @@ { "name": "@gouvfr-lasuite/proconnect.insee", - "version": "0.2.0", + "version": "0.3.0", "homepage": "https://github.com/numerique-gouv/moncomptepro/tree/master/packages/insee#readme", "bugs": "https://github.com/numerique-gouv/moncomptepro/issues", "repository": { From 140b9a3e67f64e997d63c59ae64f3a61dc25ce64 Mon Sep 17 00:00:00 2001 From: Douglas Duteil Date: Tue, 7 Jan 2025 16:10:50 +0100 Subject: [PATCH 33/38] docs(CHANGELOG): copy/paste typo (#914) --- packages/identite/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/identite/CHANGELOG.md b/packages/identite/CHANGELOG.md index cd6fd84d2..5a31e27a9 100644 --- a/packages/identite/CHANGELOG.md +++ b/packages/identite/CHANGELOG.md @@ -6,4 +6,4 @@ - [#913](https://github.com/numerique-gouv/moncomptepro/pull/913) [`21d47b6`](https://github.com/numerique-gouv/moncomptepro/commit/21d47b6c00670b7bbea1ce1f59b96a91c59bbe7a) Thanks [@douglasduteil](https://github.com/douglasduteil)! - ♻️ Prélevement d'un partie du buisness proconnect identité - Dans le cadres la migration du script d'import de comptes coop, une partie de l'API INSEE est déplacées dans le package `@gouvfr-lasuite/proconnect.identite` pour permettre leur réutilisation dans Hyyypertool. + Dans le cadres la migration du script d'import de comptes coop, une partie de du buisness proconnect est déplacées dans le package `@gouvfr-lasuite/proconnect.identite` pour permettre leur réutilisation dans Hyyypertool. From b83588a74f5bf3978c3b76de421e425b581151b6 Mon Sep 17 00:00:00 2001 From: Douglas DUTEIL Date: Wed, 8 Jan 2025 16:00:23 +0100 Subject: [PATCH 34/38] docs(README): add internal dependencies build script --- cypress/README.md | 2 +- installation.md | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cypress/README.md b/cypress/README.md index 3458e4005..c6e4e25a5 100644 --- a/cypress/README.md +++ b/cypress/README.md @@ -19,7 +19,7 @@ FEATURE_SEND_MAIL=True Note that this will delete your database. Load the specific fixtures in the database: ```bash -ENABLE_DATABASE_DELETION=True npm run delete-database ; npx run-s "migrate up" "fixtures:load-ci cypress/e2e/redirect_after_session_expiration/fixtures.sql" "update-organization-info 2000" +ENABLE_DATABASE_DELETION=True npm run delete-database ; npx run-s "build:workspaces" "migrate up" "fixtures:load-ci cypress/e2e/redirect_after_session_expiration/fixtures.sql" "update-organization-info 2000" ``` ### Start ProConnect Identité with the test configuration diff --git a/installation.md b/installation.md index b42926a19..49844965a 100644 --- a/installation.md +++ b/installation.md @@ -42,7 +42,13 @@ This guide provides steps to run the ProConnect Identité Node.js application lo Then fill your `.env` file with them. -4. **Database Initialization**: The database will be automatically initialized with data from `scripts/fixtures.sql`. +4. **Build internal dependencies**: Build the internal dependencies located in the `packages` directory. + + ```bash + npm run build:workspaces + ``` + +5. **Database Initialization**: The database will be automatically initialized with data from `scripts/fixtures.sql`. ```bash npm run fixtures:load From 1421a6c615948c008117276d661a6f79e8af8e0c Mon Sep 17 00:00:00 2001 From: Douglas Duteil Date: Wed, 8 Jan 2025 16:04:47 +0100 Subject: [PATCH 35/38] fix(pkg): ensure to build before migrate (#917) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d36addbed..ffee20ef5 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "build": "run-s build:**", "build:assets": "vite build --clearScreen false", "build:db:conditionaly-load-fixtures": "if [ \"$RUN_FIXTURES\" = \"True\" ]; then npm run fixtures:load; fi", - "build:db:migrate": "npm run --silent migrate -- up", "build:workspaces": "npm run build --if-present --workspaces", + "build:db:migrate": "npm run --silent migrate -- up", "delete-database": "if [ \"$ENABLE_DATABASE_DELETION\" = \"True\" ]; then tsx scripts/run-sql-script.ts scripts/delete_all.sql; fi", "predev": "npm run build:workspaces", "dev": "NODE_ENV=development concurrently \"npm:watch:*\"", From 001aecf1658f0704791dc02611e2a21d5b573320 Mon Sep 17 00:00:00 2001 From: Douglas Duteil Date: Wed, 8 Jan 2025 16:07:20 +0100 Subject: [PATCH 36/38] fix(pkg): ensure to build before fixture load (#918) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ffee20ef5..36e66c57b 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "scripts": { "build": "run-s build:**", "build:assets": "vite build --clearScreen false", - "build:db:conditionaly-load-fixtures": "if [ \"$RUN_FIXTURES\" = \"True\" ]; then npm run fixtures:load; fi", "build:workspaces": "npm run build --if-present --workspaces", + "build:db:conditionaly-load-fixtures": "if [ \"$RUN_FIXTURES\" = \"True\" ]; then npm run fixtures:load; fi", "build:db:migrate": "npm run --silent migrate -- up", "delete-database": "if [ \"$ENABLE_DATABASE_DELETION\" = \"True\" ]; then tsx scripts/run-sql-script.ts scripts/delete_all.sql; fi", "predev": "npm run build:workspaces", From 79ffeeb822de10497298584ab8d4d024ff188fda Mon Sep 17 00:00:00 2001 From: rebeccadumazert Date: Wed, 8 Jan 2025 16:02:44 +0100 Subject: [PATCH 37/38] fix(html): replace h3 with p balise --- cypress/e2e/signup_entreprise_unipersonnelle/index.cy.ts | 2 +- src/views/user/verify-email.ejs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/signup_entreprise_unipersonnelle/index.cy.ts b/cypress/e2e/signup_entreprise_unipersonnelle/index.cy.ts index c85353da9..a1edb12d8 100644 --- a/cypress/e2e/signup_entreprise_unipersonnelle/index.cy.ts +++ b/cypress/e2e/signup_entreprise_unipersonnelle/index.cy.ts @@ -15,7 +15,7 @@ describe("Signup into new entreprise unipersonnelle", () => { cy.get('[action="/users/sign-up"] [type="submit"]').click(); // Check that the website is waiting for the user to verify their email - cy.get("#verify-email > div > h3").contains( + cy.get("#verify-email > div > p").contains( "lion.eljonson@darkangels.world", ); diff --git a/src/views/user/verify-email.ejs b/src/views/user/verify-email.ejs index 7311a1668..cbb542160 100644 --- a/src/views/user/verify-email.ejs +++ b/src/views/user/verify-email.ejs @@ -14,8 +14,8 @@
<% if (codeSent || newCodeSent) { %>
-

Un <% if (newCodeSent) { %>nouveau <% } %>code de vérification a été envoyé à <%= email; %>. -

+

Un <% if (newCodeSent) { %>nouveau <% } %>code de vérification a été envoyé à <%= email; %>. +

<% } %> From 91e77fb25d7b9cc09153b52cf10321c55e37de4b Mon Sep 17 00:00:00 2001 From: Douglas Duteil Date: Wed, 8 Jan 2025 18:23:53 +0100 Subject: [PATCH 38/38] deploy: strange dependencies stuff (#919) --- package-lock.json | 305 ++++++++++++++++++++++++++++++++++++++++------ package.json | 43 ++++--- 2 files changed, 290 insertions(+), 58 deletions(-) diff --git a/package-lock.json b/package-lock.json index ce0e4894b..4054e171d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,32 +15,17 @@ "packages/identite" ], "dependencies": { - "@dotenvx/dotenvx": "^1.19.2", - "@fullhuman/postcss-purgecss": "^6.0.0", "@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", - "@panva/jose": "^1.9.3", "@sentry/node": "^7.120.0", "@simplewebauthn/browser": "^8.3.4", "@simplewebauthn/server": "^8.3.5", - "@simplewebauthn/typescript-types": "^8.3.4", "@sunknudsen/totp": "^1.1.0", - "@tsconfig/node22": "^22.0.0", - "@types/console-log-level": "^1.4.5", - "@types/cookie-parser": "^1.4.7", - "@types/ejs": "^3.1.5", - "@types/express": "^4.17.21", - "@types/express-session": "^1.18.0", - "@types/morgan": "^1.9.9", "await-to-js": "^3.0.0", "axios": "^1.7.7", - "body-parser": "^1.20.3", "connect-redis": "^7.1.1", "console-log-level": "^1.4.1", "cookie-parser": "^1.4.7", @@ -54,38 +39,46 @@ "html-to-text": "^9.0.5", "http-errors": "^2.0.0", "ioredis": "^5.4.1", - "lodash": "^4.17.21", "lodash-es": "^4.17.21", "moment": "^2.30.1", "moment-timezone": "^0.5.45", "morgan": "^1.10.0", "nocache": "^4.0.0", - "node-pg-migrate": "^7.6.1", "nodemailer": "^6.9.15", - "npm-run-all2": "^6.1.2", "oidc-provider": "^8.5.1", "pg": "^8.13.0", "qrcode": "^1.5.4", "rate-limiter-flexible": "^2.4.2", - "tld-extract": "^2.1.0", "tsx": "^4.19.2", - "typescript": "^5.7.2", - "vite": "^5.2.14", "zod": "^3.23.8", "zod-validation-error": "^3.3.1" }, "devDependencies": { "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.27.10", + "@dotenvx/dotenvx": "^1.19.2", + "@fullhuman/postcss-purgecss": "^6.0.0", + "@gouvfr/dsfr": "^1.12.1", + "@kitajs/html": "^4.2.4", + "@kitajs/ts-html-plugin": "^4.1.0", + "@panva/jose": "^1.9.3", "@simplewebauthn/types": "^10.0.0", + "@simplewebauthn/typescript-types": "^8.3.4", "@sinonjs/fake-timers": "^11.2.2", + "@tsconfig/node22": "^22.0.0", "@types/chai": "^5.0.1", "@types/chai-as-promised": "^7.1.8", + "@types/console-log-level": "^1.4.5", + "@types/cookie-parser": "^1.4.7", + "@types/ejs": "^3.1.5", + "@types/express": "^4.17.21", + "@types/express-session": "^1.18.0", "@types/html-to-text": "^9.0.4", "@types/http-errors": "^2.0.4", "@types/lodash": "^4.17.10", "@types/lodash-es": "^4.17.12", "@types/mocha": "^10.0.10", + "@types/morgan": "^1.9.9", "@types/node": "^22.10.2", "@types/nodemailer": "^6.4.16", "@types/oidc-provider": "^8.5.2", @@ -100,10 +93,16 @@ "cypress": "^13.17.0", "cypress-axe": "^1.5.0", "cypress-maildev": "^1.3.2", + "lodash": "^4.17.21", "mocha": "^11.0.1", "nock": "^13.5.6", + "node-pg-migrate": "^7.6.1", + "npm-run-all2": "^6.1.2", "prettier": "^3.4.2", - "prettier-plugin-organize-imports": "^4.1.0" + "prettier-plugin-organize-imports": "^4.1.0", + "tld-extract": "^2.1.0", + "typescript": "^5.7.2", + "vite": "^5.2.14" }, "engines": { "node": "^22.12.0" @@ -777,6 +776,7 @@ "version": "1.19.2", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.19.2.tgz", "integrity": "sha512-xvGMjUJLiV5ijmfaK4+mDFdAKLGyYSPJ8inWPtRqq6aiH8pBhys7qN2xGfEkcs3XpwkM4XTg9PkyXWE7Cusahg==", + "dev": true, "dependencies": { "commander": "^11.1.0", "dotenv": "^16.4.5", @@ -800,6 +800,7 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, "engines": { "node": ">=16" } @@ -808,6 +809,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -830,6 +832,7 @@ "version": "6.4.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz", "integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==", + "dev": true, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -843,6 +846,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, "engines": { "node": ">=10.17.0" } @@ -851,6 +855,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, "engines": { "node": ">=16" } @@ -859,6 +864,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, "engines": { "node": ">=12" }, @@ -870,6 +876,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, "dependencies": { "isexe": "^3.1.1" }, @@ -1275,6 +1282,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-6.0.0.tgz", "integrity": "sha512-sUvk5PV7O5xvTJcxDYrQ00xlKtSxivvJdZrwgxE8F1GmNMs7w9U+dSbr83N/qEs9b+f+6QsZKXDs0k8nMjBIqA==", + "dev": true, "license": "MIT", "dependencies": { "purgecss": "^6.0.0" @@ -1310,6 +1318,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@gouvfr/dsfr/-/dsfr-1.12.1.tgz", "integrity": "sha512-2zXBHSDjjlaNcxGtqbPQN/vyjJQKIRUP3CKOYoodj2/45pAWM1AXo3kCwXFLUo5kQOltShZUquQ7hSeVg4RRtA==", + "dev": true, "engines": { "node": ">=18.16.1" }, @@ -1348,6 +1357,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -1364,6 +1374,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, "engines": { "node": ">=12" }, @@ -1375,6 +1386,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, "engines": { "node": ">=12" }, @@ -1385,12 +1397,14 @@ "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -1407,6 +1421,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1421,6 +1436,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -1695,6 +1711,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.0.0.tgz", "integrity": "sha512-wH5EHOmLi0rEazphPbecAzmjd12I6/Yv/SiHdkA9LSycsQk7RuuTp7am5/o62qYr0RScE7Pc9icXGBbsr6cesA==", + "dev": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -1706,6 +1723,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz", "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", + "dev": true, "dependencies": { "@noble/hashes": "1.5.0" }, @@ -1720,6 +1738,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "dev": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -1769,6 +1788,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==", + "dev": true, "engines": { "node": ">=10.13.0" } @@ -1778,6 +1798,7 @@ "resolved": "https://registry.npmjs.org/@panva/jose/-/jose-1.9.3.tgz", "integrity": "sha512-oKzjM5YsCSaL+/7NDQhfqeu0xj7owqRduNLM/W+dtIyGI7/AIaPue/bz4fqjYVDTpmRtrlsserR6kiqJucmbjA==", "deprecated": "@panva/jose has moved. It is now called just \"jose\", no further updates will be made under the @panva/jose package name.", + "dev": true, "dependencies": { "jose": "^1.15.0" }, @@ -1789,6 +1810,7 @@ "version": "1.28.2", "resolved": "https://registry.npmjs.org/jose/-/jose-1.28.2.tgz", "integrity": "sha512-wWy51U2MXxYi3g8zk2lsQ8M6O1lartpkxuq1TYexzPKYLgHLZkCjklaATP36I5BUoWjF2sInB9U1Qf18fBZxNA==", + "dev": true, "dependencies": { "@panva/asn1.js": "^1.0.0" }, @@ -1865,6 +1887,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "optional": true, "engines": { "node": ">=14" @@ -1877,6 +1900,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1890,6 +1914,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1903,6 +1928,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1916,6 +1942,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1929,6 +1956,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1942,6 +1970,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1955,6 +1984,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1968,6 +1998,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1981,6 +2012,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1994,6 +2026,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2007,6 +2040,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2020,6 +2054,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2033,6 +2068,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2046,6 +2082,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2059,6 +2096,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2072,6 +2110,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2342,6 +2381,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true, "peer": true }, "node_modules/@storybook/csf": { @@ -2387,6 +2427,7 @@ "version": "22.0.0", "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.0.tgz", "integrity": "sha512-twLQ77zevtxobBOD4ToAtVmuYrpeYUh3qh+TEp+08IWhpsrIflVHqQ1F1CiPxQGL7doCdBIOOCF+1Tm833faNg==", + "dev": true, "license": "MIT" }, "node_modules/@types/accepts": { @@ -2419,6 +2460,7 @@ "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -2447,6 +2489,7 @@ "version": "3.4.36", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -2455,7 +2498,8 @@ "node_modules/@types/console-log-level": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@types/console-log-level/-/console-log-level-1.4.5.tgz", - "integrity": "sha512-ANoa0gMtzWhKKMYbBt+NM11VqbuwJwpMEkvuZTJ1JcQ7C6Qw/yjV5R4yPkOPjFAULcvX8wenC9BnI5K9jExmiw==" + "integrity": "sha512-ANoa0gMtzWhKKMYbBt+NM11VqbuwJwpMEkvuZTJ1JcQ7C6Qw/yjV5R4yPkOPjFAULcvX8wenC9BnI5K9jExmiw==", + "dev": true }, "node_modules/@types/content-disposition": { "version": "0.5.5", @@ -2467,6 +2511,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true, "license": "MIT", "peer": true }, @@ -2474,6 +2519,7 @@ "version": "1.4.7", "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.7.tgz", "integrity": "sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==", + "dev": true, "dependencies": { "@types/express": "*" } @@ -2494,6 +2540,7 @@ "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -2510,17 +2557,20 @@ "node_modules/@types/ejs": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", - "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==" + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -2532,6 +2582,7 @@ "version": "4.17.33", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", + "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -2542,6 +2593,7 @@ "version": "1.18.0", "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.0.tgz", "integrity": "sha512-27JdDRgor6PoYlURY+Y5kCakqp5ulC0kmf7y+QwaY+hv9jEFuQOThgkjyA53RP3jmKuBsH5GR6qEfFmvb8mwOA==", + "dev": true, "dependencies": { "@types/express": "*" } @@ -2620,7 +2672,8 @@ "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "dev": true }, "node_modules/@types/mocha": { "version": "10.0.10", @@ -2633,6 +2686,7 @@ "version": "1.9.9", "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -2641,6 +2695,7 @@ "version": "22.10.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -2671,6 +2726,7 @@ "version": "8.6.1", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==", + "dev": true, "optional": true, "peer": true, "dependencies": { @@ -2691,17 +2747,20 @@ "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true }, "node_modules/@types/range-parser": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true }, "node_modules/@types/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "dev": true, "dependencies": { "@types/mime": "*", "@types/node": "*" @@ -2832,6 +2891,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -2942,6 +3002,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz", "integrity": "sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==", + "dev": true, "peer": true, "engines": { "node": ">=0.8.0" @@ -3041,6 +3102,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, "license": "MIT", "peer": true, "engines": { @@ -3062,6 +3124,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, "peer": true }, "node_modules/bcrypt-pbkdf": { @@ -3097,6 +3160,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "dev": true, "engines": { "node": ">=8" } @@ -3161,6 +3225,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -3178,6 +3243,7 @@ "version": "2.29.3", "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.29.3.tgz", "integrity": "sha512-NiM38O6XU84+MN+gzspVmXV2fTOoe+jBqIBx3IBdhZrdeURr6ZgznJr/p+hQ+KzkKEiGH/GcC4SQFSL0jV49bg==", + "dev": true, "peer": true, "dependencies": { "browser-sync-client": "^2.29.3", @@ -3221,6 +3287,7 @@ "version": "2.29.3", "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.29.3.tgz", "integrity": "sha512-4tK5JKCl7v/3aLbmCBMzpufiYLsB1+UI+7tUXCCp5qF0AllHy/jAqYu6k7hUF3hYtlClKpxExWaR+rH+ny07wQ==", + "dev": true, "peer": true, "dependencies": { "etag": "1.8.1", @@ -3235,6 +3302,7 @@ "version": "2.29.3", "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.29.3.tgz", "integrity": "sha512-kBYOIQjU/D/3kYtUIJtj82e797Egk1FB2broqItkr3i4eF1qiHbFCG6srksu9gWhfmuM/TNG76jMfzAdxEPakg==", + "dev": true, "peer": true, "dependencies": { "async-each-series": "0.1.1", @@ -3250,6 +3318,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, "peer": true, "engines": { "node": ">= 0.6" @@ -3259,12 +3328,14 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", + "dev": true, "peer": true }, "node_modules/browser-sync/node_modules/fs-extra": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", "integrity": "sha512-V3Z3WZWVUYd8hoCL5xfXJCaHWYzmtwW5XWYSlLgERi8PWd8bx1kUHUk8L1BT57e49oKnDDD180mjfrHc1yA9rg==", + "dev": true, "peer": true, "dependencies": { "graceful-fs": "^4.1.2", @@ -3276,6 +3347,7 @@ "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, "peer": true, "dependencies": { "depd": "~1.1.2", @@ -3291,12 +3363,14 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, "peer": true }, "node_modules/browser-sync/node_modules/jsonfile": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", "integrity": "sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==", + "dev": true, "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" @@ -3306,6 +3380,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "dev": true, "peer": true, "bin": { "mime": "cli.js" @@ -3315,6 +3390,7 @@ "version": "0.16.2", "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "dev": true, "peer": true, "dependencies": { "debug": "2.6.9", @@ -3339,6 +3415,7 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "dev": true, "peer": true, "dependencies": { "encodeurl": "~1.0.2", @@ -3354,12 +3431,14 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, "peer": true }, "node_modules/browser-sync/node_modules/statuses": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "dev": true, "peer": true, "engines": { "node": ">= 0.6" @@ -3369,6 +3448,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, "peer": true, "engines": { "node": ">= 4.0.0" @@ -3378,6 +3458,7 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz", "integrity": "sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==", + "dev": true, "peer": true }, "node_modules/buffer": { @@ -3619,6 +3700,7 @@ "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, "funding": [ { "type": "individual", @@ -3713,6 +3795,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -3782,6 +3865,7 @@ "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -3831,6 +3915,7 @@ "version": "3.6.6", "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", "integrity": "sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ==", + "dev": true, "peer": true, "dependencies": { "debug": "2.6.9", @@ -3846,6 +3931,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true, "peer": true, "engines": { "node": ">=0.8" @@ -3866,6 +3952,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", "integrity": "sha512-ejnvM9ZXYzp6PUPUyQBMBf0Co5VX2gr5H2VQe2Ui2jWXNlxv+PYZo8wpAymJNJdLsG1R4p+M4aynF8KuoUEwRw==", + "dev": true, "peer": true, "dependencies": { "debug": "2.6.9", @@ -3884,6 +3971,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", "integrity": "sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg==", + "dev": true, "peer": true, "engines": { "node": ">= 0.6" @@ -4057,6 +4145,7 @@ "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -4071,6 +4160,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -4093,6 +4183,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -4428,6 +4519,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", "integrity": "sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==", + "dev": true, "peer": true, "bin": { "dev-ip": "lib/dev-ip.js" @@ -4532,12 +4624,14 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true }, "node_modules/easy-extender": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz", "integrity": "sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==", + "dev": true, "peer": true, "dependencies": { "lodash": "^4.17.10" @@ -4550,6 +4644,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.0.1.tgz", "integrity": "sha512-2GSFtnnC6U4IEKhEI7+PvdxrmjJ04mdsj3wHZTFiw0tUtG4HCWzTr13ZYTk8XOGnA1xQMaDljoBOYlk3D/MMSw==", + "dev": true, "peer": true, "dependencies": { "chalk": "4.1.2" @@ -4573,6 +4668,7 @@ "version": "0.4.8", "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.8.tgz", "integrity": "sha512-U2wAn6yEOVBP9lOVh3nryufg3hQTKVicG+qjEfqB/70m/mU9DzwWNdK0mC5zuxlJH42EGAezFlHVWI0snwg1nw==", + "dev": true, "dependencies": { "@noble/ciphers": "^1.0.0", "@noble/curves": "^1.6.0", @@ -4627,6 +4723,7 @@ "version": "6.6.2", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -4649,6 +4746,7 @@ "version": "6.5.4", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "dev": true, "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", @@ -4662,6 +4760,7 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, "peer": true, "dependencies": { "ms": "2.1.2" @@ -4679,12 +4778,14 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, "peer": true }, "node_modules/engine.io-parser": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, "peer": true, "engines": { "node": ">=10.0.0" @@ -4694,6 +4795,7 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -4712,6 +4814,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT", "peer": true }, @@ -4867,6 +4970,7 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, "peer": true }, "node_modules/execa": { @@ -5269,6 +5373,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -5372,6 +5477,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -5388,6 +5494,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "engines": { "node": ">=14" }, @@ -5586,6 +5693,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -5667,7 +5775,8 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true }, "node_modules/has-flag": { "version": "4.0.0", @@ -5855,6 +5964,7 @@ "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, "peer": true, "dependencies": { "eventemitter3": "^4.0.0", @@ -5954,6 +6064,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, "engines": { "node": ">= 4" } @@ -5968,6 +6079,7 @@ "version": "3.8.2", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", + "dev": true, "peer": true, "engines": { "node": ">=0.10.0" @@ -6062,6 +6174,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -6081,6 +6194,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -6111,6 +6225,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -6138,6 +6253,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "engines": { "node": ">=0.12.0" } @@ -6146,6 +6262,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", + "dev": true, "peer": true, "dependencies": { "lodash.isfinite": "^3.3.2" @@ -6173,6 +6290,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, "engines": { "node": ">=8" }, @@ -6225,7 +6343,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/isstream": { "version": "0.1.2", @@ -6238,6 +6357,7 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", + "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -6330,6 +6450,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", + "dev": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -6519,6 +6640,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", + "dev": true, "peer": true }, "node_modules/listr2": { @@ -6561,6 +6683,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.2.tgz", "integrity": "sha512-n418Cn5ynvJd7m/N1d9WVJISLJF/ellZnfsLnx8WBWGzxv/ntNcFkJ1o6se5quUhCplfLGBNL5tYHiq5WF3Nug==", + "dev": true, "peer": true, "dependencies": { "axios": "0.21.4", @@ -6579,6 +6702,7 @@ "version": "0.21.4", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dev": true, "peer": true, "dependencies": { "follow-redirects": "^1.14.0" @@ -6588,6 +6712,7 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, "peer": true, "dependencies": { "ms": "2.1.2" @@ -6605,12 +6730,14 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, "peer": true }, "node_modules/localtunnel/node_modules/yargs": { "version": "17.1.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.1.1.tgz", "integrity": "sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ==", + "dev": true, "peer": true, "dependencies": { "cliui": "^7.0.2", @@ -6629,6 +6756,7 @@ "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, "peer": true, "engines": { "node": ">=10" @@ -6652,7 +6780,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash-es": { "version": "4.17.21", @@ -6673,6 +6802,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", "integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==", + "dev": true, "peer": true }, "node_modules/lodash.once": { @@ -6798,6 +6928,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, "engines": { "node": ">= 0.10.0" } @@ -6814,7 +6945,8 @@ "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true }, "node_modules/merge2": { "version": "1.4.1", @@ -6838,6 +6970,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -6882,6 +7015,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, "engines": { "node": ">=6" } @@ -6921,6 +7055,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -6929,6 +7064,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz", "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", + "dev": true, "peer": true }, "node_modules/mocha": { @@ -7139,6 +7275,7 @@ "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, "funding": [ { "type": "github", @@ -7240,6 +7377,7 @@ "version": "7.6.1", "resolved": "https://registry.npmjs.org/node-pg-migrate/-/node-pg-migrate-7.6.1.tgz", "integrity": "sha512-CUocdo8kh25QZU2MeCjss2/OQ/Jq8ejCeilpja1MVOgk2c03r4U7vux9fAZCfrVg0YASwnhnpjz+rSK4tAVB2w==", + "dev": true, "license": "MIT", "dependencies": { "yargs": "~17.7.0" @@ -7275,6 +7413,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -7294,6 +7433,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -7302,6 +7442,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-6.1.2.tgz", "integrity": "sha512-WwwnS8Ft+RpXve6T2EIEVpFLSqN+ORHRvgNk3H9N62SZXjmzKoRhMFg3I17TK3oMaAEr+XFbRirWS2Fn3BCPSg==", + "dev": true, "dependencies": { "ansi-styles": "^6.2.1", "cross-spawn": "^7.0.3", @@ -7326,6 +7467,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, "engines": { "node": ">=12" }, @@ -7337,6 +7479,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -7345,6 +7488,7 @@ "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -7359,6 +7503,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, "dependencies": { "path-key": "^3.0.0" }, @@ -7370,6 +7515,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "peer": true, "engines": { @@ -7396,6 +7542,7 @@ "version": "1.1.33", "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", + "dev": true, "engines": { "node": ">= 10" } @@ -7504,6 +7651,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "dependencies": { "mimic-fn": "^2.1.0" }, @@ -7523,12 +7671,14 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz", "integrity": "sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==", + "dev": true, "peer": true }, "node_modules/opn": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", + "dev": true, "peer": true, "dependencies": { "is-wsl": "^1.1.0" @@ -7541,6 +7691,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, "peer": true, "engines": { "node": ">=4" @@ -7656,7 +7807,8 @@ "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true }, "node_modules/package-manager-detector": { "version": "0.2.4", @@ -7707,6 +7859,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "engines": { "node": ">=8" } @@ -7715,6 +7868,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -7730,6 +7884,7 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==", + "dev": true, "engines": { "node": "14 || >=16.14" } @@ -7871,12 +8026,14 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -7888,6 +8045,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, "bin": { "pidtree": "bin/pidtree.js" }, @@ -7916,6 +8074,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", "integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==", + "dev": true, "peer": true, "dependencies": { "async": "^2.6.0", @@ -7930,6 +8089,7 @@ "version": "2.6.4", "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, "peer": true, "dependencies": { "lodash": "^4.17.14" @@ -7939,6 +8099,7 @@ "version": "8.4.47", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -7967,6 +8128,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -8111,6 +8273,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-6.0.0.tgz", "integrity": "sha512-s3EBxg5RSWmpqd0KGzNqPiaBbWDz1/As+2MzoYVGMqgDqRTLBhJW6sywfTBek7OwNfoS/6pS0xdtvChNhFj2cw==", + "dev": true, "license": "MIT", "dependencies": { "commander": "^12.0.0", @@ -8126,6 +8289,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -8135,6 +8299,7 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -8155,6 +8320,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -8412,6 +8578,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, "dependencies": { "json-parse-even-better-errors": "^3.0.0", "npm-normalize-package-bin": "^3.0.0" @@ -8474,6 +8641,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -8533,6 +8701,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, "peer": true }, "node_modules/resolve-alpn": { @@ -8563,6 +8732,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", "integrity": "sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw==", + "dev": true, "peer": true, "dependencies": { "debug": "^2.2.0", @@ -8620,6 +8790,7 @@ "version": "4.22.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.5" @@ -8679,6 +8850,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", "integrity": "sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==", + "dev": true, "peer": true }, "node_modules/rxjs": { @@ -8792,6 +8964,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, "peer": true, "dependencies": { "accepts": "~1.3.4", @@ -8810,6 +8983,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, "peer": true, "engines": { "node": ">= 0.6" @@ -8819,6 +8993,7 @@ "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, "peer": true, "dependencies": { "depd": "~1.1.2", @@ -8834,12 +9009,14 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, "peer": true }, "node_modules/serve-index/node_modules/setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, "peer": true }, "node_modules/serve-static": { @@ -8870,6 +9047,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "dev": true, "peer": true }, "node_modules/set-blocking": { @@ -8902,6 +9080,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -8913,6 +9092,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "engines": { "node": ">=8" } @@ -8921,6 +9101,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8945,7 +9126,8 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true }, "node_modules/slash": { "version": "3.0.0", @@ -8975,6 +9157,7 @@ "version": "4.8.0", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -8994,6 +9177,7 @@ "version": "2.5.5", "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, "peer": true, "dependencies": { "debug": "~4.3.4", @@ -9004,6 +9188,7 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, "peer": true, "dependencies": { "ms": "2.1.2" @@ -9021,12 +9206,14 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, "peer": true }, "node_modules/socket.io-client": { "version": "4.7.5", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "dev": true, "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", @@ -9042,6 +9229,7 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, "peer": true, "dependencies": { "ms": "2.1.2" @@ -9059,12 +9247,14 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, "peer": true }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", @@ -9078,6 +9268,7 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, "peer": true, "dependencies": { "ms": "2.1.2" @@ -9095,12 +9286,14 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, "peer": true }, "node_modules/socket.io/node_modules/debug": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, "peer": true, "dependencies": { "ms": "2.1.2" @@ -9118,12 +9311,14 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, "peer": true }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -9220,6 +9415,7 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", "integrity": "sha512-889+B9vN9dq7/vLbGyuHeZ6/ctf5sNuGWsDy89uNxkFTAgzy0eK7+w5fL3KLNRTkLle7EgZGvHUphZW0Q26MnQ==", + "dev": true, "peer": true, "dependencies": { "commander": "^2.2.0", @@ -9236,6 +9432,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, "peer": true }, "node_modules/stream-transform": { @@ -9262,6 +9459,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -9287,6 +9485,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -9308,6 +9507,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, "engines": { "node": ">=6" } @@ -9417,6 +9617,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -9581,6 +9782,7 @@ "version": "1.0.38", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz", "integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -9615,6 +9817,7 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, "license": "MIT" }, "node_modules/universalify": { @@ -9647,6 +9850,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -9694,6 +9898,7 @@ "version": "5.4.9", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz", "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==", + "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", @@ -9756,6 +9961,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9772,6 +9978,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9788,6 +9995,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9804,6 +10012,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9820,6 +10029,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9836,6 +10046,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9852,6 +10063,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9868,6 +10080,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9884,6 +10097,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9900,6 +10114,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9916,6 +10131,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9932,6 +10148,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9948,6 +10165,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9964,6 +10182,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9980,6 +10199,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9996,6 +10216,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10012,6 +10233,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10028,6 +10250,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10044,6 +10267,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10060,6 +10284,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10076,6 +10301,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10092,6 +10318,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10108,6 +10335,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10121,6 +10349,7 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -10193,6 +10422,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -10236,6 +10466,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -10258,6 +10489,7 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, "peer": true, "engines": { "node": ">=10.0.0" @@ -10279,6 +10511,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "dev": true, "peer": true, "engines": { "node": ">=0.4.0" diff --git a/package.json b/package.json index 36e66c57b..8bbdcdfc8 100644 --- a/package.json +++ b/package.json @@ -51,32 +51,17 @@ ] }, "dependencies": { - "@dotenvx/dotenvx": "^1.19.2", - "@fullhuman/postcss-purgecss": "^6.0.0", "@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", - "@panva/jose": "^1.9.3", "@sentry/node": "^7.120.0", "@simplewebauthn/browser": "^8.3.4", "@simplewebauthn/server": "^8.3.5", - "@simplewebauthn/typescript-types": "^8.3.4", "@sunknudsen/totp": "^1.1.0", - "@tsconfig/node22": "^22.0.0", - "@types/console-log-level": "^1.4.5", - "@types/cookie-parser": "^1.4.7", - "@types/ejs": "^3.1.5", - "@types/express": "^4.17.21", - "@types/express-session": "^1.18.0", - "@types/morgan": "^1.9.9", "await-to-js": "^3.0.0", "axios": "^1.7.7", - "body-parser": "^1.20.3", "connect-redis": "^7.1.1", "console-log-level": "^1.4.1", "cookie-parser": "^1.4.7", @@ -90,38 +75,46 @@ "html-to-text": "^9.0.5", "http-errors": "^2.0.0", "ioredis": "^5.4.1", - "lodash": "^4.17.21", "lodash-es": "^4.17.21", "moment": "^2.30.1", "moment-timezone": "^0.5.45", "morgan": "^1.10.0", "nocache": "^4.0.0", - "node-pg-migrate": "^7.6.1", "nodemailer": "^6.9.15", - "npm-run-all2": "^6.1.2", "oidc-provider": "^8.5.1", "pg": "^8.13.0", "qrcode": "^1.5.4", "rate-limiter-flexible": "^2.4.2", - "tld-extract": "^2.1.0", "tsx": "^4.19.2", - "typescript": "^5.7.2", - "vite": "^5.2.14", "zod": "^3.23.8", "zod-validation-error": "^3.3.1" }, "devDependencies": { "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.27.10", + "@dotenvx/dotenvx": "^1.19.2", + "@fullhuman/postcss-purgecss": "^6.0.0", + "@gouvfr/dsfr": "^1.12.1", + "@kitajs/html": "^4.2.4", + "@kitajs/ts-html-plugin": "^4.1.0", + "@panva/jose": "^1.9.3", "@simplewebauthn/types": "^10.0.0", + "@simplewebauthn/typescript-types": "^8.3.4", "@sinonjs/fake-timers": "^11.2.2", + "@tsconfig/node22": "^22.0.0", "@types/chai": "^5.0.1", "@types/chai-as-promised": "^7.1.8", + "@types/console-log-level": "^1.4.5", + "@types/cookie-parser": "^1.4.7", + "@types/ejs": "^3.1.5", + "@types/express": "^4.17.21", + "@types/express-session": "^1.18.0", "@types/html-to-text": "^9.0.4", "@types/http-errors": "^2.0.4", "@types/lodash": "^4.17.10", "@types/lodash-es": "^4.17.12", "@types/mocha": "^10.0.10", + "@types/morgan": "^1.9.9", "@types/node": "^22.10.2", "@types/nodemailer": "^6.4.16", "@types/oidc-provider": "^8.5.2", @@ -136,10 +129,16 @@ "cypress": "^13.17.0", "cypress-axe": "^1.5.0", "cypress-maildev": "^1.3.2", + "lodash": "^4.17.21", "mocha": "^11.0.1", "nock": "^13.5.6", + "node-pg-migrate": "^7.6.1", + "npm-run-all2": "^6.1.2", "prettier": "^3.4.2", - "prettier-plugin-organize-imports": "^4.1.0" + "prettier-plugin-organize-imports": "^4.1.0", + "tld-extract": "^2.1.0", + "typescript": "^5.7.2", + "vite": "^5.2.14" }, "packageManager": "npm@11.0.0+sha512.11dff29565d2297c74e7c594a9762581bde969f0aa5cbe6f5b3644bf008a16c065ece61094d9ffbb81125be38df8e1ba43eb8244b3d30c61eb797e9a2440e3ec", "engines": {