From 3990c1a4adb8d37ca5076e3058fa8994a87a0d8e Mon Sep 17 00:00:00 2001 From: Meierschlumpf Date: Sun, 23 Jul 2023 14:18:10 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Add=20env=20variable=20val?= =?UTF-8?q?idation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 23 +++++++ next.config.js | 1 + package.json | 5 +- .../Customization/CustomizationAccordeon.tsx | 3 +- src/env.js | 66 +++++++++++++++++++ src/pages/_app.tsx | 17 ++--- src/pages/api/docker/DockerSingleton.ts | 7 +- src/pages/api/trpc/[trpc].ts | 3 +- .../api/routers/docker/DockerSingleton.ts | 7 +- src/tools/server/getPackageVersion.ts | 4 +- src/utils/api.ts | 6 +- yarn.lock | 23 +++++++ 12 files changed, 140 insertions(+), 25 deletions(-) create mode 100644 .env.example create mode 100644 src/env.js diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..37c6640d7 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# Since the ".env" file is gitignored, you can use the ".env.example" file to +# build a new ".env" file when you clone the repo. Keep this file up-to-date +# when you add new variables to `.env`. + +# This file will be committed to version control, so make sure not to have any +# secrets in it. If you are cloning this repo, create a copy of this file named +# ".env" and populate it with your secrets. + +# When adding additional environment variables, the schema in "/src/env.js" +# should be updated accordingly. + +# Prisma +# https://www.prisma.io/docs/reference/database-reference/connection-urls#env +DATABASE_URL="file:./db.sqlite" + +# Next Auth +# You can generate a new secret on the command line with: +# openssl rand -base64 32 +# https://next-auth.js.org/configuration/options#secret +# NEXTAUTH_SECRET="" +NEXTAUTH_URL="http://localhost:3000" + +NEXTAUTH_SECRET="" diff --git a/next.config.js b/next.config.js index cd8c563ea..2e183dbb4 100644 --- a/next.config.js +++ b/next.config.js @@ -1,3 +1,4 @@ +require('./src/env'); const { i18n } = require('./next-i18next.config'); const withBundleAnalyzer = require('@next/bundle-analyzer')({ diff --git a/package.json b/package.json index 49f0b7b82..b2ac01ced 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@nivo/core": "^0.83.0", "@nivo/line": "^0.83.0", "@react-native-async-storage/async-storage": "^1.18.1", + "@t3-oss/env-nextjs": "^0.6.0", "@tabler/icons-react": "^2.18.0", "@tanstack/query-async-storage-persister": "^4.27.1", "@tanstack/query-sync-storage-persister": "^4.27.1", @@ -155,7 +156,9 @@ "^[./]" ], "importOrderSeparation": true, - "plugins": ["@trivago/prettier-plugin-sort-imports"], + "plugins": [ + "@trivago/prettier-plugin-sort-imports" + ], "importOrderSortSpecifiers": true }, "eslintConfig": { diff --git a/src/components/Settings/Customization/CustomizationAccordeon.tsx b/src/components/Settings/Customization/CustomizationAccordeon.tsx index 939b8c2ac..33789f50c 100644 --- a/src/components/Settings/Customization/CustomizationAccordeon.tsx +++ b/src/components/Settings/Customization/CustomizationAccordeon.tsx @@ -9,6 +9,7 @@ import { } from '@tabler/icons-react'; import { i18n, useTranslation } from 'next-i18next'; import { ReactNode } from 'react'; +import { env } from '~/env'; import { AccessibilitySettings } from './Accessibility/AccessibilitySettings'; import { GridstackConfiguration } from './Layout/GridstackConfiguration'; @@ -130,7 +131,7 @@ const getItems = () => { ), }, ]; - if (process.env.NODE_ENV === 'development') { + if (env.NEXT_PUBLIC_NODE_ENV === 'development') { items.push({ id: 'dev', image: , diff --git a/src/env.js b/src/env.js new file mode 100644 index 000000000..c45f748cb --- /dev/null +++ b/src/env.js @@ -0,0 +1,66 @@ +const { z } = require('zod'); +const { createEnv } = require('@t3-oss/env-nextjs'); + +const portSchema = z.string().regex(/\d+/).transform(Number).optional() +const envSchema = z.enum(["development", "test", "production"]); + +const env = createEnv({ + /** + * Specify your server-side environment variables schema here. This way you can ensure the app + * isn't built with invalid env vars. + */ + server: { + DATABASE_URL: z.string().url(), + NODE_ENV: envSchema, + NEXTAUTH_SECRET: + process.env.NODE_ENV === "production" + ? z.string().min(1) + : z.string().min(1).optional(), + NEXTAUTH_URL: z.preprocess( + // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL + // Since NextAuth.js automatically uses the VERCEL_URL if present. + (str) => process.env.VERCEL_URL ?? str, + // VERCEL_URL doesn't include `https` so it cant be validated as a URL + process.env.VERCEL ? z.string().min(1) : z.string().url(), + ), + DEFAULT_COLOR_SCHEME: z.enum(['light', 'dark']).optional().default('light'), + DOCKER_HOST: z.string().optional(), + DOCKER_PORT: z.string().regex(/\d+/).transform(Number).optional(), + PORT: portSchema + }, + + /** + * Specify your client-side environment variables schema here. This way you can ensure the app + * isn't built with invalid env vars. To expose them to the client, prefix them with + * `NEXT_PUBLIC_`. + */ + client: { + // NEXT_PUBLIC_CLIENTVAR: z.string().min(1), + NEXT_PUBLIC_PORT: portSchema, + NEXT_PUBLIC_NODE_ENV: envSchema + }, + + /** + * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. + * middlewares) or client-side so we need to destruct manually. + */ + runtimeEnv: { + DATABASE_URL: process.env.DATABASE_URL, + NODE_ENV: process.env.NODE_ENV, + NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, + NEXTAUTH_URL: process.env.NEXTAUTH_URL, + NEXT_PUBLIC_DISABLE_EDIT_MODE: process.env.DISABLE_EDIT_MODE, + DISABLE_EDIT_MODE: process.env.DISABLE_EDIT_MODE, + DEFAULT_COLOR_SCHEME: process.env.DEFAULT_COLOR_SCHEME, + DOCKER_HOST: process.env.DOCKER_HOST, + DOCKER_PORT: process.env.DOCKER_PORT, + VERCEL_URL: process.env.VERCEL_URL, + PORT: process.env.PORT, + NEXT_PUBLIC_PORT: process.env.PORT, + NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV + }, +}); + +module.exports = { + env +} \ No newline at end of file diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 37ed1a412..3a21f1960 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -14,9 +14,10 @@ import { AppProps } from 'next/app'; import Head from 'next/head'; import { useEffect, useState } from 'react'; import 'video.js/dist/video-js.css'; +import { env } from '~/env.js'; import { api } from '~/utils/api'; -import nextI18nextConfig from '../../next-i18next.config'; +import nextI18nextConfig from '../../next-i18next.config.js'; import { ChangeAppPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal'; import { ChangeWidgetPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal'; import { EditAppModal } from '../components/Dashboard/Modals/EditAppModal/EditAppModal'; @@ -149,26 +150,22 @@ function App( } App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => { - const disableEditMode = - process.env.DISABLE_EDIT_MODE && process.env.DISABLE_EDIT_MODE.toLowerCase() === 'true'; - if (disableEditMode) { + if (process.env.DISABLE_EDIT_MODE === 'true') { Consola.warn( 'EXPERIMENTAL: You have disabled the edit mode. Modifications are no longer possible and any requests on the API will be dropped. If you want to disable this, unset the DISABLE_EDIT_MODE environment variable. This behaviour may be removed in future versions of Homarr' ); } - if (process.env.DEFAULT_COLOR_SCHEME !== undefined) { - Consola.debug(`Overriding the default color scheme with ${process.env.DEFAULT_COLOR_SCHEME}`); + if (env.DEFAULT_COLOR_SCHEME !== 'light') { + Consola.debug(`Overriding the default color scheme with ${env.DEFAULT_COLOR_SCHEME}`); } - const colorScheme: ColorScheme = (process.env.DEFAULT_COLOR_SCHEME as ColorScheme) ?? 'light'; - return { pageProps: { colorScheme: getCookie('color-scheme', ctx) || 'light', packageAttributes: getServiceSidePackageAttributes(), - editModeEnabled: !disableEditMode, - defaultColorScheme: colorScheme, + editModeEnabled: process.env.DISABLE_EDIT_MODE !== 'true', + defaultColorScheme: env.DEFAULT_COLOR_SCHEME, }, }; }; diff --git a/src/pages/api/docker/DockerSingleton.ts b/src/pages/api/docker/DockerSingleton.ts index 9694059be..4602ed7d7 100644 --- a/src/pages/api/docker/DockerSingleton.ts +++ b/src/pages/api/docker/DockerSingleton.ts @@ -1,4 +1,5 @@ import Docker from 'dockerode'; +import { env } from '~/env'; export default class DockerSingleton extends Docker { private static dockerInstance: DockerSingleton; @@ -10,10 +11,8 @@ export default class DockerSingleton extends Docker { public static getInstance(): DockerSingleton { if (!DockerSingleton.dockerInstance) { DockerSingleton.dockerInstance = new Docker({ - // If env variable DOCKER_HOST is not set, it will use the default socket - ...(process.env.DOCKER_HOST && { host: process.env.DOCKER_HOST }), - // Same thing for docker port - ...(process.env.DOCKER_PORT && { port: process.env.DOCKER_PORT }), + host: env.DOCKER_HOST, + port: env.DOCKER_PORT, }); } return DockerSingleton.dockerInstance; diff --git a/src/pages/api/trpc/[trpc].ts b/src/pages/api/trpc/[trpc].ts index d3f7428f9..7bec97bf0 100644 --- a/src/pages/api/trpc/[trpc].ts +++ b/src/pages/api/trpc/[trpc].ts @@ -1,5 +1,6 @@ import { createNextApiHandler } from '@trpc/server/adapters/next'; import Consola from 'consola'; +import { env } from '~/env'; import { rootRouter } from '~/server/api/root'; import { createTRPCContext } from '~/server/api/trpc'; @@ -8,7 +9,7 @@ export default createNextApiHandler({ router: rootRouter, createContext: createTRPCContext, onError: - process.env.NODE_ENV === 'development' + env.NODE_ENV === 'development' ? ({ path, error }) => { Consola.error(`❌ tRPC failed on ${path ?? ''}: ${error.message}`); } diff --git a/src/server/api/routers/docker/DockerSingleton.ts b/src/server/api/routers/docker/DockerSingleton.ts index 9694059be..4602ed7d7 100644 --- a/src/server/api/routers/docker/DockerSingleton.ts +++ b/src/server/api/routers/docker/DockerSingleton.ts @@ -1,4 +1,5 @@ import Docker from 'dockerode'; +import { env } from '~/env'; export default class DockerSingleton extends Docker { private static dockerInstance: DockerSingleton; @@ -10,10 +11,8 @@ export default class DockerSingleton extends Docker { public static getInstance(): DockerSingleton { if (!DockerSingleton.dockerInstance) { DockerSingleton.dockerInstance = new Docker({ - // If env variable DOCKER_HOST is not set, it will use the default socket - ...(process.env.DOCKER_HOST && { host: process.env.DOCKER_HOST }), - // Same thing for docker port - ...(process.env.DOCKER_PORT && { port: process.env.DOCKER_PORT }), + host: env.DOCKER_HOST, + port: env.DOCKER_PORT, }); } return DockerSingleton.dockerInstance; diff --git a/src/tools/server/getPackageVersion.ts b/src/tools/server/getPackageVersion.ts index dfbed1606..cb7986d54 100644 --- a/src/tools/server/getPackageVersion.ts +++ b/src/tools/server/getPackageVersion.ts @@ -1,8 +1,10 @@ +import { env } from '~/env'; + import packageJson from '../../../package.json'; const getServerPackageVersion = (): string | undefined => packageJson.version; -const getServerNodeEnvironment = (): 'development' | 'production' | 'test' => process.env.NODE_ENV; +const getServerNodeEnvironment = () => env.NODE_ENV; const getDependencies = (): PackageJsonDependencies => packageJson.dependencies; diff --git a/src/utils/api.ts b/src/utils/api.ts index 7a3d03785..241c0af95 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -8,6 +8,7 @@ import { createTRPCProxyClient, httpBatchLink, loggerLink } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server'; import superjson from 'superjson'; +import { env } from '~/env'; import { type RootRouter } from '~/server/api/root'; const getTrpcConfiguration = () => ({ @@ -26,7 +27,7 @@ const getTrpcConfiguration = () => ({ links: [ loggerLink({ enabled: (opts) => - process.env.NODE_ENV === 'development' || + env.NEXT_PUBLIC_NODE_ENV === 'development' || (opts.direction === 'down' && opts.result instanceof Error), }), httpBatchLink({ @@ -37,8 +38,7 @@ const getTrpcConfiguration = () => ({ const getBaseUrl = () => { if (typeof window !== 'undefined') return ''; // browser should use relative url - if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url - return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost + return `http://localhost:${env.NEXT_PUBLIC_PORT ?? 3000}`; // dev SSR should use localhost }; /** A set of type-safe react-query hooks for your tRPC API. */ diff --git a/yarn.lock b/yarn.lock index df67a75fe..19bb809af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1714,6 +1714,28 @@ __metadata: languageName: node linkType: hard +"@t3-oss/env-core@npm:0.6.0": + version: 0.6.0 + resolution: "@t3-oss/env-core@npm:0.6.0" + peerDependencies: + typescript: ">=4.7.2" + zod: ^3.0.0 + checksum: 00c5b8e2d893f85e9d33099fded1e9ee1c74e642144b91d60096d31ed5bcd09986f14b275316568aa1a1f42d1b01a34b67dcf1396e11d837ff5c11b4bfb56a3a + languageName: node + linkType: hard + +"@t3-oss/env-nextjs@npm:^0.6.0": + version: 0.6.0 + resolution: "@t3-oss/env-nextjs@npm:0.6.0" + dependencies: + "@t3-oss/env-core": 0.6.0 + peerDependencies: + typescript: ">=4.7.2" + zod: ^3.0.0 + checksum: d3708558241bcf857dfcfbc778a4d0166a5e690414893d7a4eb95dcafa12810d4fdc1cffe41402004acdd0d8f558f9369499bd9032d04e158d8698b5e85c7f32 + languageName: node + linkType: hard + "@tabler/icons-react@npm:^2.18.0": version: 2.26.0 resolution: "@tabler/icons-react@npm:2.26.0" @@ -5537,6 +5559,7 @@ __metadata: "@nivo/core": ^0.83.0 "@nivo/line": ^0.83.0 "@react-native-async-storage/async-storage": ^1.18.1 + "@t3-oss/env-nextjs": ^0.6.0 "@tabler/icons-react": ^2.18.0 "@tanstack/query-async-storage-persister": ^4.27.1 "@tanstack/query-sync-storage-persister": ^4.27.1