diff --git a/README.MD b/README.MD index 5f106601..94b314db 100644 --- a/README.MD +++ b/README.MD @@ -193,3 +193,14 @@ In order to test the uniqueness of email validation flow, the `config/config.jso ``` and to validate/invalidate the email or make the email already taken you have to navigate from the browser to `http://localhost:3000/` +## Cie ID +To enable this feature, you must set the FeatureFlag to a minimum version higher than `0.0.0` in `src/payloads/backend.ts` + +```ts +cie_id: { + min_app_version: { + ios: "1.0.0", + android: "1.0.0" + } +} +``` diff --git a/assets/bonus_available/bonus_available_v2.json b/assets/bonus_available/bonus_available_v2.json index 01b469c2..624a6380 100644 --- a/assets/bonus_available/bonus_available_v2.json +++ b/assets/bonus_available/bonus_available_v2.json @@ -4,9 +4,9 @@ "it": { "name": "Carta Giovani Nazionale", "description": "Carta Giovani Nazionale (CGN) è l’incentivo per i giovani che favorisce la partecipazione ad attività culturali, sportive e ricreative, su tutto il territorio nazionale", - "subtitle": "Carta Giovani Nazionale (CGN) è l’incentivo per i giovani che favorisce la partecipazione ad attività culturali, sportive e ricreative, su tutto il territorio nazionale", - "title": "Attiva la Carta Giovani Nazionale", - "content": "#### Chi può attivarla?\nI giovani italiani ed europei che risiedono in Italia e che hanno tra i 18 e i 35 anni di età.\n\n#### Quanto valgono le agevolazioni?\nIl tipo di agevolazioni e sconti è variabile e dipende dall’operatore che aderisce all’iniziativa.\n\n#### Dove e come posso utilizzarla?\nConsulta la lista dei partner aderenti su IO. Se ti rechi presso un operatore fisico, accedi a IO e mostra la CGN dal tuo smartphone. Online, utilizza il codice sconto relativo all'agevolazione in fase di acquisto.\n\n**Se risiedi nel [Lazio](https://www.regione.lazio.it/youthcard) o in [Sardegna](https://sardegna.cartagiovani.eu/), puoi aderire anche alle iniziative di scontistica per i giovani della tua regione, scaricando la carta regionale.**\n\n#### Cos'è il circuito EYCA?\nSe hai tra i 18 e i 30 anni, la tua CGN aderisce al circuito delle carte giovani europee e ti permette di accedere ad agevolazioni anche nei paesi europei aderenti. \n\n#### Come funziona il processo di richiesta?\nL’app IO è l’unico canale per ottenere la CGN. \nSe hai tra i 18 e i 35 anni, effettua questi passaggi:\n- Leggi la [Guida](https://io.italia.it/carta-giovani-nazionale/guida-beneficiari) e l’[Informativa privacy](https://io.italia.it/carta-giovani-nazionale/informativa-beneficiari).\n- Premi il pulsante “Richiedi la Carta”.\n- L'app IO verificherà se hai i requisiti e genererà la tua CGN.\n- Una volta attiva, la tua CGN sarà visibile nella sezione Portafoglio.", + "subtitle": "Carta Giovani Nazionale (CGN) è la **carta virtuale** rivolta ai giovani, nata per favorire la partecipazione a tante opportunità, come ad esempio attività culturali, sportive, ricreative e formative, grazie alle proposte di diversi partner, valide su tutto il territorio nazionale.\n\nL’attivazione della Carta è **gratuita** e senza **limiti di utilizzo**!", + "title": "Carta Giovani Nazionale: cos’è e come funziona", + "content": "#### Chi può attivarla?\nTutti i giovani italiani ed europei che risiedono in Italia e che hanno tra i **18 e i 35 anni di età**.\n\nSe hai dai 18 ai 30 anni, la tua Carta Giovani Nazionale aderisce anche al **circuito EYCA** (European Youth Card Association).\n\n[Cos’è il circuito EYCA?](https://eyca.org/card/card-italy)\n\n#### Facile da attivare\nTi basterà premere su “Attiva Carta Giovani Nazionale” e in un paio di passaggi la carta sarà attiva. **Non è necessario fornire documenti o dettagli** durante l’attivazione, sarà l’app IO a verificare se hai i requisiti.\n\nUna volta attiva, la tua CGN sarà visibile nella sezione Portafoglio.\n\n#### Come si usa\n**Su app IO**, apri il dettaglio della proposta di tuo interesse e segui le indicazioni.\n\n**Presso i negozi o gli sportelli fisici** dei partner, accedi a IO e mostra Carta Giovani Nazionale dal tuo dispositivo. Non devi fornire nessun codice, solo mostrare la carta e, se richiesto, un tuo documento.\n\n**Se sei residente nel Lazio o in Sardegna, puoi usufruire delle opportunità per i giovani, scaricando anche la carta regionale.**", "tos_url": "https://io.italia.it/carta-giovani-nazionale/informativa-beneficiari", "urls": [ { @@ -18,9 +18,9 @@ "en": { "name": "Carta Giovani Nazionale", "description": "Carta Giovani Nazionale (CGN) is an incentive for young people to participate in cultural, sporting and recreational activities throughout the country", - "subtitle": "Carta Giovani Nazionale (CGN) is an incentive for young people to participate in cultural, sporting and recreational activities throughout the country", - "title": "Activate the Carta Giovani Nazionale", - "content": "#### Who can activate it?\nYoung Italians and Europeans who reside in Italy and are between 18 and 35 years of age.\n\n#### How much are the benefits worth?\nThe type of benefits and discounts is variable and depends on the operator participating in the initiative.\n\n#### Where and how can I use it?\nConsult the list of participating partners on IO. If you go to a physical operator, access IO and show CGN from your smartphone. Online, use the discount code related to the benefit in the purchase phase.\n\n**If you live in [Lazio](https://www.regione.lazio.it/youthcard) or in [Sardegna](https://sardegna.cartagiovani.eu/), you can also join the discount initiatives for young people in your region, by downloading the regional card.**\n\n#### What is the EYCA circuit?\nIf you are between 18 and 30 years old, your CGN joins the circuit of the European youth cards and allows you to have access to benefits also in the adhering European countries.\n\n#### How does the application process work?\nThe IO app is the only channel to get your CGN. \nIf you are between 18 and 35 years old, please follow these steps:\n- Read the [Guide](https://io.italia.it/carta-giovani-nazionale/guida-beneficiari) and the [Privacy Policy](https://io.italia.it/carta-giovani-nazionale/informativa-beneficiari).\n- Press the “Apply for Card“, button.\n- The IO app will check if you are eligible and will generate your CGN.\n- Once activated, your CGN will be visible in the Wallet section.", + "subtitle": "Carta Giovani Nazionale (CGN)is the **virtual card** addressed to young people, created to promote participation in many opportunities, such as cultural, sports, recreational and educational activities, thanks to the proposals of several partners, valid throughout the country.\n\n The activation of the Card is **free** and without **limits to use**!", + "title": "Carta Giovani Nazionale: what it is and how it works.", + "content": "#### Who can activate it?\nAll Italian and European young people who reside in Italy and are between **18 and 35 years of age**.\n If you are 18 to 30 years old, your Carta Giovani Nazionale also joins the **EYCA** (European Youth Card Association) circuit. \n\n[What is the EYCA circuit?](https://eyca.org/card/card-italy)\n\n#### Easy to activate\nYou just press “Activate National Youth Card” and in a couple of steps your card will be activated. **You don't need to provide any documents or details** during activation, the IO app will check if you are eligible.\nOnce activated, your CGN will be visible in the Wallet section.\n#### How to use it\n**On the IO app**, open the detail of the proposal you are interested in and follow the directions.\n**At the stores or physical counters** of the partners, log in to IO and show National Youth Card from your device. You don't have to provide any code, just show the card and, if requested, your ID.\n\n**If you are a resident of Lazio or Sardinia, you can take advantage of youth opportunities by downloading the regional card as well.**", "tos_url": "https://io.italia.it/carta-giovani-nazionale/informativa-beneficiari", "urls": [ { diff --git a/assets/wallet/wallet_onboarding.html b/assets/wallet/wallet_onboarding.html index 309b171b..e821fb3f 100644 --- a/assets/wallet/wallet_onboarding.html +++ b/assets/wallet/wallet_onboarding.html @@ -18,7 +18,7 @@ const paymentMethodId = urlParams.get('paymentMethodId'); const container = document.getElementById("outcomeSelect"); if (container.value === "0") { - fetch('/payment-wallet/v1/wallets/mock', { + fetch('/io-payment-wallet/v1/wallets/mock', { method: 'POST', body: JSON.stringify({ paymentMethodId, diff --git a/assets/wallet/wallet_payment.html b/assets/wallet/wallet_payment.html index d975a278..a9216086 100644 --- a/assets/wallet/wallet_payment.html +++ b/assets/wallet/wallet_payment.html @@ -21,6 +21,8 @@ ["14", "INVALID_SESSION"], ["15", "METHOD_NOT_ENABLED"], ["17", "WAITING_CONFIRMATION_EMAIL"], + ["18", "PAYMENT_REVERSED"], + ["19", "PAYPAL_REMOVED_ERROR"], ]; const simulateOutcome = () => { @@ -29,7 +31,7 @@ const transactionId = urlParams.get("transactionId"); const container = document.getElementById("outcomeSelect"); if (container.value === "0") { - fetch('/ecommerce/io/v1/mock-transaction', { + fetch('/ecommerce/io/v2/mock-transaction', { method: 'POST', body: JSON.stringify({ transactionId, diff --git a/package.json b/package.json index be5ea0af..4d221e06 100644 --- a/package.json +++ b/package.json @@ -4,24 +4,27 @@ "description": "A mock server to help io-app development https://io.italia.it/", "version": "1.0.0", "main": "app.js", - "api_backend_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.39.1-RELEASE/api_backend.yaml", - "api_public_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.39.1-RELEASE/api_public.yaml", - "api_cgn": "https://raw.githubusercontent.com/pagopa/io-backend/v13.39.1-RELEASE/api_cgn.yaml", - "api_cgn_merchants": "https://raw.githubusercontent.com/pagopa/io-backend/v13.39.1-RELEASE/api_cgn_operator_search.yaml", + "api_backend_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v14.3.0-RELEASE/api_backend.yaml", + "io_session_manager_api": "https://raw.githubusercontent.com/pagopa/io-auth-n-identity-domain/io-session-manager@1.0.0/apps/io-session-manager/api/internal.yaml", + "io_session_manager_public_api": "https://raw.githubusercontent.com/pagopa/io-auth-n-identity-domain/io-session-manager@1.0.0/apps/io-session-manager/api/public.yaml", + "api_public_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v14.3.0-RELEASE/api_public.yaml", + "api_cgn": "https://raw.githubusercontent.com/pagopa/io-backend/v14.3.0-RELEASE/api_cgn.yaml", + "api_cgn_merchants": "https://raw.githubusercontent.com/pagopa/io-backend/v14.3.0-RELEASE/api_cgn_operator_search.yaml", "api_cgn_geo": "https://raw.githubusercontent.com/pagopa/io-backend/here_geoapi_integration/api_geo.yaml", - "content_specs": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.33/definitions.yml", - "api_pagopa_walletv2": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.30/bonus/specs/bpd/pm/walletv2.json", + "content_specs": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.43/definitions.yml", + "api_pagopa_walletv2": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.43/bonus/specs/bpd/pm/walletv2.json", "api_pagopa": "https://raw.githubusercontent.com/pagopa/io-app/master/assets/paymentManager/spec.json", - "pagopa_cobadge_configuration": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.30/pagopa/cobadge/abi_definitions.yml", - "pagopa_privative_configuration": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.30/pagopa/privative/definitions.yml", - "api_eu_covid_cert": "https://raw.githubusercontent.com/pagopa/io-backend/v13.39.1-RELEASE/api_eucovidcert.yaml", + "pagopa_cobadge_configuration": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.43/pagopa/cobadge/abi_definitions.yml", + "pagopa_privative_configuration": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.43/pagopa/privative/definitions.yml", + "api_eu_covid_cert": "https://raw.githubusercontent.com/pagopa/io-backend/v14.3.0-RELEASE/api_eucovidcert.yaml", "api_cdc": "https://raw.githubusercontent.com/pagopa/io-app/master/assets/CdcSwagger.yml", - "api_fci": "https://raw.githubusercontent.com/pagopa/io-backend/v13.39.1-RELEASE/api_io_sign.yaml", - "api_pn": "https://raw.githubusercontent.com/pagopa/io-backend/v13.39.1-RELEASE/api_pn.yaml", - "api_idpay": "https://raw.githubusercontent.com/pagopa/cstar-infrastructure/v6.5.0/src/domains/idpay-app/api/idpay_appio_full/openapi.appio.full.yml", - "api_fast_login": "https://raw.githubusercontent.com/pagopa/io-backend/v13.39.1-RELEASE/openapi/generated/api_fast_login.yaml", - "api_pagopa_walletv3": "https://raw.githubusercontent.com/pagopa/pagopa-infra/v1.64.0/src/domains/pay-wallet-app/api/io-payment-wallet/v1/_openapi.json.tpl", - "api_pagopa_ecommerce": "https://raw.githubusercontent.com/pagopa/pagopa-infra/v1.64.0/src/domains/ecommerce-app/api/ecommerce-io/v2/_openapi.json.tpl", + "api_fci": "https://raw.githubusercontent.com/pagopa/io-backend/v14.3.0-RELEASE/api_io_sign.yaml", + "api_pn": "https://raw.githubusercontent.com/pagopa/io-backend/v14.3.0-RELEASE/api_pn.yaml", + "api_idpay": "https://raw.githubusercontent.com/pagopa/cstar-infrastructure/v8.25.1/src/domains/idpay-app/api/idpay_appio_full/openapi.appio.full.yml", + "api_fast_login": "https://raw.githubusercontent.com/pagopa/io-auth-n-identity-domain/io-session-manager@1.0.0/apps/io-session-manager/api/fast-login.yaml", + "api_trial_system": "https://raw.githubusercontent.com/pagopa/io-backend/v14.3.0-RELEASE/api_trial_system.yaml", + "api_pagopa_walletv3": "https://raw.githubusercontent.com/pagopa/pagopa-infra/v1.202.0/src/domains/pay-wallet-app/api/io-payment-wallet/v1/_openapi.json.tpl", + "api_pagopa_ecommerce": "https://raw.githubusercontent.com/pagopa/pagopa-infra/v1.202.0/src/domains/ecommerce-app/api/ecommerce-io/v2/_openapi.json.tpl", "api_pagopa_biz_events": "https://raw.githubusercontent.com/pagopa/pagopa-biz-events-service/0.1.37/openapi/openapi_io_patch.json", "api_pagopa_platform": "https://raw.githubusercontent.com/pagopa/pagopa-infra/v1.64.0/src/domains/shared-app/api/session-wallet/v1/_openapi.json.tpl", "api_services": "https://raw.githubusercontent.com/pagopa/io-backend/master/api_services_app_backend.yaml", @@ -41,6 +44,7 @@ "prettier:check": "prettier --check \"src/**/*.(ts|tsx)\"", "tsc:noemit": "tsc --noEmit", "generate:models": "rimraf generated && rimraf generated/definitions/backend && mkdir -p generated/definitions/backend && gen-api-models --api-spec $npm_package_api_public_specs --out-dir generated/definitions/backend && gen-api-models --api-spec $npm_package_api_backend_specs --out-dir ./generated/definitions/backend --no-strict", + "generate:api-session_manager_api-definitions": "rimraf generated/definitions/session_manager && mkdir -p generated/definitions/session_manager && gen-api-models --api-spec $npm_package_io_session_manager_public_api --out-dir generated/definitions/session_manager && gen-api-models --api-spec $npm_package_io_session_manager_api --out-dir generated/definitions/session_manager --no-strict", "generate:content-definitions": "rimraf generated/definitions/content && mkdir -p generated/definitions/content && gen-api-models --api-spec $npm_package_content_specs --out-dir ./generated/definitions/content", "generate:pagopa-cobadge-configuration-definitions": "rimraf generated/definitions/pagopa/cobadge/configuration && mkdir -p generated/definitions/pagopa/cobadge/configuration && gen-api-models --api-spec $npm_package_pagopa_cobadge_configuration --out-dir ./generated/definitions/pagopa/cobadge/configuration", "generate:pagopa-privative-configuration-definitions": "rimraf generated/definitions/pagopa/privative/configuration && mkdir -p generated/definitions/pagopa/privative/configuration && gen-api-models --api-spec $npm_package_pagopa_privative_configuration --out-dir ./generated/definitions/pagopa/privative/configuration", @@ -59,6 +63,7 @@ "generate:pn-definitions": "rimraf generated/definitions/pn && mkdir -p generated/definitions/pn && gen-api-models --api-spec $npm_package_api_pn --out-dir ./generated/definitions/pn --no-strict --request-types --response-decoders", "generate:idpay-definitions": "rimraf generated/definitions/idpay && mkdir -p generated/definitions/idpay && gen-api-models --api-spec $npm_package_api_idpay --out-dir ./generated/definitions/idpay --no-strict", "generate:fast-login-definitions": "rimraf generated/definitions/fast_login && mkdir -p generated/definitions/fast_login && gen-api-models --api-spec $npm_package_api_fast_login --out-dir ./generated/definitions/fast_login --no-strict --request-types --response-decoders", + "generate:trial-system-definitions": "rimraf generated/definitions/trial_system && mkdir -p generated/definitions/trial_system && gen-api-models --api-spec $npm_package_api_trial_system --out-dir ./generated/definitions/trial_system --no-strict --response-decoders --request-types", "generate:pagopa": "npm-run-all generate:pagopa-walletv2-definitions generate:pagopa-privative-configuration-definitions generate:pagopa-cobadge-configuration-definitions generate:pagopa-walletv3-definitions generate:pagopa-ecommerce-definitions generate:pagopa-transactions-definitions generate:pagopa-platform-definitions", "generate:services-definitions": "rimraf generated/definitions/services && mkdir -p generated/definitions/services && gen-api-models --api-spec $npm_package_api_services --out-dir ./generated/definitions/services --no-strict", "generate": "npm-run-all generate:*" @@ -74,13 +79,15 @@ "chalk": "^4.1.1", "cli-ux": "^5.6.3", "compare-versions": "^6.0.0-rc.1", + "cookie-parser": "^1.4.6", "crypto": "^1.0.1", "date-fns": "^2.27.0", - "express": "^4.17.3", + "express": "4.20.0", "figlet": "^1.5.2", "fp-ts": "^2.16.0", "io-ts": "^2.2.20", "jose": "^4.15.5", + "jsontokens": "^4.0.1", "lodash": "^4.17.21", "morgan": "^1.10.0", "npm-run-all": "^4.1.5", @@ -88,6 +95,7 @@ "sha256": "^0.2.0", "ts-pattern": "^3.3.4", "ulid": "^2.3.0", + "uuid": "^9.0.1", "xml2js": "^0.5.0" }, "resolutions": { @@ -97,8 +105,9 @@ "@faker-js/faker": "^7.6.0", "@pagopa/openapi-codegen-ts": "^12.0.2", "@types/body-parser": "^1.19.2", + "@types/cookie-parser": "^1.4.7", "@types/date-fns": "^2.6.0", - "@types/express": "^4.17.13", + "@types/express": "^4.17.21", "@types/figlet": "^1.5.4", "@types/jest": "^27.4.0", "@types/lodash": "^4.14.178", @@ -106,6 +115,7 @@ "@types/node": "^16.11.17", "@types/sha256": "^0.2.0", "@types/supertest": "^2.0.8", + "@types/uuid": "^9.0.8", "@types/xml2js": "^0.4.11", "@typescript-eslint/eslint-plugin": "^5.59.5", "@typescript-eslint/parser": "^5.59.5", diff --git a/src/config.ts b/src/config.ts index 921fa057..c789693e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -9,12 +9,15 @@ import { EmailAddress } from "../generated/definitions/backend/EmailAddress"; import { PreferredLanguageEnum } from "../generated/definitions/backend/PreferredLanguage"; import { PushNotificationsContentTypeEnum } from "../generated/definitions/backend/PushNotificationsContentType"; import { ReminderStatusEnum } from "../generated/definitions/backend/ReminderStatus"; +import { SubscriptionStateEnum } from "../generated/definitions/trial_system/SubscriptionState"; +import { TrialId } from "../generated/definitions/trial_system/TrialId"; import { IoDevServerConfig, ProfileAttrs, WalletMethodConfig } from "./types/config"; import { readFileAsJSON } from "./utils/file"; +import { serverUrl } from "./utils/server"; export const staticContentRootPath = "/static_contents"; const root = path.resolve("."); @@ -27,7 +30,7 @@ const defaultProfileAttrs: ProfileAttrs = { mobile: "5555555555" as NonEmptyString, fiscal_code: "TAMMRA80A41H501I" as FiscalCode, email: "maria.giovanna.rossi@email.it" as EmailAddress, - accepted_tos_version: 4.8 as NonNegativeNumber, + accepted_tos_version: 4.91 as NonNegativeNumber, preferred_languages: [PreferredLanguageEnum.it_IT], reminder_status: ReminderStatusEnum.ENABLED, push_notifications_content_type: PushNotificationsContentTypeEnum.FULL @@ -180,7 +183,8 @@ const defaultConfig: IoDevServerConfig = { cgn: { isCgnEligible: true, isEycaEligible: true, - allowRandomValues: true + allowRandomValues: true, + hangOnActivation: false } }, idpay: { @@ -202,6 +206,49 @@ const defaultConfig: IoDevServerConfig = { servicesByInstitutionIdResponseCode: 200 } }, + trials: { + ["01J2GN4TA8FB6DPTAX3T3YD6M1" as TrialId]: SubscriptionStateEnum.ACTIVE // IT Wallet trial + }, + fims: { + history: { + count: 52, + consentsEndpointFailureStatusCode: undefined, + exportEndpointFailureStatusCode: undefined, + exportProcessingTimeMilliseconds: 15000, + pageSize: 12 + }, + provider: { + federationCookieName: "_io_fims_token", + idTokenRawPrivateKey: + "278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f", + idTokenRawPublicKey: + "03fdd57adec3d438ea237fe46b33ee1e016eda6b585c3e27ea66686c2ea5358479", + idTokenSigningAlgorithm: "ES256K", + idTokenTTLMilliseconds: 15 * 60 * 1000, + ignoreFederationCookiePresence: false, + ignoreFederationCookieValue: true, + implicitCodeFlow: false, + interactionCookieKey: "_interaction", + interactionResumeCookieKey: "_interaction_resume", + interactionResumeSignatureCookieKey: "_interaction_resume.sig", + interactionSignatureCookieKey: "_interaction.sig", + interactionTTLMilliseconds: 5 * 60 * 1000, + sessionCookieKey: "_session", + sessionLegacyCookieKey: "_session.legacy", + sessionLegacySignatureCookieKey: "_session.legacy.sig", + sessionSignatureCookieKey: "_session.sig", + sessionTTLMilliseconds: 1 * 60 * 1000, + useLaxInsteadOfNoneForSameSiteOnSessionCookies: true + }, + relyingParties: [ + { + id: "1", + redirectUri: [`${serverUrl}/fims/relyingParty/1/redirectUri`], + registrationName: "Example Relying Party 1", + scopes: ["openid", "profile"] + } + ] + }, allowRandomValues: true } }; diff --git a/src/features/fims/routers/historyRouter.ts b/src/features/fims/routers/historyRouter.ts new file mode 100644 index 00000000..266db6d0 --- /dev/null +++ b/src/features/fims/routers/historyRouter.ts @@ -0,0 +1,83 @@ +import { Router } from "express"; +import { addHandler } from "../../../payloads/response"; +import { addApiV1Prefix } from "../../../utils/strings"; +import { getProblemJson } from "../../../payloads/error"; +import { HistoryPieceOfData } from "../types/history"; +import { + generateHistory, + isProcessingExport, + nextPageFromRequest +} from "../services/historyService"; +import { getFimsConfig } from "../services/configurationService"; +import { FIMSConfig } from "../types/config"; + +export const fimsHistoryRouter = Router(); + +// eslint-disable-next-line functional/no-let +let history: HistoryPieceOfData[] | null = null; +// eslint-disable-next-line functional/no-let +let lastExportRequestTimestamp: number = 0; + +addHandler( + fimsHistoryRouter, + "get", + addApiV1Prefix("/fims/consents"), + (req, res) => { + const config = getFimsConfig(); + const failureResponseCode = + config.history.consentsEndpointFailureStatusCode; + if (failureResponseCode) { + res + .status(failureResponseCode) + .send(getProblemJson(failureResponseCode, "Simulated error")); + return; + } + + initializeIfNeeded(config); + + const nextPage = nextPageFromRequest(config, history, req); + if (typeof nextPage === "string") { + res.status(500).send(getProblemJson(500, nextPage)); + return; + } + + res.status(200).send(nextPage); + }, + () => Math.floor(2500 * Math.random()) +); + +addHandler( + fimsHistoryRouter, + "post", + addApiV1Prefix("/fims/exports"), + (_req, res) => { + const config = getFimsConfig(); + const failureResponseCode = config.history.exportEndpointFailureStatusCode; + if (failureResponseCode) { + res + .status(failureResponseCode) + .send(getProblemJson(failureResponseCode, "Simulated error")); + return; + } + + const isStillProcessingExport = isProcessingExport( + config, + lastExportRequestTimestamp + ); + if (isStillProcessingExport) { + res.status(409).send(); + return; + } + + lastExportRequestTimestamp = Date.now(); + res.status(202).send(); + }, + () => Math.floor(2500 + 1400 * Math.random()) +); + +const initializeIfNeeded = (config: FIMSConfig) => { + if (history) { + return; + } + history = generateHistory(config); +}; diff --git a/src/features/fims/routers/providerRouter.ts b/src/features/fims/routers/providerRouter.ts new file mode 100644 index 00000000..97e76ce4 --- /dev/null +++ b/src/features/fims/routers/providerRouter.ts @@ -0,0 +1,752 @@ +import { Request, Response, Router } from "express"; +import { v4 } from "uuid"; +import { TokenSigner } from "jsontokens"; +import { addHandler } from "../../../payloads/response"; +import { FIMSToken } from "../../../payloads/session"; +import { + InteractionData, + OIdCData, + SessionData +} from "../types/authentication"; +import { + baseProviderPath, + generateIdTokenRedirectHTML, + providerConfig, + translationForScope +} from "../services/providerService"; +import { isSessionTokenValid } from "../../../persistence/sessionInfo"; +import { getRelyingParty } from "./relyingPartyRouter"; + +export const fimsProviderRouter = Router(); + +const providerRequests = new Map>(); +const interactionIds = new Map(); + +addHandler( + fimsProviderRouter, + "get", + `${baseProviderPath()}/oauth/authorize`, + (req, res) => { + // Required parameters + const relyingPartyId = req.query.client_id; + if (!relyingPartyId) { + res + .status(400) + .send({ message: "Missing 'client_id' parameter in request" }); + return; + } + const scopes = req.query.scope; + if (!scopes) { + res.status(400).send({ message: "Missing 'scope' parameter in request" }); + return; + } + const responseType = req.query.response_type; + if (!responseType) { + res + .status(400) + .send({ message: "Missing 'response_type' parameter in request" }); + return; + } + const redirectUri = req.query.redirect_uri; + if (!redirectUri) { + res + .status(400) + .send({ message: "Missing 'redirect_uri' parameter in request" }); + return; + } + const responseMode = req.query.response_mode; + if (!responseMode) { + res + .status(400) + .send({ message: "Missing 'response_mode' parameter in request" }); + return; + } + const nonce = req.query.nonce; + if (!nonce) { + res.status(400).send({ message: "Missing 'nonce' parameter in request" }); + return; + } + const state = req.query.state; + if (!state) { + res.status(400).send({ message: "Missing 'state' parameter in request" }); + return; + } + + // Relying Party registration conformance + const relyingParty = getRelyingParty(String(relyingPartyId)); + if (!relyingParty) { + res.status(400).send({ + message: `Relying Party with id (${relyingPartyId}) not found` + }); + return; + } + const requestScopes = String(scopes).split(" "); + const relyingPartyScopes = new Set(relyingParty.scopes); + if ( + requestScopes.length === 0 || + !requestScopes.every(requestScope => relyingPartyScopes.has(requestScope)) + ) { + res + .status(400) + .send({ message: `Relying Party does not allow requested scopes` }); + return; + } + if (relyingParty.responseType !== responseType) { + res.status(400).send({ + message: `Relying Party does not allow response type (${responseType})` + }); + return; + } + + if (!relyingParty.redirectUris.includes(String(redirectUri))) { + res.status(400).send({ + message: `Relying Party does not allow redirect uri (${redirectUri})` + }); + return; + } + + if (relyingParty.responseMode !== responseMode) { + res.status(400).send({ + message: `Relying Party does not allow response mode (${responseMode})` + }); + return; + } + + // FIMS Token + const cookies = req.cookies; + if (!validateFIMSToken(cookies, res)) { + return; + } + + // Fast login + if (!isSessionTokenValid()) { + res.status(401).send({ + message: "Fast Login Session expired" + }); + return; + } + + // OIdC session + const relyingPartyIdString = String(relyingPartyId); + if (!providerRequests.has(relyingPartyIdString)) { + providerRequests.set(relyingPartyIdString, new Map()); + } + const providerOIdCsForRelyingParty = + providerRequests.get(relyingPartyIdString); + if (!providerOIdCsForRelyingParty) { + res.status(500).send({ + message: "Unable to allocate OIdC data for current Relying Party" + }); + return; + } + + const interactionId = v4(); + const interactionData: InteractionData = { + interaction: interactionId, + interactionResumeSignature: v4(), + interactionResume: interactionId, + interactionSignature: v4() + }; + const oidcData: OIdCData = { + id: () => `${relyingPartyId}${state}${nonce}`, + relyingPartyId: relyingPartyIdString, + nonce: String(nonce), + state: String(state), + scopes: requestScopes, + redirectUri: String(redirectUri), + firstInteraction: interactionData + }; + const oidcDataId = oidcData.id(); + if (providerRequests.has(oidcDataId)) { + res.status(400).send({ + message: `Bad state (${state}) and nonce (${nonce}) for current request` + }); + return; + } + providerRequests.set(oidcDataId, providerOIdCsForRelyingParty); + interactionIds.set(interactionId, oidcData); + + const config = providerConfig(); + const authorizationRedirectUri = `${baseProviderPath()}/interaction/${interactionId}`; + const cookieExpirationTime = new Date( + new Date().getTime() + config.interactionTTLMilliseconds + ); + res + .cookie(config.interactionCookieKey, interactionData.interaction, { + path: `${baseProviderPath()}/interaction/${interactionId}`, + expires: cookieExpirationTime, + sameSite: "lax", + httpOnly: true + }) + .cookie( + config.interactionSignatureCookieKey, + interactionData.interactionSignature, + { + path: `${baseProviderPath()}/interaction/${interactionId}`, + expires: cookieExpirationTime, + sameSite: "lax", + httpOnly: true + } + ) + .cookie( + config.interactionResumeCookieKey, + interactionData.interactionResume, + { + path: `${baseProviderPath()}/oauth/authorize/${interactionId}`, + expires: cookieExpirationTime, + sameSite: "lax", + httpOnly: true + } + ) + .cookie( + config.interactionResumeSignatureCookieKey, + interactionData.interactionResumeSignature, + { + path: `${baseProviderPath()}/oauth/authorize/${interactionId}`, + expires: cookieExpirationTime, + sameSite: "lax", + httpOnly: true + } + ) + .redirect(303, authorizationRedirectUri); + }, + () => Math.random() * 2500 +); + +addHandler( + fimsProviderRouter, + "get", + `${baseProviderPath()}/interaction/:id`, + (req, res) => { + const requestInteractionId = req.params.id; + const oidcData = interactionIds.get(requestInteractionId); + if (!oidcData) { + res.status(400).send({ + message: `Interaction Id (${requestInteractionId}) not found` + }); + return; + } + + // Cookie validation + const cookies = req.cookies; + if (!validateFIMSToken(cookies, res)) { + return; + } + + // Header validation + const requestHeaders = req.headers; + const acceptLanguage = requestHeaders["accept-language"] as string; + if (!acceptLanguage || acceptLanguage.trim().length === 0) { + res.status(400).send({ + message: `Missing or empty header 'Accept-Language': (${acceptLanguage})` + }); + return; + } + + const config = providerConfig(); + if (oidcData.firstInteraction) { + const cookiesToValidate = { + [config.interactionCookieKey]: oidcData.firstInteraction?.interaction, + [config.interactionSignatureCookieKey]: + oidcData.firstInteraction?.interactionSignature + }; + if (!validateCookies(cookiesToValidate, cookies, res)) { + return; + } + + const redirectUri = `${baseProviderPath()}/oauth/authorize/${requestInteractionId}`; + res.redirect(303, redirectUri); + return; + } else if (oidcData.secondInteraction && oidcData.session) { + const cookiesToValidate = { + [config.interactionCookieKey]: oidcData.secondInteraction?.interaction, + [config.interactionSignatureCookieKey]: + oidcData.secondInteraction?.interactionSignature, + [config.sessionCookieKey]: oidcData.session?.session, + [config.sessionSignatureCookieKey]: oidcData.session?.sessionSignature, + [config.sessionLegacyCookieKey]: oidcData.session?.sessionLegacy, + [config.sessionLegacySignatureCookieKey]: + oidcData.session?.sessionLegacySignature + }; + if (!validateCookies(cookiesToValidate, cookies, res)) { + return; + } + + const relyingParty = getRelyingParty(oidcData.relyingPartyId); + if (!relyingParty) { + res.status(500).send({ + message: `Internal inconsistency for Interaction Id (${requestInteractionId}): unable to find Relying Party (${oidcData.relyingPartyId})` + }); + return; + } + + const consentData = { + _links: { + abort: { + href: `${baseProviderPath()}/interaction/${requestInteractionId}/abort` + }, + consent: { + href: `${baseProviderPath()}/interaction/${requestInteractionId}/confirm` + } + }, + service_id: relyingParty.serviceId, + redirect: { + display_name: relyingParty.displayName + }, + type: "consent", + user_metadata: oidcData.scopes.map(scope => ({ + name: scope, + display_name: translationForScope(scope) + })) + }; + res.status(200).send(consentData); + return; + } + + res.status(500).send({ + message: `Internal inconsistency for Interaction Id (${requestInteractionId})` + }); + }, + () => Math.random() * 2500 +); + +addHandler( + fimsProviderRouter, + "post", + `${baseProviderPath()}/interaction/:id/confirm`, + (req, res) => { + const requestInteractionId = req.params.id; + const oidcData = interactionIds.get(requestInteractionId); + if (!oidcData) { + res.status(400).send({ + message: `Interaction Id (${requestInteractionId}) not found` + }); + return; + } + + if ( + requestInteractionId !== oidcData.secondInteraction?.interaction || + !oidcData.session + ) { + res.status(400).send({ + message: `Bad Interaction Id (${requestInteractionId}) for current state` + }); + return; + } + + // Cookie validation + const cookies = req.cookies; + if (!validateFIMSToken(cookies, res)) { + return; + } + + const config = providerConfig(); + const cookiesToValidate = { + [config.interactionCookieKey]: oidcData.secondInteraction?.interaction, + [config.interactionSignatureCookieKey]: + oidcData.secondInteraction?.interactionSignature, + [config.sessionCookieKey]: oidcData.session?.session, + [config.sessionSignatureCookieKey]: oidcData.session?.sessionSignature, + [config.sessionLegacyCookieKey]: oidcData.session?.sessionLegacy, + [config.sessionLegacySignatureCookieKey]: + oidcData.session?.sessionLegacySignature + }; + if (!validateCookies(cookiesToValidate, cookies, res)) { + return; + } + + // Fast login + if (!isSessionTokenValid()) { + res.status(401).send({ + message: "Fast Login Session expired" + }); + return; + } + + const redirectUri = `${baseProviderPath()}/oauth/authorize/${requestInteractionId}`; + res.redirect(303, redirectUri); + }, + () => Math.random() * 2500 +); + +addHandler( + fimsProviderRouter, + "post", + `${baseProviderPath()}/interaction/:id/abort`, + (req, res) => { + const requestInteractionId = req.params.id; + const currentOidcData = interactionIds.get(requestInteractionId); + if (!currentOidcData) { + res.status(400).send({ + message: `Interaction Id (${requestInteractionId}) not found` + }); + return; + } + + interactionIds.delete(requestInteractionId); + + const relyingPartyId = currentOidcData.relyingPartyId; + const providerOIdCsForRelyingParty = providerRequests.get(relyingPartyId); + const currentOIdCDataId = currentOidcData.id(); + providerOIdCsForRelyingParty?.delete(currentOIdCDataId); + + const config = providerConfig(); + // TODO replace by redirecting to the relying party with an abort + res + .clearCookie(config.interactionCookieKey) + .clearCookie(config.interactionSignatureCookieKey) + .clearCookie(config.interactionResumeCookieKey) + .clearCookie(config.interactionResumeSignatureCookieKey) + .clearCookie(config.sessionCookieKey) + .clearCookie(config.sessionSignatureCookieKey) + .clearCookie(config.sessionLegacyCookieKey) + .clearCookie(config.sessionLegacySignatureCookieKey) + .status(200).send(` + + + + + + + FIMS Provider: aborted + + +

Your request has been aborted

+ + + `); + }, + () => Math.random() * 2500 +); + +addHandler( + fimsProviderRouter, + "get", + `${baseProviderPath()}/oauth/authorize/:id`, + (req, res) => { + const requestInteractionId = req.params.id; + const currentOidcData = interactionIds.get(requestInteractionId); + if (!currentOidcData) { + res.status(400).send({ + message: `Interaction Id (${requestInteractionId}) not found` + }); + return; + } + + // Cookie validation + const cookies = req.cookies; + if (!validateFIMSToken(cookies, res)) { + return; + } + + const config = providerConfig(); + if (currentOidcData.firstInteraction) { + const cookiesToValidate = { + [config.interactionResumeCookieKey]: + currentOidcData.firstInteraction?.interactionResume, + [config.interactionResumeSignatureCookieKey]: + currentOidcData.firstInteraction?.interactionResumeSignature + }; + if (!validateCookies(cookiesToValidate, cookies, res)) { + return; + } + + responseFromOAuthAuthorizeFirstInteraction( + requestInteractionId, + currentOidcData, + res + ); + return; + } else if (currentOidcData.secondInteraction && currentOidcData.session) { + // This case happens as a redirect of the /confirm endpoint + const cookiesToValidate = { + [config.interactionResumeCookieKey]: + currentOidcData.secondInteraction?.interactionResume, + [config.interactionResumeSignatureCookieKey]: + currentOidcData.secondInteraction?.interactionResumeSignature, + [config.sessionCookieKey]: currentOidcData.session?.session, + [config.sessionSignatureCookieKey]: + currentOidcData.session?.sessionSignature, + [config.sessionLegacyCookieKey]: currentOidcData.session?.sessionLegacy, + [config.sessionLegacySignatureCookieKey]: + currentOidcData.session?.sessionLegacySignature + }; + if (!validateCookies(cookiesToValidate, cookies, res)) { + return; + } + + interactionIds.delete(requestInteractionId); + + const relyingPartyId = currentOidcData.relyingPartyId; + const providerOIdCsForRelyingParty = providerRequests.get(relyingPartyId); + const currentOIdCDataId = currentOidcData.id(); + providerOIdCsForRelyingParty?.delete(currentOIdCDataId); + + responseFromOAuthAuthorizeSecondInteraction( + requestInteractionId, + currentOidcData, + req, + res + ); + return; + } + + res.status(500).send({ + message: `Internal inconsistency for Interaction Id (${requestInteractionId})` + }); + }, + () => Math.random() * 2500 +); + +const responseFromOAuthAuthorizeFirstInteraction = ( + requestInteractionId: string, + currentOIdCData: OIdCData, + res: Response +) => { + const interactionId = v4(); + const interactionData: InteractionData = { + interaction: interactionId, + interactionResumeSignature: v4(), + interactionResume: interactionId, + interactionSignature: v4() + }; + const sessionId = v4(); + const sessionData: SessionData = { + session: sessionId, + sessionSignature: v4(), + sessionLegacy: sessionId, + sessionLegacySignature: v4() + }; + const oidcData: OIdCData = { + ...currentOIdCData, + firstInteraction: undefined, + secondInteraction: interactionData, + session: sessionData + }; + const providerOIdCsForRelyingParty = providerRequests.get( + oidcData.relyingPartyId + ); + providerOIdCsForRelyingParty?.set(oidcData.id(), oidcData); + interactionIds.delete(requestInteractionId); + interactionIds.set(interactionId, oidcData); + + const config = providerConfig(); + const interactionRedirectUri = `${baseProviderPath()}/interaction/${interactionId}`; + const interactionCookieExpirationTime = new Date( + new Date().getTime() + config.interactionTTLMilliseconds + ); + const sessionCookieExpirationTime = new Date( + new Date().getTime() + config.sessionTTLMilliseconds + ); + res + .cookie(config.interactionCookieKey, interactionData.interaction, { + path: `${baseProviderPath()}/interaction/${interactionId}`, + expires: interactionCookieExpirationTime, + sameSite: "lax", + httpOnly: true + }) + .cookie( + config.interactionSignatureCookieKey, + interactionData.interactionSignature, + { + path: `${baseProviderPath()}/interaction/${interactionId}`, + expires: interactionCookieExpirationTime, + sameSite: "lax", + httpOnly: true + } + ) + .cookie( + config.interactionResumeCookieKey, + interactionData.interactionResume, + { + path: `${baseProviderPath()}/oauth/authorize/${interactionId}`, + expires: interactionCookieExpirationTime, + sameSite: "lax", + httpOnly: true + } + ) + .cookie( + config.interactionResumeSignatureCookieKey, + interactionData.interactionResumeSignature, + { + path: `${baseProviderPath()}/oauth/authorize/${interactionId}`, + expires: interactionCookieExpirationTime, + sameSite: "lax", + httpOnly: true + } + ) + .cookie(config.sessionCookieKey, sessionData.session, { + path: `${baseProviderPath()}`, + expires: sessionCookieExpirationTime, + sameSite: sameSitePolicyForSessionCookie(), + httpOnly: true + }) + .cookie(config.sessionSignatureCookieKey, sessionData.sessionSignature, { + path: `${baseProviderPath()}`, + expires: sessionCookieExpirationTime, + sameSite: sameSitePolicyForSessionCookie(), + httpOnly: true + }) + .cookie(config.sessionLegacyCookieKey, sessionData.sessionLegacy, { + path: `${baseProviderPath()}`, + expires: sessionCookieExpirationTime, + httpOnly: true + }) + .cookie( + config.sessionLegacySignatureCookieKey, + sessionData.sessionLegacySignature, + { + path: `${baseProviderPath()}`, + expires: sessionCookieExpirationTime, + httpOnly: true + } + ) + .redirect(303, interactionRedirectUri); +}; + +const responseFromOAuthAuthorizeSecondInteraction = ( + requestInteractionId: string, + currentOIdCData: OIdCData, + req: Request, + res: Response +) => { + const config = providerConfig(); + const isImplicitCodeFlow = config.implicitCodeFlow; + const relyingPartyRedirectUri = currentOIdCData.redirectUri; + const relyingPartyNonce = currentOIdCData.nonce; + const relyingPartyState = currentOIdCData.state; + const issuer = `${req.protocol}://${req.get("host")}`; + // TODO retrieve from profile (or configuration?) + const tokenPayload = { + sub: "SMTJHN50P01D222E", + family_name: "Smith", + given_name: "John", + name: "John Smith", + nonce: relyingPartyNonce, + s_hash: "NotImplemented", // TODO? + aud: currentOIdCData.relyingPartyId, + exp: new Date( + new Date().getTime() + config.idTokenTTLMilliseconds + ).getTime(), + iat: new Date().getTime(), + iss: issuer + }; + const idToken = new TokenSigner( + config.idTokenSigningAlgorithm, + config.idTokenRawPrivateKey + ).sign(tokenPayload); + + const newSessionId = v4(); + const invalidationExpirationTime = new Date(1970, 0, 1, 0, 0, 0); + const sessionCookieExpirationTime = new Date( + new Date().getTime() + config.sessionTTLMilliseconds + ); + + const commonResponse = res + .cookie(config.interactionResumeCookieKey, "", { + path: `${baseProviderPath()}/oauth/authorize/${requestInteractionId}`, + expires: invalidationExpirationTime, + sameSite: "lax", + httpOnly: true + }) + .cookie(config.interactionResumeSignatureCookieKey, v4(), { + path: `${baseProviderPath()}/oauth/authorize/${requestInteractionId}`, + expires: invalidationExpirationTime, + sameSite: "lax", + httpOnly: true + }) + .cookie(config.sessionCookieKey, newSessionId, { + path: `${baseProviderPath()}`, + expires: sessionCookieExpirationTime, + sameSite: sameSitePolicyForSessionCookie(), + httpOnly: true + }) + .cookie(config.sessionSignatureCookieKey, v4(), { + path: `${baseProviderPath()}`, + expires: sessionCookieExpirationTime, + sameSite: sameSitePolicyForSessionCookie(), + httpOnly: true + }) + .cookie(config.sessionLegacyCookieKey, newSessionId, { + path: `${baseProviderPath()}`, + expires: sessionCookieExpirationTime, + httpOnly: true + }) + .cookie(config.sessionLegacySignatureCookieKey, v4(), { + path: `${baseProviderPath()}`, + expires: sessionCookieExpirationTime, + httpOnly: true + }); + + const relyingPartyURLInstance = new URL(relyingPartyRedirectUri); + if (isImplicitCodeFlow) { + const responseHTMLBody = generateIdTokenRedirectHTML( + relyingPartyURLInstance.href, + idToken, + relyingPartyState + ); + commonResponse.status(200).send(responseHTMLBody); + } else { + relyingPartyURLInstance.searchParams.set("authorization_code", idToken); + relyingPartyURLInstance.searchParams.set("nonce", relyingPartyNonce); + relyingPartyURLInstance.searchParams.set("state", relyingPartyState); + commonResponse.redirect(303, relyingPartyURLInstance.href); + } +}; + +const validateFIMSToken = (cookies: Record, res: Response) => { + const config = providerConfig(); + if (config.ignoreFederationCookiePresence) { + return true; + } + const fimsTokenCookieName = config.federationCookieName; + const requestFimsToken = cookies[fimsTokenCookieName]; + if (!requestFimsToken) { + res + .status(400) + .send({ message: `Missing '${fimsTokenCookieName}' cookie in request` }); + return false; + } + if (config.ignoreFederationCookieValue) { + return true; + } + const requestFimsTokenString = String(requestFimsToken); + const fimsToken = FIMSToken(); + if (requestFimsTokenString !== fimsToken) { + res.status(401).send({ + message: `'${fimsTokenCookieName}' with value (${requestFimsTokenString}) does not match` + }); + return false; + } + return true; +}; + +const sameSitePolicyForSessionCookie = () => + providerConfig().useLaxInsteadOfNoneForSameSiteOnSessionCookies + ? "lax" + : "none"; + +const validateCookies = ( + mandatoryCookies: Record, + requestCookies: Record, + res: Response +) => { + // eslint-disable-next-line guard-for-in + for (const mandatoryCookieKey in mandatoryCookies) { + const cookieValue = requestCookies[mandatoryCookieKey]; + if (!cookieValue) { + res.status(400).send({ + message: `Mising cookie with name '${mandatoryCookieKey}'` + }); + return false; + } + const mandatoryCookieValue = mandatoryCookies[mandatoryCookieKey]; + if (cookieValue !== mandatoryCookieValue) { + res.status(400).send({ + message: `Value of cookie with name '${mandatoryCookieKey}' (${cookieValue}) does not match exptected one (${mandatoryCookieValue})` + }); + return false; + } + } + return true; +}; diff --git a/src/features/fims/routers/relyingPartyRouter.ts b/src/features/fims/routers/relyingPartyRouter.ts new file mode 100644 index 00000000..bc1f2517 --- /dev/null +++ b/src/features/fims/routers/relyingPartyRouter.ts @@ -0,0 +1,256 @@ +import { v4 } from "uuid"; +import { Router } from "express"; +import { addHandler } from "../../../payloads/response"; +import { RelyingParty, RelyingPartyRequest } from "../types/relyingParty"; +import { + baseRelyingPartyPath, + commonRedirectionValidation, + generateUserProfileHTML, + relyingPartiesConfig +} from "../services/relyingPartyService"; +import { baseProviderPath } from "../services/providerService"; +import ServicesDB from "../../../persistence/services"; +import { serverUrl } from "../../../utils/server"; + +export const fimsRelyingPartyRouter = Router(); + +const relyingParties = new Map(); +const relyingPartyRequests = new Map< + string, + Map +>(); + +export const getRelyingParty = (id: string) => relyingParties.get(id); + +addHandler( + fimsRelyingPartyRouter, + "get", + `${baseRelyingPartyPath()}/:id/landingPage`, + (req, res) => { + const relyingPartyId = req.params.id; + const relyingParty = findOrLazyLoadRelyingParty(relyingPartyId); + if (!relyingParty) { + res.status(404).send({ + message: `Relying Party with id (${relyingPartyId}) not found` + }); + return; + } + + const scopes = relyingParty.scopes.join(" "); + const state = v4(); + const relyingPartyRequest: RelyingPartyRequest = { + relyingPartyId, + nonce: v4(), + state + }; + if (!relyingPartyRequests.has(relyingPartyId)) { + relyingPartyRequests.set( + relyingPartyId, + new Map() + ); + } + const relyingPartyRequestMap = relyingPartyRequests.get(relyingPartyId); + relyingPartyRequestMap?.set(relyingPartyRequest.state, relyingPartyRequest); + const fimsProviderRedirectUri = `${serverUrl}${baseProviderPath()}/oauth/authorize?client_id=${ + relyingParty.id + }&scope=${scopes}&response_type=${relyingParty.responseType}&redirect_uri=${ + relyingParty.redirectUris[0] + }&response_mode=${relyingParty.responseMode}&nonce=${ + relyingPartyRequest.nonce + }&state=${relyingPartyRequest.state}`; + const encodedFimsProviderRedirectUri = encodeURI(fimsProviderRedirectUri); + res.redirect(303, encodedFimsProviderRedirectUri); + }, + () => Math.random() * 2500 +); + +addHandler( + fimsRelyingPartyRouter, + "get", + `${baseRelyingPartyPath()}/:id/redirectUri`, + (req, res) => { + const relyingPartyId = req.params.id; + + const requestHeaders = req.headers; + const lollipopMethod = requestHeaders[ + "x-pagopa-lollipop-original-method" + ] as string; + if (!lollipopMethod || lollipopMethod.trim().length === 0) { + res.status(400).send({ + message: `Missing or empty lollipop header 'x-pagopa-lollipop-original-method'` + }); + return; + } + const lollipopOriginalUrl = requestHeaders[ + "x-pagopa-lollipop-original-url" + ] as string; + if (!lollipopOriginalUrl || lollipopOriginalUrl.trim().length === 0) { + res.status(400).send({ + message: `Missing or empty lollipop header 'x-pagopa-lollipop-original-url'` + }); + return; + } + const lollipopAuthorizationCode = requestHeaders[ + "x-pagopa-lollipop-custom-authorization_code" + ] as string; + if ( + !lollipopAuthorizationCode || + lollipopAuthorizationCode.trim().length === 0 + ) { + res.status(400).send({ + message: `Missing or empty lollipop header 'x-pagopa-lollipop-custom-authorization_code'` + }); + return; + } + + const state = req.query.state as string; + if (!state) { + res.status(400).send({ message: `Missing parameter 'state' in request` }); + return; + } + + const nonce = req.query.nonce as string; + if (!nonce) { + res.status(400).send({ message: `Missing parameter 'nonce' in request` }); + return; + } + + const fakeIdToken = req.query.authorization_code as string; + if (!fakeIdToken) { + res + .status(400) + .send({ message: `Missing parameter 'authorization_code' in request` }); + return; + } + + commonRedirectionValidation( + fakeIdToken, + relyingPartyId, + state, + relyingPartyRequests, + req, + res + ); + }, + () => Math.random() * 2500 +); + +addHandler( + fimsRelyingPartyRouter, + "post", + `${baseRelyingPartyPath()}/:id/redirectUri`, + (req, res) => { + const contentType = req.headers["content-type"]; + if ( + !contentType || + !contentType.toLowerCase().includes("application/x-www-form-urlencoded") + ) { + res.status(400).send({ + message: `Content-type (${contentType}) is not supported` + }); + return; + } + + const relyingPartyId = req.params.id; + + const requestHeaders = req.headers; + const lollipopMethod = requestHeaders[ + "x-pagopa-lollipop-original-method" + ] as string; + if (!lollipopMethod || lollipopMethod.trim().length === 0) { + res.status(400).send({ + message: `Missing or empty lollipop header 'x-pagopa-lollipop-original-method'` + }); + return; + } + const lollipopOriginalUrl = requestHeaders[ + "x-pagopa-lollipop-original-url" + ] as string; + if (!lollipopOriginalUrl || lollipopOriginalUrl.trim().length === 0) { + res.status(400).send({ + message: `Missing or empty lollipop header 'x-pagopa-lollipop-original-url'` + }); + return; + } + const lollipopIdToken = requestHeaders[ + "x-pagopa-lollipop-custom-id_token" + ] as string; + if (!lollipopIdToken || lollipopIdToken.trim().length === 0) { + res.status(400).send({ + message: `Missing or empty lollipop header 'x-pagopa-lollipop-custom-id_token'` + }); + return; + } + + const state = req.body.state as string; + if (!state) { + res + .status(400) + .send({ message: `Missing parameter 'state' in request body` }); + return; + } + + const fakeIdToken = req.body.id_token as string; + if (!fakeIdToken) { + res.status(400).send({ + message: `Missing parameter 'authorization_code' in request body` + }); + return; + } + + commonRedirectionValidation( + fakeIdToken, + relyingPartyId, + state, + relyingPartyRequests, + req, + res + ); + }, + () => Math.random() * 2500 +); + +addHandler( + fimsRelyingPartyRouter, + "get", + `${baseRelyingPartyPath()}/authenticatedPage`, + (req, res) => { + const query = req.query; + const userProfileHTML = generateUserProfileHTML(query); + res.status(200).send(userProfileHTML); + } +); + +const findOrLazyLoadRelyingParty = (id: string) => { + const inMemoryRelyingParty = relyingParties.get(id); + if (inMemoryRelyingParty) { + return inMemoryRelyingParty; + } + + const config = relyingPartiesConfig(); + config.forEach(relyingPartyConfig => { + const serviceId = relyingPartyConfig.serviceId ?? randomServiceId(); + relyingParties.set(relyingPartyConfig.id, { + displayName: relyingPartyConfig.registrationName, + id: relyingPartyConfig.id, + redirectUris: relyingPartyConfig.redirectUri, + responseMode: "form_post", + responseType: "id_token", + scopes: relyingPartyConfig.scopes, + serviceId + }); + }); + + return relyingParties.get(id); +}; + +const randomServiceId = () => { + const allServices = ServicesDB.getAllServices(); + if (allServices.length > 0) { + const firstNationalService = allServices[0]; + return firstNationalService.service_id; + } + throw new Error( + "RelyingPartyRouter.randomServiceId: empty service collection. It must have some values at this point" + ); +}; diff --git a/src/features/fims/services/configurationService.ts b/src/features/fims/services/configurationService.ts new file mode 100644 index 00000000..54348989 --- /dev/null +++ b/src/features/fims/services/configurationService.ts @@ -0,0 +1,7 @@ +import { ioDevServerConfig } from "../../../config"; +import { IoDevServerConfig } from "../../../types/config"; +import { FIMSConfig } from "../types/config"; + +export const getFimsConfig = ( + config: IoDevServerConfig = ioDevServerConfig +): FIMSConfig => config.features.fims; diff --git a/src/features/fims/services/historyService.ts b/src/features/fims/services/historyService.ts new file mode 100644 index 00000000..e49d43f8 --- /dev/null +++ b/src/features/fims/services/historyService.ts @@ -0,0 +1,78 @@ +import { Request } from "express"; +import { ulid } from "ulid"; +import { faker } from "@faker-js/faker/locale/it"; +import ServicesDB from "../../../persistence/services"; +import { HistoryPieceOfData } from "../types/history"; +import { FIMSConfig } from "../types/config"; + +export const generateHistory = (config: FIMSConfig) => { + const services = ServicesDB.getAllServices(); + if (services.length === 0) { + throw new Error("FIMS cannot work without any configured service"); + } + + const relyingPartyNameForService = new Map(); + + const historyConfig = config.history; + return [...Array(historyConfig.count).keys()].map(valueIndex => { + const serviceId = + services[Math.round(Math.random() * (services.length - 1))].service_id; + if (!relyingPartyNameForService.has(serviceId)) { + relyingPartyNameForService.set(serviceId, faker.company.name()); + } + const displayName = relyingPartyNameForService.get(serviceId) ?? ""; + + const timestamp = new Date(); + timestamp.setMonth(timestamp.getMonth() - valueIndex); + return { + id: ulid(), + service_id: serviceId, + redirect: { + display_name: displayName + }, + timestamp: timestamp.toISOString() + }; + }); +}; + +export const nextPageFromRequest = ( + config: FIMSConfig, + history: HistoryPieceOfData[] | null, + req: Request +) => { + if (!history) { + return "History not initialized"; + } + + const pageSize = config.history.pageSize; + + const continuationToken = req.query.continuationToken; + if (continuationToken) { + const firstItemIndex = history.findIndex( + pieceOfData => pieceOfData.id === continuationToken + ); + if (firstItemIndex < 0) { + return `No match for given continuation token (${continuationToken})`; + } + const lastItemIndex = Math.min(firstItemIndex + pageSize, history.length); + return { + items: history.slice(firstItemIndex, lastItemIndex), + continuationToken: + lastItemIndex < history.length ? history[lastItemIndex].id : undefined + }; + } + + const lastItemIndex = Math.min(pageSize, history.length); + return { + items: history.slice(0, lastItemIndex), + continuationToken: + lastItemIndex < history.length ? history[lastItemIndex].id : undefined + }; +}; + +export const isProcessingExport = ( + config: FIMSConfig, + lastExportRequestTimestamp: number +) => + Date.now() - lastExportRequestTimestamp < + config.history.exportProcessingTimeMilliseconds; diff --git a/src/features/fims/services/providerService.ts b/src/features/fims/services/providerService.ts new file mode 100644 index 00000000..ae6bf45c --- /dev/null +++ b/src/features/fims/services/providerService.ts @@ -0,0 +1,79 @@ +import { ioDevServerConfig } from "../../../config"; +import { IoDevServerConfig } from "../../../types/config"; +import { ProviderConfig } from "../types/config"; + +export const providerConfig = ( + config: IoDevServerConfig = ioDevServerConfig +): ProviderConfig => config.features.fims.provider; + +export const baseProviderPath = () => "/fims/provider"; + +export const generatePermissionHTML = ( + confirmUrl: string, + abortUrl: string, + relyingPartyName?: string, + scopes?: ReadonlyArray +) => ` + + + + + + + FIMS Provider: user action required + + +
+

Autorizzi l'invio dei dati?

+

I seguenti dati stanno per essere condivisi con ${relyingPartyName}

+

${scopes?.join(" ")}

+
+
+ + +
+
+
+ Annulla + +
+
+
+ + +`; + +export const generateIdTokenRedirectHTML = ( + redirectUrl: string, + idToken: string, + relyingPartyState: string +) => ` + + + + + + + FIMS Provider: submit callback + + + +
+ + + +
+ + +`; + +export const translationForScope = (scope: string) => { + if ("openid" === scope.toLowerCase()) { + return "ID"; + } else if ("profile" === scope.toLowerCase()) { + return "Name and Surname"; + } + return scope; +}; diff --git a/src/features/fims/services/relyingPartyService.ts b/src/features/fims/services/relyingPartyService.ts new file mode 100644 index 00000000..1896b712 --- /dev/null +++ b/src/features/fims/services/relyingPartyService.ts @@ -0,0 +1,143 @@ +import { Request, Response } from "express"; +import { ParsedQs } from "qs"; +import { TokenVerifier, decodeToken } from "jsontokens"; +import { ioDevServerConfig } from "../../../config"; +import { IoDevServerConfig } from "../../../types/config"; +import { RelyingPartiesConfig } from "../types/config"; +import { RelyingPartyRequest } from "../types/relyingParty"; +import { providerConfig } from "./providerService"; + +export const relyingPartiesConfig = ( + config: IoDevServerConfig = ioDevServerConfig +): ReadonlyArray => config.features.fims.relyingParties; + +export const baseRelyingPartyPath = () => "/fims/relyingParty"; + +export const tokenPayloadToUrl = ( + tokenPayload: Record, + baseUrl: string +) => { + const fullName = tokenPayload.name as string; + const name = tokenPayload.given_name as string; + const surname = tokenPayload.family_name as string; + const fiscalCode = tokenPayload.sub as string; + const signatureHash = tokenPayload.s_hash as string; + const audienceId = tokenPayload.aud as string; + const issuer = tokenPayload.iss as string; + const issuedOn = tokenPayload.iat as number; + const expiresOn = tokenPayload.exp as number; + + const url = new URL(baseUrl); + url.searchParams.set("fullname", fullName); + url.searchParams.set("name", name); + url.searchParams.set("surname", surname); + url.searchParams.set("fiscalCode", fiscalCode); + url.searchParams.set("signatureHash", signatureHash); + url.searchParams.set("audienceId", audienceId); + url.searchParams.set("issuer", issuer); + url.searchParams.set("issuedOn", `${issuedOn}`); + url.searchParams.set("expiresOn", `${expiresOn}`); + + return url.href; +}; + +export const generateUserProfileHTML = (query: ParsedQs) => { + const fullName = query.fullname; + const name = query.name; + const surname = query.surname; + const fiscalCode = query.fiscalCode; + const signatureHash = query.signatureHash; + const audienceId = query.audienceId; + const issuer = query.issuer; + const issuedOn = query.issuedOn; + const expiresOn = query.expiresOn; + + return ` + + + + + + + Relying Party: authenticated + + +

Your data

+
    +
  • Full name: ${fullName}
  • +
  • Name: ${name}
  • +
  • Surname: ${surname}
  • +
  • Fiscal Code: ${fiscalCode}
  • +
  • Signature Hash: ${signatureHash}
  • +
  • Audience Id: ${audienceId}
  • +
  • Issuer: ${issuer}
  • +
  • Issued on: ${new Date(Number(issuedOn))}
  • +
  • Expires on: ${new Date(Number(expiresOn))}
  • +
+ + +`; +}; + +export const commonRedirectionValidation = ( + idToken: string, + relyingPartyId: string, + state: string, + relyingPartyRequests: Map>, + req: Request, + res: Response +) => { + const relyingPartyCurrentRequests = relyingPartyRequests.get(relyingPartyId); + if (!relyingPartyCurrentRequests) { + res.status(400).send({ + message: `Relying Party with id (${relyingPartyId}) not found` + }); + return; + } + + const relyingPartyRequest = relyingPartyCurrentRequests.get(state); + if (!relyingPartyRequest) { + res.status(400).send({ + message: `No active request for state (${state}) on Relying Party with id (${relyingPartyId})` + }); + return; + } + + const config = providerConfig(); + const verified = new TokenVerifier( + config.idTokenSigningAlgorithm, + config.idTokenRawPublicKey + ).verify(idToken); + if (!verified) { + res.status(400).send({ message: `Received ID token cannot be verified` }); + return; + } + + try { + const tokenData = decodeToken(idToken); + const tokenPayload = tokenData.payload as Record; + const payloadNonce = tokenPayload.nonce as string; + if (payloadNonce !== relyingPartyRequest.nonce) { + res.status(400).send({ + message: `Bad nonce value (${payloadNonce}) for Relying Party with id (${relyingPartyId}) with state (${state})` + }); + return; + } + + relyingPartyCurrentRequests.delete(state); + + const authenticatedUrl = tokenPayloadToUrl( + tokenPayload, + `${req.protocol}://${ + req.headers.host + }${baseRelyingPartyPath()}/authenticatedPage` + ); + res.redirect(302, authenticatedUrl); + } catch (e) { + res.status(400).send({ + message: `Unable to decode token. Error is (${ + e instanceof Error ? e.message : "unknown error" + })` + }); + } +}; diff --git a/src/features/fims/types/authentication.ts b/src/features/fims/types/authentication.ts new file mode 100644 index 00000000..ba72d181 --- /dev/null +++ b/src/features/fims/types/authentication.ts @@ -0,0 +1,25 @@ +export type OIdCData = { + id: () => string; + relyingPartyId: string; + state: string; + nonce: string; + scopes: ReadonlyArray; + redirectUri: string; + firstInteraction?: InteractionData; + secondInteraction?: InteractionData; + session?: SessionData; +}; + +export type InteractionData = { + interaction: string; + interactionSignature: string; + interactionResume: string; + interactionResumeSignature: string; +}; + +export type SessionData = { + session: string; + sessionSignature: string; + sessionLegacy: string; + sessionLegacySignature: string; +}; diff --git a/src/features/fims/types/config.ts b/src/features/fims/types/config.ts new file mode 100644 index 00000000..78940bcb --- /dev/null +++ b/src/features/fims/types/config.ts @@ -0,0 +1,67 @@ +import * as t from "io-ts"; +import { FailureHttpResponseCode } from "../../../types/httpResponseCode"; + +export const HistoryConfig = t.intersection([ + t.type({ + count: t.number, + exportProcessingTimeMilliseconds: t.number, + pageSize: t.number + }), + t.partial({ + consentsEndpointFailureStatusCode: FailureHttpResponseCode, + exportEndpointFailureStatusCode: FailureHttpResponseCode + }) +]); + +export const ProviderConfig = t.intersection([ + t.type({ + federationCookieName: t.string, + idTokenRawPrivateKey: t.string, + idTokenRawPublicKey: t.string, + idTokenSigningAlgorithm: t.string, + idTokenTTLMilliseconds: t.number, + interactionCookieKey: t.string, + interactionResumeCookieKey: t.string, + interactionResumeSignatureCookieKey: t.string, + interactionSignatureCookieKey: t.string, + interactionTTLMilliseconds: t.number, + implicitCodeFlow: t.boolean, + sessionCookieKey: t.string, + sessionLegacyCookieKey: t.string, + sessionLegacySignatureCookieKey: t.string, + sessionSignatureCookieKey: t.string, + sessionTTLMilliseconds: t.number + }), + t.partial({ + ignoreFederationCookiePresence: t.boolean, + ignoreFederationCookieValue: t.boolean, + useLaxInsteadOfNoneForSameSiteOnSessionCookies: t.boolean + }) +]); + +export const RelyingPartiesConfig = t.intersection([ + t.type({ + id: t.string, + registrationName: t.string, + redirectUri: t.readonlyArray(t.string), + scopes: t.readonlyArray( + t.union([t.literal("openid"), t.literal("profile")]) + ) + }), + t.partial({ + serviceId: t.string + }) +]); + +export const FIMSConfig = t.intersection([ + t.type({ + history: HistoryConfig, + provider: ProviderConfig, + relyingParties: t.readonlyArray(RelyingPartiesConfig) + }), + t.partial({}) +]); + +export type ProviderConfig = t.TypeOf; +export type RelyingPartiesConfig = t.TypeOf; +export type FIMSConfig = t.TypeOf; diff --git a/src/features/fims/types/history.ts b/src/features/fims/types/history.ts new file mode 100644 index 00000000..4e7fbd3f --- /dev/null +++ b/src/features/fims/types/history.ts @@ -0,0 +1,8 @@ +export type HistoryPieceOfData = { + id: string; + service_id: string; + redirect: { + display_name: string; + }; + timestamp: string; +}; diff --git a/src/features/fims/types/relyingParty.ts b/src/features/fims/types/relyingParty.ts new file mode 100644 index 00000000..634025cf --- /dev/null +++ b/src/features/fims/types/relyingParty.ts @@ -0,0 +1,17 @@ +export type RelyingParty = { + id: string; + scopes: ReadonlyArray<"openid" | "profile">; + responseType: "id_token"; + redirectUris: ReadonlyArray; // TODO use relative path (i.e. compose protocol, host, port and base path dinamically) + responseMode: "form_post"; + serviceId: string; + displayName: string; + // TODO failure callbackUrl + // TODO programmatic flow flag +}; + +export type RelyingPartyRequest = { + relyingPartyId: string; + nonce: string; + state: string; +}; diff --git a/src/features/payments/persistence/paymentMethods.ts b/src/features/payments/persistence/paymentMethods.ts index c2a4e1bd..405b9c92 100644 --- a/src/features/payments/persistence/paymentMethods.ts +++ b/src/features/payments/persistence/paymentMethods.ts @@ -55,6 +55,22 @@ export const paymentMethodsDB: ReadonlyArray = [ } as Range ], methodManagement: PaymentMethodManagementTypeEnum.NOT_ONBOARDABLE + }, + { + id: "4", + name: "POSTEPAY", + description: "PostePay", + asset: + "https://github.com/pagopa/io-services-metadata/raw/master/logos/apps/paga-con-postepay.png", + status: PaymentMethodStatusEnum.ENABLED, + paymentTypeCode: "PPAY", + ranges: [ + { + min: 0, + max: Math.floor(Math.random() * 5000) + } as Range + ], + methodManagement: PaymentMethodManagementTypeEnum.REDIRECT } ]; diff --git a/src/features/payments/persistence/userWallet.ts b/src/features/payments/persistence/userWallet.ts index 68188219..111d9f71 100644 --- a/src/features/payments/persistence/userWallet.ts +++ b/src/features/payments/persistence/userWallet.ts @@ -8,11 +8,21 @@ import { WalletApplicationStatusEnum } from "../../../../generated/definitions/p import { WalletClientStatusEnum } from "../../../../generated/definitions/pagopa/walletv3/WalletClientStatus"; import { WalletInfo } from "../../../../generated/definitions/pagopa/walletv3/WalletInfo"; import { WalletInfoDetails } from "../../../../generated/definitions/pagopa/walletv3/WalletInfoDetails"; +import { UserLastPaymentMethodResponse } from "../../../../generated/definitions/pagopa/ecommerce/UserLastPaymentMethodResponse"; import { WalletStatusEnum } from "../../../../generated/definitions/pagopa/walletv3/WalletStatus"; +import { + WalletLastUsageType, + WalletLastUsageTypeEnum +} from "../../../../generated/definitions/pagopa/ecommerce/WalletLastUsageType"; +import { GuestMethodLastUsageType } from "../../../../generated/definitions/pagopa/ecommerce/GuestMethodLastUsageType"; +import { uuidv4 } from "../../../utils/strings"; import { generateWalletDetailsByPaymentMethod } from "./paymentMethods"; const userWallets = new Map(); +// eslint-disable-next-line functional/no-let, @typescript-eslint/no-unused-vars +let recentUsedPaymentMethod: UserLastPaymentMethodResponse | undefined; + const getUserWallets = () => Array.from(userWallets.values()); const getUserWalletInfo = (walletId: WalletInfo["walletId"]) => @@ -30,7 +40,7 @@ const generateUserWallet = ( paymentMethodId: number, extraDetails: Partial = {} ) => { - const walletId = (getUserWallets().length + 1).toString(); + const walletId = uuidv4(); const { details, paymentMethodAsset } = generateWalletDetailsByPaymentMethod(paymentMethodId); @@ -87,6 +97,27 @@ const updateUserWalletApplication = ( return E.left("Wallet not found"); }; +const setRecentUsedPaymentMethod = ( + id: string, + type: WalletLastUsageType | GuestMethodLastUsageType +) => { + if (type === WalletLastUsageTypeEnum.wallet) { + recentUsedPaymentMethod = { + date: new Date(), + type, + walletId: id + }; + return; + } + recentUsedPaymentMethod = { + date: new Date(), + type, + paymentMethodId: id + }; +}; + +const getRecentusedPaymentMethod = () => recentUsedPaymentMethod; + // At server startup generateWalletData(); @@ -96,5 +127,7 @@ export default { getUserWalletInfo, generateUserWallet, removeUserWallet, + setRecentUsedPaymentMethod, + getRecentusedPaymentMethod, updateUserWalletApplication }; diff --git a/src/features/payments/routers/payment.ts b/src/features/payments/routers/payment.ts index 8f76a6d0..8d22fa3e 100644 --- a/src/features/payments/routers/payment.ts +++ b/src/features/payments/routers/payment.ts @@ -7,6 +7,9 @@ import { FaultCategoryEnum } from "../../../../generated/definitions/pagopa/ecom import { NewTransactionRequest } from "../../../../generated/definitions/pagopa/ecommerce/NewTransactionRequest"; import { RequestAuthorizationRequest } from "../../../../generated/definitions/pagopa/ecommerce/RequestAuthorizationRequest"; import { RequestAuthorizationResponse } from "../../../../generated/definitions/pagopa/ecommerce/RequestAuthorizationResponse"; +import { GuestMethodLastUsageTypeEnum } from "../../../../generated/definitions/pagopa/ecommerce/GuestMethodLastUsageType"; +import { WalletLastUsageTypeEnum } from "../../../../generated/definitions/pagopa/ecommerce/WalletLastUsageType"; +import { WalletDetailTypeEnum } from "../../../../generated/definitions/pagopa/ecommerce/WalletDetailType"; import { RptId } from "../../../../generated/definitions/pagopa/ecommerce/RptId"; import { ioDevServerConfig } from "../../../config"; import { serverUrl } from "../../../utils/server"; @@ -163,16 +166,31 @@ addPaymentHandler( }), O.fold( () => res.sendStatus(403), - ({ transactionId }) => + ({ transactionId, requestAuthorization }) => pipe( getTransactionInfoPayload(transactionId), O.fold( () => res.sendStatus(404), - () => - res.status(200).json({ + () => { + const usedPaymentMethodType = + requestAuthorization.details.detailType === + WalletDetailTypeEnum.wallet + ? WalletLastUsageTypeEnum.wallet + : GuestMethodLastUsageTypeEnum.guest; + const usedPaymentMethodId = + requestAuthorization.details.detailType === + WalletDetailTypeEnum.wallet + ? requestAuthorization.details.walletId + : requestAuthorization.details.paymentMethodId; + WalletDB.setRecentUsedPaymentMethod( + usedPaymentMethodId, + usedPaymentMethodType + ); + return res.status(200).json({ authorizationUrl: `${serverUrl}${WALLET_PAYMENT_PATH}?transactionId=${transactionId}`, authorizationRequestId: ulid() - } as RequestAuthorizationResponse) + } as RequestAuthorizationResponse); + } ) ) ) @@ -207,6 +225,12 @@ addPaymentHandler("get", "/wallets", (req, res) => { }); }); +addPaymentHandler("get", "/user/lastPaymentMethodUsed", (req, res) => { + res.json({ + ...WalletDB.getRecentusedPaymentMethod() + }); +}); + /** * This API is used to retrieve a list of payment methods available */ diff --git a/src/features/payments/routers/router.ts b/src/features/payments/routers/router.ts index e3c22f38..470954b9 100644 --- a/src/features/payments/routers/router.ts +++ b/src/features/payments/routers/router.ts @@ -3,8 +3,8 @@ import { addHandler, SupportedMethod } from "../../../payloads/response"; export const walletRouter = Router(); -export const PAYMENT_WALLET_PREFIX = "/payment-wallet/v1"; -export const ECOMMERCE_PREFIX = "/ecommerce/io/v1"; +export const PAYMENT_WALLET_PREFIX = "/io-payment-wallet/v1"; +export const ECOMMERCE_PREFIX = "/ecommerce/io/v2"; export const TRANSACTIONS_PREFIX = "/bizevents/tx-service-jwt/v1"; export const PLATFORM_PREFIX = "/session-wallet/v1"; diff --git a/src/features/services/payloads/get-featured-services.ts b/src/features/services/payloads/get-featured-services.ts index ca8b5cfc..f8f2d0cb 100644 --- a/src/features/services/payloads/get-featured-services.ts +++ b/src/features/services/payloads/get-featured-services.ts @@ -5,6 +5,7 @@ import { FeaturedService } from "../../../../generated/definitions/services/Feat import { FeaturedServices } from "../../../../generated/definitions/services/FeaturedServices"; import { ioDevServerConfig } from "../../../config"; import ServicesDB from "../../../persistence/services"; +import { cgnServiceId } from "../../../payloads/services/special/cgn/factoryCGNService"; const featuredServicesSize = ioDevServerConfig.features.service.featuredServicesSize; @@ -52,7 +53,20 @@ export const getFeaturedServicesResponsePayload = (): FeaturedServices => { featuredServicesSize ); + // CGN Service + const cgnSpecialService = featuredSpecialServices.find( + service => service.id === cgnServiceId + ); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [first, ...rest] = featuredServices; + return { - services: featuredServices + services: cgnSpecialService + ? [ + cgnSpecialService, + ...rest.filter(service => service.id !== cgnSpecialService.id) + ] + : featuredServices }; }; diff --git a/src/features/trialSystem/routers/index.ts b/src/features/trialSystem/routers/index.ts new file mode 100644 index 00000000..5bde3f21 --- /dev/null +++ b/src/features/trialSystem/routers/index.ts @@ -0,0 +1,73 @@ +/* eslint-disable functional/immutable-data */ +import { Router } from "express"; +import { addApiV1Prefix } from "../../../utils/strings"; +import { addHandler } from "../../../payloads/response"; +import { Subscription } from "../../../../generated/definitions/trial_system/Subscription"; +import { TrialId } from "../../../../generated/definitions/trial_system/TrialId"; +import { SubscriptionStateEnum } from "../../../../generated/definitions/trial_system/SubscriptionState"; +import { ioDevServerConfig } from "../../../config"; +export const trialSystemRouter = Router(); + +const addPrefix = (path: string) => addApiV1Prefix(`/trials${path}`); + +const trials: Record = {}; + +const loadTrials = () => + Object.entries(ioDevServerConfig.features.trials || {})?.forEach( + ([trialId, state]) => { + trials[trialId as TrialId] = { + trialId: trialId as TrialId, + state, + createdAt: new Date() + }; + } + ); + +addHandler( + trialSystemRouter, + "post", + addPrefix("/:trialId/subscriptions"), + (req, res) => { + const currentTrial = trials[req.params.trialId as TrialId]; + + if (!currentTrial) { + return res.status(400); + } + + if (currentTrial.state !== SubscriptionStateEnum.UNSUBSCRIBED) { + return res.status(409).json({ + detail: "The resource already exists.", + title: "Conflict" + }); + } + + trials[req.params.trialId as TrialId] = { + trialId: req.params.trialId as TrialId, + state: SubscriptionStateEnum.SUBSCRIBED, + createdAt: new Date() + }; + + res.status(201).json({ + trialId: req.params.trialId as TrialId, + state: SubscriptionStateEnum.SUBSCRIBED, + createdAt: new Date() + } as Subscription); + } +); + +addHandler( + trialSystemRouter, + "get", + addPrefix("/:trialId/subscriptions"), + (req, res) => { + const currentTrial = trials[req.params.trialId as TrialId]; + + if (currentTrial) { + return res.status(200).json(currentTrial); + } + + res.sendStatus(404); + } +); + +loadTrials(); diff --git a/src/payloads/backend.ts b/src/payloads/backend.ts index 75fea924..aa0039d8 100644 --- a/src/payloads/backend.ts +++ b/src/payloads/backend.ts @@ -2,6 +2,7 @@ import { ToolEnum } from "../../generated/definitions/content/AssistanceToolConf import { BackendStatus } from "../../generated/definitions/content/BackendStatus"; import { LevelEnum } from "../../generated/definitions/content/SectionStatus"; import { pnOptInServiceId } from "../features/pn/services/services"; +import { serverUrl } from "../utils/server"; export const backendInfo = { min_app_version: { android: "1.27.0", ios: "1.27.0" }, @@ -16,6 +17,9 @@ export const backendStatus: BackendStatus = { "it-IT": "", "en-EN": "English message" }, + statusMessages: { + items: [] + }, config: { bpd_ranking: false, bpd_ranking_v2: true, @@ -43,21 +47,13 @@ export const backendStatus: BackendStatus = { merchants_v2: false }, fims: { + domain: `${serverUrl}/fims/provider/`, enabled: true, - domain: "http://localhost:3000/" - }, - uaDonations: { - enabled: false, - banner: { - visible: false, - description: { - "it-IT": - "Fai una donazione alle organizzazioni umanitarie che assistono le vittime civili della crisi in Ucraina", - "en-EN": - "Make a donation to humanitarian organizations that assist the civilians affected by the crisis in Ukraine" - }, - url: "https://assets.cdn.io.pagopa.it/html/donate.html" - } + min_app_version: { + android: "2.68.0.0", + ios: "2.68.0.0" + }, + historyEnabled: true }, premiumMessages: { opt_in_out_enabled: true @@ -114,6 +110,18 @@ export const backendStatus: BackendStatus = { ios: "0.0.0", android: "0.0.0" } + }, + sessionRefresh: { + min_app_version: { + ios: "0.0.0", + android: "0.0.0" + } + } + }, + cie_id: { + min_app_version: { + ios: "0.0.0.0", + android: "0.0.0.0" } }, emailUniquenessValidation: { @@ -129,8 +137,22 @@ export const backendStatus: BackendStatus = { } }, tos: { - tos_version: 4.8, - tos_url: "https://io.italia.it/app-content/tos_privacy.html" + tos_version: 4.91, + tos_url: "https://io.italia.it/app-content/tos_privacy.html?v=4.91" + }, + itw: { + enabled: true, + min_app_version: { + ios: "2.66.0.0", + android: "2.66.0.0" + } + }, + landing_banners: { + priority_order: [ + "PUSH_NOTIFICATIONS_REMINDER", + "ITW_DISCOVERY", + "INVALID_ID" + ] } }, sections: { @@ -233,7 +255,7 @@ export const backendStatus: BackendStatus = { } }, payments: { - is_visible: true, + is_visible: false, level: LevelEnum.critical, message: { "it-IT": "Dalle 20:30 alle 22:40 non sarà possibile pagare con PayPal.", diff --git a/src/payloads/features/idpay/get-timeline-detail.ts b/src/payloads/features/idpay/get-timeline-detail.ts index f3a89509..7b3858b9 100644 --- a/src/payloads/features/idpay/get-timeline-detail.ts +++ b/src/payloads/features/idpay/get-timeline-detail.ts @@ -24,7 +24,7 @@ const generateRandomOperationDetailDTO = ( case "TRANSACTION": return { ...operation, - accrued: operation.accrued || faker.datatype.number(100), + accruedCents: operation.accruedCents || faker.datatype.number(10000), idTrxAcquirer: ulid(), idTrxIssuer: ulid() }; diff --git a/src/payloads/session.ts b/src/payloads/session.ts index 427c2b4d..e62c5e14 100644 --- a/src/payloads/session.ts +++ b/src/payloads/session.ts @@ -1,6 +1,6 @@ import { faker } from "@faker-js/faker/locale/it"; -import { PublicSession } from "../../generated/definitions/backend/PublicSession"; -import { SpidLevel } from "../../generated/definitions/backend/SpidLevel"; +import { PublicSession } from "../../generated/definitions/session_manager/PublicSession"; +import { SpidLevel } from "../../generated/definitions/session_manager/SpidLevel"; import { getRandomValue } from "../utils/random"; import { validatePayload } from "../utils/validator"; import { IOResponse } from "./response"; @@ -12,13 +12,22 @@ const getToken = (defaultValue: string) => "global" ); +// eslint-disable-next-line functional/no-let +let mFIMSToken: string | undefined; +export const FIMSToken = () => { + if (!mFIMSToken) { + mFIMSToken = getToken("AAAAAAAAAAAAA5"); + } + return mFIMSToken; +}; + const generateSessionTokens = (): PublicSession => ({ spidLevel: "https://www.spid.gov.it/SpidL2" as SpidLevel, walletToken: getToken("AAAAAAAAAAAAA1"), myPortalToken: getToken("AAAAAAAAAAAAA2"), bpdToken: getToken("AAAAAAAAAAAAA3"), zendeskToken: getToken("AAAAAAAAAAAAA4"), - fimsToken: getToken("AAAAAAAAAAAAA5") + fimsToken: FIMSToken() }); // eslint-disable-next-line functional/no-let diff --git a/src/persistence/appInfo.ts b/src/persistence/appInfo.ts index 3351d1a6..6358dd84 100644 --- a/src/persistence/appInfo.ts +++ b/src/persistence/appInfo.ts @@ -2,21 +2,24 @@ import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; import * as A from "fp-ts/lib/Array"; import { Request } from "express"; -import { VersionPerPlatform } from "../../generated/definitions/content/VersionPerPlatform"; + +type osPlatform = "ios" | "android"; type DeviceOS = { - iPhone: keyof VersionPerPlatform; - Android: keyof VersionPerPlatform; + iPhone: osPlatform; + Android: osPlatform; + Darwin: osPlatform; }; const osPerDevice: DeviceOS = { iPhone: "ios", - Android: "android" + Android: "android", + Darwin: "ios" }; type AppInfo = { appVersion: string | undefined; - appOs: O.Option; + appOs: O.Option; }; const appInfo: AppInfo = { diff --git a/src/persistence/idpay.ts b/src/persistence/idpay.ts index af2597f9..9cb9e450 100644 --- a/src/persistence/idpay.ts +++ b/src/persistence/idpay.ts @@ -65,19 +65,19 @@ const { idPay: walletConfig } = ioDevServerConfig.wallet; const pagoPaWallet: WalletV2 = getWalletV2()[0]; const generateRandomInitiativeDTO = (): InitiativeDTO => { - const amount = faker.datatype.number({ min: 50, max: 200, precision: 10 }); - const accrued = faker.datatype.number({ max: 200, precision: 10 }); - const refunded = faker.datatype.number({ max: accrued, precision: 10 }); + const amountCents = faker.datatype.number({ min: 5000, max: 20000 }); + const accruedCents = faker.datatype.number({ max: 20000 }); + const refundedCents = faker.datatype.number({ max: accruedCents }); return { initiativeId: ulid(), initiativeName: faker.company.name(), status: getRandomEnumValue(InitiativeStatus), endDate: faker.date.future(1), - amount, - accrued, + amountCents, + accruedCents, initiativeRewardType: getRandomEnumValue(InitiativeRewardTypeEnum), - refunded, + refundedCents, lastCounterUpdate: faker.date.recent(1), iban: faker.helpers.arrayElement(ibanList)?.iban || "", nInstr: 1, @@ -107,8 +107,8 @@ const generateRandomTransactionOperationDTO = ( operationType: getRandomEnumValue(TransactionOperationTypeEnum), operationDate: new Date(), operationId: ulid(), - accrued: faker.datatype.number({ min: 5, max: 25 }), - amount: faker.datatype.number({ min: 50, max: 100 }), + accruedCents: faker.datatype.number({ min: 500, max: 2500 }), + amountCents: faker.datatype.number({ min: 5000, max: 10000 }), brand, circuitType: "01", brandLogo, @@ -128,7 +128,7 @@ const generateRandomRefundOperationDTO = ( operationDate: new Date(), operationId: ulid(), eventId: ulid(), - amount: faker.datatype.number({ min: 5, max: 100 }), + amountCents: faker.datatype.number({ min: 500, max: 10000 }), ...withInfo }); @@ -525,8 +525,8 @@ range(0, walletConfig.discountCount).forEach(() => { status: InitiativeStatus.REFUNDABLE, iban: undefined, nInstr: 0, - accrued: 0, - refunded: 0 + accruedCents: 0, + refundedCents: 0 }; const { initiativeId } = initiative; diff --git a/src/persistence/sessionInfo.ts b/src/persistence/sessionInfo.ts index 5ce12c0c..f0e1f3f0 100644 --- a/src/persistence/sessionInfo.ts +++ b/src/persistence/sessionInfo.ts @@ -69,12 +69,14 @@ export const setSessionAuthenticationProvider = (req: Request) => { | undefined; }; -export const isSessionTokenValid = (req: Request) => { - const bearerToken = req.get("authorization"); - - // if there is no bearer token , we assume the call does not require verification - if (!bearerToken) { - return true; +export const isSessionTokenValid = (requestOrUndefined?: Request) => { + if (requestOrUndefined) { + const bearerToken = requestOrUndefined.get("authorization"); + + // if there is no bearer token , we assume the call does not require verification + if (!bearerToken) { + return true; + } } // if user is authenticated but this is not a fast login, the token in always ok diff --git a/src/populate-persistence.ts b/src/populate-persistence.ts index a3beb3d4..fb110f60 100644 --- a/src/populate-persistence.ts +++ b/src/populate-persistence.ts @@ -104,7 +104,7 @@ const createMessagesWithCTA = ( ), getNewMessage( customConfig, - `1 CTA start FISM SSO`, + `1 CTA start FIMS SSO`, frontMatter1CTAFims + messageMarkdown ) ] diff --git a/src/routers/__tests__/json-mock.test.ts b/src/routers/__tests__/json-mock.test.ts new file mode 100644 index 00000000..da788c18 --- /dev/null +++ b/src/routers/__tests__/json-mock.test.ts @@ -0,0 +1,9 @@ +import { assetsFolder } from "../../config"; +import { readFileAsJSON } from "../../utils/file"; + +// test added beacuse CI did not catch malformed json file +it("should return bonus_available_v2.json", async () => { + await readFileAsJSON( + assetsFolder + "/bonus_available/bonus_available_v2.json" + ); +}); diff --git a/src/routers/__tests__/server.test.ts b/src/routers/__tests__/server.test.ts index deb33904..c5f7c6d7 100644 --- a/src/routers/__tests__/server.test.ts +++ b/src/routers/__tests__/server.test.ts @@ -1,6 +1,6 @@ import * as E from "fp-ts/lib/Either"; import supertest from "supertest"; -import { PublicSession } from "../../../generated/definitions/backend/PublicSession"; +import { PublicSession } from "../../../generated/definitions/session_manager/PublicSession"; import { AppUrlLoginScheme } from "../../payloads/login"; import { basePath } from "../../payloads/response"; import app from "../../server"; @@ -37,8 +37,30 @@ it("session should return a valid session", async () => { expect(E.isRight(session)).toBeTruthy(); }); -it("test-login /test-login should always return sessionToken", async () => { - const result = await request.post("/test-login"); +it("test-login for LEGACY /test-login should always return sessionToken", async () => { + const result = await request + .post("/test-login") + .set("x-pagopa-lollipop-pub-key-hash-algo", "sha256") + .set( + "x-pagopa-lollipop-pub-key", + "eyJrdHkiOiJFQyIsInkiOiJuYkFGd0JLT3AvRnh4VHpITGgvbVdUL3NtSjllY0lxaElkK0dBemQxTFB3PSIsIngiOiJkdHhFZU5PK1B2RFdoVkM2ZnQyTFRLMlZvWHoxektpQmI4bkRyUy9sZGY4PSIsImNydiI6IlAtMjU2In0=" + ) + .set("x-pagopa-idp-id", "spid"); + + expect(result.status).toBe(200); + expect(result.body).toStrictEqual({ token: getLoginSessionToken() }); +}); + +it("test-login for FL /test-login should always return sessionToken", async () => { + const result = await request + .post("/test-login") + .set("x-pagopa-lollipop-pub-key-hash-algo", "sha256") + .set( + "x-pagopa-lollipop-pub-key", + "eyJrdHkiOiJFQyIsInkiOiJuYkFGd0JLT3AvRnh4VHpITGgvbVdUL3NtSjllY0lxaElkK0dBemQxTFB3PSIsIngiOiJkdHhFZU5PK1B2RFdoVkM2ZnQyTFRLMlZvWHoxektpQmI4bkRyUy9sZGY4PSIsImNydiI6IlAtMjU2In0=" + ) + .set("x-pagopa-idp-id", "spid") + .set("x-pagopa-login-type", "LV"); expect(result.status).toBe(200); expect(result.body).toStrictEqual({ token: getLoginSessionToken() }); diff --git a/src/routers/__tests__/session.ts b/src/routers/__tests__/session.ts index 32669a29..122a2864 100644 --- a/src/routers/__tests__/session.ts +++ b/src/routers/__tests__/session.ts @@ -1,6 +1,6 @@ import * as E from "fp-ts/lib/Either"; import supertest from "supertest"; -import { PublicSession } from "../../../generated/definitions/backend/PublicSession"; +import { PublicSession } from "../../../generated/definitions/session_manager/PublicSession"; import { basePath } from "../../payloads/response"; import { createOrRefreshSessionTokens, diff --git a/src/routers/features/cgn/index.ts b/src/routers/features/cgn/index.ts index 69400ca8..5fab24d7 100644 --- a/src/routers/features/cgn/index.ts +++ b/src/routers/features/cgn/index.ts @@ -69,6 +69,9 @@ export const isCgnActivated = () => firstCgnActivationRequestTime > 0; // 409 -> Cannot activate the user's cgn because another updateCgn request was found for this user or it is already active. // 403 -> Cannot activate a new CGN because the user is ineligible to get the CGN. addHandler(cgnRouter, "post", addPrefix("/activation"), (_, res) => { + if (ioDevServerConfig.features.bonus.cgn.hangOnActivation) { + return; + } // if there is no previous activation -> Request created -> send back the created id pipe( O.fromNullable(idActivationCgn), diff --git a/src/routers/features/idpay/payment.ts b/src/routers/features/idpay/payment.ts index 44fc0727..cbbe69a1 100644 --- a/src/routers/features/idpay/payment.ts +++ b/src/routers/features/idpay/payment.ts @@ -16,8 +16,8 @@ import { addIdPayHandler } from "./router"; const generateRandomAuthPaymentResponseDTO = (): AuthPaymentResponseDTO => { const amount = faker.datatype.number({ - min: 100, - max: 10000 + min: 10000, + max: 1000000 }); return { @@ -25,12 +25,12 @@ const generateRandomAuthPaymentResponseDTO = (): AuthPaymentResponseDTO => { initiativeId: Object.values(initiatives)[0]?.initiativeId ?? ulid(), status: getRandomEnumValue(PaymentStatusEnum), trxCode: faker.datatype.string(), - reward: amount, + rewardCents: amount, amountCents: amount, businessName: faker.commerce.productName(), initiativeName: faker.company.name(), trxDate: faker.date.recent(0), - residualBudget: faker.datatype.number({ + residualBudgetCents: faker.datatype.number({ min: 1000, max: 20000 }) diff --git a/src/routers/public.ts b/src/routers/public.ts index 7f15ed72..7cf3c59c 100644 --- a/src/routers/public.ts +++ b/src/routers/public.ts @@ -128,7 +128,7 @@ addHandler(publicRouter, "get", "/info", (_, res) => res.json(backendInfo)); addHandler(publicRouter, "get", "/ping", (_, res) => res.send("ok")); // test login -addHandler(publicRouter, "post", "/test-login", (req, res) => { +addHandler(publicRouter, "post", "/test-login", async (req, res) => { const { password } = req.body; if (password === "error") { res.status(500).json({ token: getLoginSessionToken() }); @@ -137,6 +137,22 @@ addHandler(publicRouter, "post", "/test-login", (req, res) => { res.json({ token: getLoginSessionToken() }); }, 3000); } else { + const lollipopPublicKeyHeaderValue = req.get( + DEFAULT_HEADER_LOLLIPOP_PUB_KEY + ); + const jwkPK = parseJwkOrError(lollipopPublicKeyHeaderValue); + + if (E.isLeft(jwkPK) || !JwkPublicKey.is(jwkPK.right)) { + res.sendStatus(400); + return; + } + const thumbprint = await jose.calculateJwkThumbprint( + jwkPK.right, + DEFAULT_LOLLIPOP_HASH_ALGORITHM + ); + setLollipopInfo(thumbprint, jwkPK.right); + + createOrRefreshEverySessionToken(); res.json({ token: getLoginSessionToken() }); } }); diff --git a/src/server.ts b/src/server.ts index 956e3c0a..ec531262 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,6 +2,7 @@ import { Millisecond } from "@pagopa/ts-commons/lib/units"; import bodyParser from "body-parser"; import express, { Application } from "express"; import morgan from "morgan"; +import cookieParser from "cookie-parser"; import { ioDevServerConfig } from "./config"; import { messageRouter } from "./features/messages/routers/messagesRouter"; import { pnRouter } from "./features/pn/routers/routers"; @@ -35,6 +36,10 @@ import { delayer } from "./utils/delay_middleware"; import { walletRouter as newWalletRouter } from "./features/payments"; import { serviceRouter as newServiceRouter } from "./features/services"; import { dashboardHomeRouter } from "./routers/configHomeDashboard/configHomeDashboard"; +import { fimsProviderRouter } from "./features/fims/routers/providerRouter"; +import { fimsRelyingPartyRouter } from "./features/fims/routers/relyingPartyRouter"; +import { fimsHistoryRouter } from "./features/fims/routers/historyRouter"; +import { trialSystemRouter } from "./features/trialSystem/routers"; // create express server const app: Application = express(); @@ -51,6 +56,7 @@ app.use( ) ); app.use(errorMiddleware); +app.use(cookieParser()); app.use(fastLoginMiddleware); [ @@ -82,7 +88,11 @@ app.use(fastLoginMiddleware); lollipopRouter, fastLoginRouter, newWalletRouter, - newServiceRouter + newServiceRouter, + fimsRelyingPartyRouter, + fimsProviderRouter, + fimsHistoryRouter, + trialSystemRouter ].forEach(r => app.use(r)); export default app; diff --git a/src/types/config.ts b/src/types/config.ts index 1e6e2eed..90d041e4 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -13,9 +13,12 @@ import { Detail_v2Enum } from "../../generated/definitions/backend/PaymentProble import { PreferredLanguages } from "../../generated/definitions/backend/PreferredLanguages"; import { PushNotificationsContentType } from "../../generated/definitions/backend/PushNotificationsContentType"; import { ReminderStatus } from "../../generated/definitions/backend/ReminderStatus"; +import { SubscriptionState } from "../../generated/definitions/trial_system/SubscriptionState"; +import { TrialId } from "../../generated/definitions/trial_system/TrialId"; import { MessagesConfig } from "../features/messages/types/messagesConfig"; import { WalletConfiguration } from "../features/payments/types/configuration"; import { ServiceConfiguration } from "../features/services/types/configuration"; +import { FIMSConfig } from "../features/fims/types/config"; import { AllowRandomValue } from "./allowRandomValue"; import { HttpResponseCode } from "./httpResponseCode"; @@ -198,7 +201,9 @@ export const IoDevServerConfig = t.interface({ // if true the user is eligible to the CGN isCgnEligible: t.boolean, // if true the user is eligible to the EYCA related activation - isEycaEligible: t.boolean + isEycaEligible: t.boolean, + // if true the handler does nothing, effectively timing out, use to test loading states + hangOnActivation: t.boolean }), AllowRandomValue ]) @@ -215,7 +220,8 @@ export const IoDevServerConfig = t.interface({ assertionRefValidityMS: t.number }) ]), - service: ServiceConfiguration + service: ServiceConfiguration, + fims: FIMSConfig }), t.partial({ wallet: WalletConfiguration @@ -225,6 +231,9 @@ export const IoDevServerConfig = t.interface({ sessionTTLinMS: t.number }) }), + t.partial({ + trials: t.record(TrialId, SubscriptionState) + }), AllowRandomValue ]) }); diff --git a/src/types/httpResponseCode.ts b/src/types/httpResponseCode.ts index 445a32d6..92db8ce6 100644 --- a/src/types/httpResponseCode.ts +++ b/src/types/httpResponseCode.ts @@ -9,3 +9,46 @@ export const HttpResponseCode = t.union([ t.literal(429), t.literal(500) ]); + +export const FailureHttpResponseCode = t.union([ + t.literal(400), // Bad Request + t.literal(401), // Unauthorized + t.literal(402), // Payment Required + t.literal(403), // Forbidden + t.literal(404), // Not Found + t.literal(405), // Method Not Allowed + t.literal(406), // Not Acceptable + t.literal(407), // Proxy Authentication Required + t.literal(408), // Request Timeout + t.literal(409), // Conflict + t.literal(410), // Gone + t.literal(411), // Length Required + t.literal(412), // Precondition Failed + t.literal(413), // Payload Too Large + t.literal(414), // URI Too Long + t.literal(415), // Unsupported Media Type + t.literal(416), // Range Not Satisfiable + t.literal(417), // Expectation Failed + t.literal(418), // I'm a teapot + t.literal(421), // Misdirected Request + t.literal(422), // Unprocessable Content + t.literal(423), // Locked + t.literal(424), // Failed Dependency + t.literal(425), // Too Early + t.literal(426), // Upgrade Required + t.literal(428), // Precondition Required + t.literal(429), // Too Many Requests + t.literal(431), // Request Header Fields Too Large + t.literal(451), // Unavailable For Legal Reasons + t.literal(500), // Internal Server Error + t.literal(501), // Not Implemented + t.literal(502), // Bad Gateway + t.literal(503), // Service Unavailable + t.literal(504), // Gateway Timeout + t.literal(505), // HTTP Version Not Supported + t.literal(506), // Variant Also Negotiates + t.literal(507), // Insufficient Storage + t.literal(508), // Loop Detected + t.literal(510), // Not Extended + t.literal(511) // Network Authentication Required +]); diff --git a/src/utils/variables.ts b/src/utils/variables.ts index 010f1224..5748c3fb 100644 --- a/src/utils/variables.ts +++ b/src/utils/variables.ts @@ -8,6 +8,7 @@ import { SIGNED_SIGNATURE_REQUEST_ID, WAIT_QTSP_SIGNATURE_REQUEST_ID } from "../payloads/features/fci/signature-request"; +import { serverUrl } from "./server"; export const frontMatterMyPortal = `--- it: @@ -109,11 +110,11 @@ export const frontMatter1CTAFims = `--- it: cta_1: text: "Fims SSO" - action: "iosso://http://localhost:3000/myportal_playground.html" + action: "iosso://${serverUrl}/fims/relyingParty/1/landingPage" en: cta_1: text: "Fims SSO" - action: "iosso://http://localhost:3000/myportal_playground.html" + action: "iosso://${serverUrl}/fims/relyingParty/1/landingPage" ---`; export const frontMatterCTAFCISignatureRequest = `--- diff --git a/tsconfig.json b/tsconfig.json index 4bbba82e..57d93dce 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, + "target": "es2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ diff --git a/yarn.lock b/yarn.lock index b0149ece..6249234a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -711,6 +711,16 @@ structured-headers "0.5.0" url "0.11.0" +"@noble/hashes@^1.1.2": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + +"@noble/secp256k1@^1.6.3": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" + integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw== + "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" @@ -1018,6 +1028,13 @@ dependencies: "@types/node" "*" +"@types/cookie-parser@^1.4.7": + version "1.4.7" + resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.7.tgz#c874471f888c72423d78d2b3c32d1e8579cf3c8f" + integrity sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw== + dependencies: + "@types/express" "*" + "@types/cookiejar@*": version "2.1.1" resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.1.tgz#90b68446364baf9efd8e8349bb36bd3852b75b80" @@ -1038,22 +1055,23 @@ "@types/node" "*" "@types/range-parser" "*" -"@types/express-serve-static-core@^4.17.18": - version "4.17.27" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.27.tgz#7a776191e47295d2a05962ecbb3a4ce97e38b401" - integrity sha512-e/sVallzUTPdyOTiqi8O8pMdBBphscvI6E4JYaKlja4Lm+zh7UFSSdW5VMkRbhDtmrONqOUHOXRguPsDckzxNA== +"@types/express-serve-static-core@^4.17.33": + version "4.19.0" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz#3ae8ab3767d98d0b682cda063c3339e1e86ccfaa" + integrity sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" + "@types/send" "*" -"@types/express@^4.17.13": - version "4.17.13" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" - integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== +"@types/express@*", "@types/express@^4.17.21": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== dependencies: "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.18" + "@types/express-serve-static-core" "^4.17.33" "@types/qs" "*" "@types/serve-static" "*" @@ -1121,6 +1139,11 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + "@types/morgan@^1.9.3": version "1.9.3" resolved "https://registry.yarnpkg.com/@types/morgan/-/morgan-1.9.3.tgz#ae04180dff02c437312bc0cfb1e2960086b2f540" @@ -1158,6 +1181,14 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + "@types/serve-static@*": version "1.13.3" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" @@ -1193,6 +1224,11 @@ dependencies: "@types/superagent" "*" +"@types/uuid@^9.0.8": + version "9.0.8" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" + integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== + "@types/xml2js@^0.4.11": version "0.4.11" resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.11.tgz#bf46a84ecc12c41159a7bd9cf51ae84129af0e79" @@ -1648,7 +1684,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.3.1: +base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -1665,7 +1701,25 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== -body-parser@1.20.1, body-parser@^1.19.2: +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +body-parser@^1.19.2: version "1.20.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== @@ -1781,6 +1835,17 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + call-me-maybe@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" @@ -2055,6 +2120,11 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + continuation-local-storage@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz#11f613f74e914fe9b34c92ad2d28fe6ae1db7ffb" @@ -2087,15 +2157,28 @@ convert-string@~0.1.0: resolved "https://registry.yarnpkg.com/convert-string/-/convert-string-0.1.0.tgz#79ce41a9bb0d03bcf72cdc6a8f3c56fbbc64410a" integrity sha1-ec5BqbsNA7z3LNxqjzxW+7xkQQo= +cookie-parser@^1.4.6: + version "1.4.6" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.6.tgz#3ac3a7d35a7a03bbc7e365073a26074824214594" + integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA== + dependencies: + cookie "0.4.1" + cookie-signature "1.0.6" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== cookiejar@^2.1.3: version "2.1.4" @@ -2253,6 +2336,15 @@ defer-to-connect@^1.0.1: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -2400,6 +2492,11 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -2471,6 +2568,18 @@ es-abstract@^1.19.0, es-abstract@^1.20.4: unbox-primitive "^1.0.2" which-typed-array "^1.1.9" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-set-tostringtag@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" @@ -2747,37 +2856,37 @@ expect@^27.4.6: jest-matcher-utils "^27.4.6" jest-message-util "^27.4.6" -express@^4.17.3: - version "4.18.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== +express@4.20.0: + version "4.20.0" + resolved "https://registry.yarnpkg.com/express/-/express-4.20.0.tgz#f1d08e591fcec770c07be4767af8eb9bcfd67c48" + integrity sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.1" + body-parser "1.20.3" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.5.0" + cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" etag "~1.8.1" finalhandler "1.2.0" fresh "0.5.2" http-errors "2.0.0" - merge-descriptors "1.0.1" + merge-descriptors "1.0.3" methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-to-regexp "0.1.10" proxy-addr "~2.0.7" qs "6.11.0" range-parser "~1.2.1" safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" + send "0.19.0" + serve-static "1.16.0" setprototypeof "1.2.0" statuses "2.0.1" type-is "~1.6.18" @@ -3005,6 +3114,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" @@ -3048,6 +3162,17 @@ get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: has "^1.0.3" has-symbols "^1.0.3" +get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -3234,6 +3359,13 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + has-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" @@ -3268,6 +3400,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hexoid@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" @@ -4263,6 +4402,15 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsontokens@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jsontokens/-/jsontokens-4.0.1.tgz#c3edf74a01160b2ca6d62b021b288edd59d1184a" + integrity sha512-+MO415LEN6M+3FGsRz4wU20g7N2JA+2j9d9+pGaNJHviG4L8N0qzavGyENw6fJqsq9CcrHOIL6iWX5yeTZ86+Q== + dependencies: + "@noble/hashes" "^1.1.2" + "@noble/secp256k1" "^1.6.3" + base64-js "^1.5.1" + keyv@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" @@ -4418,10 +4566,10 @@ memorystream@^0.3.1: resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI= -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== merge-stream@^2.0.0: version "2.0.0" @@ -4667,6 +4815,11 @@ object-inspect@^1.12.3: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + object-inspect@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" @@ -4884,10 +5037,10 @@ path-parse@^1.0.6, path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== path-type@^3.0.0: version "3.0.0" @@ -5040,6 +5193,13 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + qs@6.9.3: version "6.9.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" @@ -5082,6 +5242,16 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -5322,10 +5492,29 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.0.tgz#2bf4ed49f8af311b519c46f272bf6ac3baf38a92" + integrity sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" @@ -5337,6 +5526,18 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" @@ -5393,6 +5594,16 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -6039,6 +6250,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + v8-to-istanbul@^8.1.0: version "8.1.1" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed"