From fc39b8c0ca5a49d2c3c16db6184938d9c9cfeb4f Mon Sep 17 00:00:00 2001 From: Patrick Browne Date: Mon, 29 Jul 2024 16:55:28 +0200 Subject: [PATCH] refactor: Use env file instead of process.env everywhere --- .env.development | 5 ++- next.config.js | 15 ++----- src/components/header.tsx | 6 ++- src/domain/data.ts | 7 +-- src/domain/env.js | 56 ++++++++++++++++++++++++ src/domain/gever/message.ts | 10 +++-- src/domain/gever/soap.ts | 11 +++-- src/domain/gitlab-wiki-api.ts | 14 +++--- src/lib/assert.ts | 7 +++ src/lib/use-query-state.ts | 3 +- src/pages/_document.tsx | 11 ++--- src/pages/api/data-export.ts | 3 +- src/pages/api/debug-download.ts | 9 ++-- src/pages/api/graphql.ts | 12 +++-- src/pages/api/matomo-id.ts | 6 ++- src/pages/api/municipalities-data.csv.ts | 6 ++- src/rdf/queries.ts | 16 ++++--- 17 files changed, 144 insertions(+), 53 deletions(-) create mode 100644 src/domain/env.js create mode 100644 src/lib/assert.ts diff --git a/.env.development b/.env.development index b26a617d..7f289393 100644 --- a/.env.development +++ b/.env.development @@ -12,4 +12,7 @@ I18N_DOMAINS={"de": "www.elcom.local", "fr": "fr.elcom.local", "it": "it.elcom.l BASIC_AUTH_CREDENTIALS= EIAM_CERTIFICATE_PASSWORD= # EIAM_CERTIFICATE_PATH= -EIAM_CERTIFICATE_CONTENT= \ No newline at end of file +EIAM_CERTIFICATE_CONTENT= +FIRST_PERIOD=2009 +CURRENT_PERIOD=2025 +DEPLOYMENT=dev \ No newline at end of file diff --git a/next.config.js b/next.config.js index 7fe40375..f53eeeae 100644 --- a/next.config.js +++ b/next.config.js @@ -6,20 +6,13 @@ const withMDX = require("@next/mdx")(); const pkg = require("./package.json"); const { locales, defaultLocale } = require("./src/locales/locales.json"); -const { - I18N_DOMAINS, - WEBPACK_ASSET_PREFIX, - CURRENT_PERIOD = "2025", - FIRST_PERIOD = "2009", - DEPLOYMENT, - MATOMO_ID, -} = process.env; +const { I18N_DOMAINS, WEBPACK_ASSET_PREFIX, MATOMO_ID } = process.env; const buildEnv = { VERSION: `v${pkg.version}`, - DEPLOYMENT: DEPLOYMENT, - CURRENT_PERIOD: CURRENT_PERIOD, - FIRST_PERIOD: FIRST_PERIOD, + DEPLOYMENT: process.env.DEPLOYMENT, + CURRENT_PERIOD: process.env.CURRENT_PERIOD, + FIRST_PERIOD: process.env.CURRENT_PERIOD, }; console.log("Build Environment:", buildEnv); diff --git a/src/components/header.tsx b/src/components/header.tsx index 215eba71..bb15d4ea 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -1,6 +1,8 @@ import { Trans } from "@lingui/macro"; import { Box, Flex, Text } from "theme-ui"; +import { buildEnv } from "src/domain/env"; + import { LanguageMenu } from "./language-menu"; import { HomeLink } from "./links"; import { LogoDesktop, LogoMobile } from "./logo"; @@ -95,8 +97,8 @@ export const Logo = () => { sx={{ pl: [0, 6], textDecoration: "none", color: "monochrome800" }} > Strompreise Schweiz - {process.env.DEPLOYMENT && - ` [${process.env.DEPLOYMENT.toLocaleUpperCase()}]`} + {buildEnv.DEPLOYMENT && + ` [${buildEnv.DEPLOYMENT.toLocaleUpperCase()}]`} diff --git a/src/domain/data.ts b/src/domain/data.ts index 3eeadcdc..9cd12a75 100644 --- a/src/domain/data.ts +++ b/src/domain/data.ts @@ -1,6 +1,7 @@ import { scaleThreshold, range } from "d3"; import { useMemo } from "react"; +import { buildEnv } from "src/domain/env"; import { Observation as QueryObservation } from "src/graphql/queries"; import { useTheme } from "../themes"; @@ -67,15 +68,15 @@ export const useColorScale = ({ export type Entity = "municipality" | "operator" | "canton"; -if (!process.env.FIRST_PERIOD || !process.env.CURRENT_PERIOD) { +if (!buildEnv.FIRST_PERIOD || !buildEnv.CURRENT_PERIOD) { throw Error( `Please configure FIRST_PERIOD and CURRENT_PERIOD in next.config.js` ); } export const periods = range( - parseInt(process.env.CURRENT_PERIOD, 10), - parseInt(process.env.FIRST_PERIOD, 10) - 1, + parseInt(buildEnv.CURRENT_PERIOD, 10), + parseInt(buildEnv.FIRST_PERIOD, 10) - 1, -1 ).map((d) => d.toString()); diff --git a/src/domain/env.js b/src/domain/env.js new file mode 100644 index 00000000..f0c5a4ed --- /dev/null +++ b/src/domain/env.js @@ -0,0 +1,56 @@ +const { z } = require("zod"); + +const buildSchema = z.object({ + // Used to display a mention of the current deployment in development mode + DEPLOYMENT: z.string().optional(), + CURRENT_PERIOD: z.string().default("2025"), + FIRST_PERIOD: z.string().default("2009"), + VERSION: z.string().optional(), +}); + +// Define the schema for server-side variables +const serverSchema = z.object({ + // Gever document download + EIAM_CERTIFICATE_CONTENT: z.string().optional(), + EIAM_CERTIFICATE_PASSWORD: z.string(), + EIAM_CERTIFICATE_PATH: z.string(), + GEVER_BINDING_IPSTS: z.string(), + GEVER_BINDING_RPSTS: z.string(), + GEVER_BINDING_SERVICE: z.string(), + DEBUG_DOWNLOAD_SECRET: z + .string() + .default("GqQF$t$Fm^oddinivkY8TT8F^kRuRUJ$NJ5Jt%vQ"), + + ELCOM_ENV: z.string(), + + // Gitlab as CMS + GITLAB_WIKI_TOKEN: z.string(), + GITLAB_WIKI_URL: z.string(), + + // Tracking + MATOMO_ID: z.string(), + + // Apollo plugin + METRICS_PLUGIN_ENABLED: z.string(), + + NODE_ENV: z.string(), + + // Sparql + SPARQL_EDITOR: z.string().optional(), + SPARQL_ENDPOINT: z.string().default("https://test.lindas.admin.ch/query"), +}); + +module.exports = { + serverEnv: + typeof window !== "undefined" ? serverSchema.parse(process.env) : null, + + // Need to inline variables here so that they are replaced at build time + buildEnv: buildSchema.parse({ + DEPLOYMENT: process.env.DEPLOYMENT, + CURRENT_PERIOD: process.env.CURRENT_PERIOD, + FIRST_PERIOD: process.env.CURRENT_PERIOD, + VERSION: process.env.VERSION, + }), + + buildSchema: buildSchema, +}; diff --git a/src/domain/gever/message.ts b/src/domain/gever/message.ts index 713f68ca..ce90e354 100644 --- a/src/domain/gever/message.ts +++ b/src/domain/gever/message.ts @@ -4,7 +4,9 @@ import fs from "fs"; import { memoize } from "lodash"; import z from "zod"; +import { serverEnv } from "src/domain/env"; import { OperatorDocumentCategory } from "src/graphql/queries"; +import assert from "src/lib/assert"; import { truthy } from "src/lib/truthy"; import { decrypt, encrypt } from "./encrypt"; @@ -25,15 +27,17 @@ import { $$, } from "./utils"; +assert(!!serverEnv, "serverEnv must be defined"); + const bindings = { ipsts: - process.env.GEVER_BINDING_IPSTS || + serverEnv.GEVER_BINDING_IPSTS || "https://idp-cert.gate-r.eiam.admin.ch/auth/sts/v14/certificatetransport", rpsts: - process.env.GEVER_BINDING_RPSTS || + serverEnv.GEVER_BINDING_RPSTS || "https://feds-r.eiam.admin.ch/adfs/services/trust/13/issuedtokenmixedsymmetricbasic256", service: - process.env.GEVER_BINDING_SERVICE || + serverEnv.GEVER_BINDING_SERVICE || "https://api-bv.egov-abn.uvek.admin.ch/BusinessManagement/GeverService/GeverServiceAdvanced.svc", }; diff --git a/src/domain/gever/soap.ts b/src/domain/gever/soap.ts index 7b3b47ce..c5c64830 100644 --- a/src/domain/gever/soap.ts +++ b/src/domain/gever/soap.ts @@ -3,9 +3,14 @@ import https from "https"; import fetch from "node-fetch"; +import { serverEnv } from "src/domain/env"; + const getCertificateContent = () => { - const CERTIFICATE_PATH = process.env.EIAM_CERTIFICATE_PATH; - const CERTIFICATE_CONTENT = process.env.EIAM_CERTIFICATE_CONTENT; + if (!serverEnv) { + throw new Error("serverEnv must be defined"); + } + const CERTIFICATE_PATH = serverEnv?.EIAM_CERTIFICATE_PATH; + const CERTIFICATE_CONTENT = serverEnv?.EIAM_CERTIFICATE_CONTENT; if (CERTIFICATE_PATH) { if (!fs.existsSync(CERTIFICATE_PATH)) { throw new Error(`Certificate file does not exist ${CERTIFICATE_PATH}`); @@ -22,7 +27,7 @@ const getCertificateContent = () => { export const makeSslConfiguredAgent = () => { const pfx = getCertificateContent(); - const CERTIFICATE_PASSWORD = process.env.EIAM_CERTIFICATE_PASSWORD; + const CERTIFICATE_PASSWORD = serverEnv?.EIAM_CERTIFICATE_PASSWORD; if (!CERTIFICATE_PASSWORD) { throw new Error("EIAM_CERTIFICATE_PASSWORD must be defined in env"); diff --git a/src/domain/gitlab-wiki-api.ts b/src/domain/gitlab-wiki-api.ts index 503b439e..28f29af5 100644 --- a/src/domain/gitlab-wiki-api.ts +++ b/src/domain/gitlab-wiki-api.ts @@ -1,9 +1,11 @@ -import https from 'https' +import https from "https"; import os from "os"; import path from "path"; import fs from "fs-extra"; +import { serverEnv } from "src/domain/env"; + import { getWikiPage as getStaticWikiPage } from "./gitlab-wiki-static"; type WikiPage = { @@ -24,7 +26,7 @@ const CACHE_TTL = 1000; const fetchWithTimeout = async ( url: string, - options: RequestInit & { timeout?: number, agent?: https.Agent } = {} + options: RequestInit & { timeout?: number; agent?: https.Agent } = {} ) => { const { timeout } = options; @@ -65,7 +67,7 @@ const getCachedWikiPages = async ( // This is necessary for GLOBAL_AGENT to correctly override the agent while // the GLOBAL_AGENT_FORCE_GLOBAL_AGENT variable is set - agent: https.globalAgent + agent: https.globalAgent, }); const pages: WikiPages = await res.json(); @@ -78,7 +80,7 @@ const getCachedWikiPages = async ( export const getWikiPage = async ( slug: string ): Promise => { - if (!process.env.GITLAB_WIKI_URL || !process.env.GITLAB_WIKI_TOKEN) { + if (!serverEnv?.GITLAB_WIKI_URL || !serverEnv.GITLAB_WIKI_TOKEN) { throw Error( "Please set GITLAB_WIKI_URL and GITLAB_WIKI_TOKEN environment variables to fetch content from GitLab Wiki." ); @@ -86,8 +88,8 @@ export const getWikiPage = async ( try { const wikiPages = await getCachedWikiPages( - `${process.env.GITLAB_WIKI_URL}?with_content=1`, - process.env.GITLAB_WIKI_TOKEN + `${serverEnv.GITLAB_WIKI_URL}?with_content=1`, + serverEnv.GITLAB_WIKI_TOKEN ); return wikiPages.find((page) => page.slug === slug); diff --git a/src/lib/assert.ts b/src/lib/assert.ts new file mode 100644 index 00000000..75521640 --- /dev/null +++ b/src/lib/assert.ts @@ -0,0 +1,7 @@ +function assert(predicate: boolean, message: string): asserts predicate { + if (!predicate) { + throw new Error(message); + } +} + +export default assert; diff --git a/src/lib/use-query-state.ts b/src/lib/use-query-state.ts index 64c80e23..e8a254f3 100644 --- a/src/lib/use-query-state.ts +++ b/src/lib/use-query-state.ts @@ -1,6 +1,7 @@ import { useRouter } from "next/router"; import { useCallback } from "react"; +import { buildEnv } from "src/domain/env"; const ensureArray = (input: string | string[]): string[] => Array.isArray(input) ? input : [input]; @@ -22,7 +23,7 @@ const queryStateKeys = [ const queryStateDefaults = { id: "261", - period: process.env.CURRENT_PERIOD, + period: buildEnv.CURRENT_PERIOD, category: "H4", priceComponent: "total", product: "standard", diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index 05546d17..cf8d2692 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -1,15 +1,12 @@ -import Document, { - Html, - Head, - Main, - NextScript, -} from "next/document"; +import Document, { Html, Head, Main, NextScript } from "next/document"; + +import { buildEnv } from "src/domain/env"; class MyDocument extends Document { render() { return ( diff --git a/src/pages/api/data-export.ts b/src/pages/api/data-export.ts index 3344041f..de152d30 100644 --- a/src/pages/api/data-export.ts +++ b/src/pages/api/data-export.ts @@ -1,6 +1,7 @@ import { csvFormat } from "d3"; import { NextApiRequest, NextApiResponse } from "next"; +import { buildEnv } from "src/domain/env"; import { parseLocaleString } from "src/locales/locales"; import { @@ -11,7 +12,7 @@ import { export default async (req: NextApiRequest, res: NextApiResponse) => { const locale = parseLocaleString(req.query.locale?.toString()); - const period = req.query.period?.toString() ?? process.env.CURRENT_PERIOD!; + const period = req.query.period?.toString() ?? buildEnv.CURRENT_PERIOD!; const cube = await getObservationsCube(); diff --git a/src/pages/api/debug-download.ts b/src/pages/api/debug-download.ts index 2e5df970..8d2449e0 100644 --- a/src/pages/api/debug-download.ts +++ b/src/pages/api/debug-download.ts @@ -1,13 +1,16 @@ import { InferAPIResponse } from "nextkit"; +import { serverEnv } from "src/domain/env"; +import assert from "src/lib/assert"; + import { searchGeverDocuments } from "../../domain/gever"; import { fetchOperatorInfo } from "../../rdf/search-queries"; import { endpointUrl } from "../../rdf/sparql-client"; import { api } from "../../server/nextkit"; -const secret = - process.env.DEBUG_DOWNLOAD_SECRET || - "GqQF$t$Fm^oddinivkY8TT8F^kRuRUJ$NJ5Jt%vQ"; +assert(!!serverEnv, "serverEnv is not defined"); + +const secret = serverEnv.DEBUG_DOWNLOAD_SECRET; const handler = api({ GET: async ({ req }) => { diff --git a/src/pages/api/graphql.ts b/src/pages/api/graphql.ts index ae45a9f7..8c34bae1 100644 --- a/src/pages/api/graphql.ts +++ b/src/pages/api/graphql.ts @@ -6,24 +6,28 @@ import responseCachePlugin from "@apollo/server-plugin-response-cache"; import { startServerAndCreateNextHandler } from "@as-integrations/next"; import { NextApiHandler } from "next"; +import { serverEnv } from "src/domain/env"; import { resolvers } from "src/graphql/resolvers"; import typeDefs from "src/graphql/schema.graphql"; import { context } from "src/graphql/server-context"; +import assert from "src/lib/assert"; import { metricsPlugin } from "./metricsPlugin"; +assert(!!serverEnv, "serverEnv is not defined"); + const server = new ApolloServer({ typeDefs, resolvers, apollo: {}, - introspection: process.env.NODE_ENV === "development", + introspection: serverEnv.NODE_ENV === "development", plugins: [ metricsPlugin({ enabled: - process.env.NODE_ENV === "development" || - process.env.METRICS_PLUGIN_ENABLED === "true", + serverEnv.NODE_ENV === "development" || + serverEnv.METRICS_PLUGIN_ENABLED === "true", }), - process.env.NODE_ENV === "development" + serverEnv.NODE_ENV === "development" ? ApolloServerPluginLandingPageLocalDefault({ embed: false }) : ApolloServerPluginLandingPageDisabled(), ApolloServerPluginCacheControl({ diff --git a/src/pages/api/matomo-id.ts b/src/pages/api/matomo-id.ts index d2b657c2..61906478 100644 --- a/src/pages/api/matomo-id.ts +++ b/src/pages/api/matomo-id.ts @@ -1,5 +1,9 @@ import { NextApiRequest, NextApiResponse } from "next"; +import { serverEnv } from "src/domain/env"; +import assert from "src/lib/assert"; + export default async (req: NextApiRequest, res: NextApiResponse) => { - res.json({ matomoId: process.env.MATOMO_ID }); + assert(!!serverEnv, "serverEnv is not defined"); + res.json({ matomoId: serverEnv.MATOMO_ID }); }; diff --git a/src/pages/api/municipalities-data.csv.ts b/src/pages/api/municipalities-data.csv.ts index 710dee86..b35a2d69 100644 --- a/src/pages/api/municipalities-data.csv.ts +++ b/src/pages/api/municipalities-data.csv.ts @@ -3,6 +3,8 @@ import { mapValues } from "lodash"; import { NextApiHandler } from "next"; import { z } from "zod"; +import { buildEnv } from "src/domain/env"; + const MunicipalityInfo = z .object({ netzbetreiber: z.string(), @@ -135,7 +137,9 @@ WHERE { }; const handler: NextApiHandler = async (req, res) => { - const period = Number(req.query.period?.toString() ?? 2024!); + const period = Number( + req.query.period?.toString() ?? buildEnv.CURRENT_PERIOD + ); const data = await fetchMunicipalitiesInfo(period); const filename = `municipalities-data-${period}.csv`; const csv = csvFormat(data, [ diff --git a/src/rdf/queries.ts b/src/rdf/queries.ts index 537c52c4..b70b6fc4 100644 --- a/src/rdf/queries.ts +++ b/src/rdf/queries.ts @@ -11,7 +11,9 @@ import { Literal, NamedNode } from "rdf-js"; import ParsingClient from "sparql-http-client/ParsingClient"; import { LRUCache } from "typescript-lru-cache"; +import { serverEnv } from "src/domain/env"; import { OperatorDocumentCategory } from "src/graphql/resolver-types"; +import assert from "src/lib/assert"; import { Observation, parseObservation } from "src/lib/observations"; import { defaultLocale } from "src/locales/locales"; @@ -27,12 +29,13 @@ export const CANTON_OBSERVATIONS_CUBE = export const SWISS_OBSERVATIONS_CUBE = "https://energy.ld.admin.ch/elcom/electricityprice-swiss"; -export const createSource = () => - new Source({ +export const createSource = () => { + assert(!!serverEnv, "serverEnv is not defined"); + return new Source({ queryOperation: "postDirect", - endpointUrl: - process.env.SPARQL_ENDPOINT ?? "https://test.lindas.admin.ch/query", + endpointUrl: serverEnv.SPARQL_ENDPOINT, }); +}; export const getCube = async ({ iri, @@ -615,7 +618,8 @@ export const getOperatorDocuments = async ({ }; export const getSparqlEditorUrl = (query: string): string | null => { - return process.env.SPARQL_EDITOR - ? `${process.env.SPARQL_EDITOR}#query=${encodeURIComponent(query)}` + assert(!!serverEnv, "serverEnv is not defined"); + return serverEnv.SPARQL_EDITOR + ? `${serverEnv.SPARQL_EDITOR}#query=${encodeURIComponent(query)}` : query; };