mirror of
				https://github.com/ajnart/homarr.git
				synced 2025-10-31 10:36:02 +01:00 
			
		
		
		
	♻️ Add env variable validation
This commit is contained in:
		
							
								
								
									
										23
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							| @@ -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="" | ||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | require('./src/env'); | ||||||
| const { i18n } = require('./next-i18next.config'); | const { i18n } = require('./next-i18next.config'); | ||||||
|  |  | ||||||
| const withBundleAnalyzer = require('@next/bundle-analyzer')({ | const withBundleAnalyzer = require('@next/bundle-analyzer')({ | ||||||
|   | |||||||
| @@ -42,6 +42,7 @@ | |||||||
|     "@nivo/core": "^0.83.0", |     "@nivo/core": "^0.83.0", | ||||||
|     "@nivo/line": "^0.83.0", |     "@nivo/line": "^0.83.0", | ||||||
|     "@react-native-async-storage/async-storage": "^1.18.1", |     "@react-native-async-storage/async-storage": "^1.18.1", | ||||||
|  |     "@t3-oss/env-nextjs": "^0.6.0", | ||||||
|     "@tabler/icons-react": "^2.18.0", |     "@tabler/icons-react": "^2.18.0", | ||||||
|     "@tanstack/query-async-storage-persister": "^4.27.1", |     "@tanstack/query-async-storage-persister": "^4.27.1", | ||||||
|     "@tanstack/query-sync-storage-persister": "^4.27.1", |     "@tanstack/query-sync-storage-persister": "^4.27.1", | ||||||
| @@ -155,7 +156,9 @@ | |||||||
|       "^[./]" |       "^[./]" | ||||||
|     ], |     ], | ||||||
|     "importOrderSeparation": true, |     "importOrderSeparation": true, | ||||||
|     "plugins": ["@trivago/prettier-plugin-sort-imports"], |     "plugins": [ | ||||||
|  |       "@trivago/prettier-plugin-sort-imports" | ||||||
|  |     ], | ||||||
|     "importOrderSortSpecifiers": true |     "importOrderSortSpecifiers": true | ||||||
|   }, |   }, | ||||||
|   "eslintConfig": { |   "eslintConfig": { | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import { | |||||||
| } from '@tabler/icons-react'; | } from '@tabler/icons-react'; | ||||||
| import { i18n, useTranslation } from 'next-i18next'; | import { i18n, useTranslation } from 'next-i18next'; | ||||||
| import { ReactNode } from 'react'; | import { ReactNode } from 'react'; | ||||||
|  | import { env } from '~/env'; | ||||||
|  |  | ||||||
| import { AccessibilitySettings } from './Accessibility/AccessibilitySettings'; | import { AccessibilitySettings } from './Accessibility/AccessibilitySettings'; | ||||||
| import { GridstackConfiguration } from './Layout/GridstackConfiguration'; | 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({ |     items.push({ | ||||||
|       id: 'dev', |       id: 'dev', | ||||||
|       image: <IconCode />, |       image: <IconCode />, | ||||||
|   | |||||||
							
								
								
									
										66
									
								
								src/env.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/env.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | } | ||||||
| @@ -14,9 +14,10 @@ import { AppProps } from 'next/app'; | |||||||
| import Head from 'next/head'; | import Head from 'next/head'; | ||||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||||
| import 'video.js/dist/video-js.css'; | import 'video.js/dist/video-js.css'; | ||||||
|  | import { env } from '~/env.js'; | ||||||
| import { api } from '~/utils/api'; | 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 { ChangeAppPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal'; | ||||||
| import { ChangeWidgetPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal'; | import { ChangeWidgetPositionModal } from '../components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal'; | ||||||
| import { EditAppModal } from '../components/Dashboard/Modals/EditAppModal/EditAppModal'; | import { EditAppModal } from '../components/Dashboard/Modals/EditAppModal/EditAppModal'; | ||||||
| @@ -149,26 +150,22 @@ function App( | |||||||
| } | } | ||||||
|  |  | ||||||
| App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => { | App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => { | ||||||
|   const disableEditMode = |   if (process.env.DISABLE_EDIT_MODE === 'true') { | ||||||
|     process.env.DISABLE_EDIT_MODE && process.env.DISABLE_EDIT_MODE.toLowerCase() === 'true'; |  | ||||||
|   if (disableEditMode) { |  | ||||||
|     Consola.warn( |     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' |       '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) { |   if (env.DEFAULT_COLOR_SCHEME !== 'light') { | ||||||
|     Consola.debug(`Overriding the default color scheme with ${process.env.DEFAULT_COLOR_SCHEME}`); |     Consola.debug(`Overriding the default color scheme with ${env.DEFAULT_COLOR_SCHEME}`); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const colorScheme: ColorScheme = (process.env.DEFAULT_COLOR_SCHEME as ColorScheme) ?? 'light'; |  | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|     pageProps: { |     pageProps: { | ||||||
|       colorScheme: getCookie('color-scheme', ctx) || 'light', |       colorScheme: getCookie('color-scheme', ctx) || 'light', | ||||||
|       packageAttributes: getServiceSidePackageAttributes(), |       packageAttributes: getServiceSidePackageAttributes(), | ||||||
|       editModeEnabled: !disableEditMode, |       editModeEnabled: process.env.DISABLE_EDIT_MODE !== 'true', | ||||||
|       defaultColorScheme: colorScheme, |       defaultColorScheme: env.DEFAULT_COLOR_SCHEME, | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import Docker from 'dockerode'; | import Docker from 'dockerode'; | ||||||
|  | import { env } from '~/env'; | ||||||
|  |  | ||||||
| export default class DockerSingleton extends Docker { | export default class DockerSingleton extends Docker { | ||||||
|   private static dockerInstance: DockerSingleton; |   private static dockerInstance: DockerSingleton; | ||||||
| @@ -10,10 +11,8 @@ export default class DockerSingleton extends Docker { | |||||||
|   public static getInstance(): DockerSingleton { |   public static getInstance(): DockerSingleton { | ||||||
|     if (!DockerSingleton.dockerInstance) { |     if (!DockerSingleton.dockerInstance) { | ||||||
|       DockerSingleton.dockerInstance = new Docker({ |       DockerSingleton.dockerInstance = new Docker({ | ||||||
|         // If env variable DOCKER_HOST is not set, it will use the default socket |         host: env.DOCKER_HOST, | ||||||
|         ...(process.env.DOCKER_HOST && { host: process.env.DOCKER_HOST }), |         port: env.DOCKER_PORT, | ||||||
|         // Same thing for docker port |  | ||||||
|         ...(process.env.DOCKER_PORT && { port: process.env.DOCKER_PORT }), |  | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|     return DockerSingleton.dockerInstance; |     return DockerSingleton.dockerInstance; | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import { createNextApiHandler } from '@trpc/server/adapters/next'; | import { createNextApiHandler } from '@trpc/server/adapters/next'; | ||||||
| import Consola from 'consola'; | import Consola from 'consola'; | ||||||
|  | import { env } from '~/env'; | ||||||
| import { rootRouter } from '~/server/api/root'; | import { rootRouter } from '~/server/api/root'; | ||||||
| import { createTRPCContext } from '~/server/api/trpc'; | import { createTRPCContext } from '~/server/api/trpc'; | ||||||
|  |  | ||||||
| @@ -8,7 +9,7 @@ export default createNextApiHandler({ | |||||||
|   router: rootRouter, |   router: rootRouter, | ||||||
|   createContext: createTRPCContext, |   createContext: createTRPCContext, | ||||||
|   onError: |   onError: | ||||||
|     process.env.NODE_ENV === 'development' |     env.NODE_ENV === 'development' | ||||||
|       ? ({ path, error }) => { |       ? ({ path, error }) => { | ||||||
|           Consola.error(`❌ tRPC failed on ${path ?? '<no-path>'}: ${error.message}`); |           Consola.error(`❌ tRPC failed on ${path ?? '<no-path>'}: ${error.message}`); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import Docker from 'dockerode'; | import Docker from 'dockerode'; | ||||||
|  | import { env } from '~/env'; | ||||||
|  |  | ||||||
| export default class DockerSingleton extends Docker { | export default class DockerSingleton extends Docker { | ||||||
|   private static dockerInstance: DockerSingleton; |   private static dockerInstance: DockerSingleton; | ||||||
| @@ -10,10 +11,8 @@ export default class DockerSingleton extends Docker { | |||||||
|   public static getInstance(): DockerSingleton { |   public static getInstance(): DockerSingleton { | ||||||
|     if (!DockerSingleton.dockerInstance) { |     if (!DockerSingleton.dockerInstance) { | ||||||
|       DockerSingleton.dockerInstance = new Docker({ |       DockerSingleton.dockerInstance = new Docker({ | ||||||
|         // If env variable DOCKER_HOST is not set, it will use the default socket |         host: env.DOCKER_HOST, | ||||||
|         ...(process.env.DOCKER_HOST && { host: process.env.DOCKER_HOST }), |         port: env.DOCKER_PORT, | ||||||
|         // Same thing for docker port |  | ||||||
|         ...(process.env.DOCKER_PORT && { port: process.env.DOCKER_PORT }), |  | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|     return DockerSingleton.dockerInstance; |     return DockerSingleton.dockerInstance; | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
|  | import { env } from '~/env'; | ||||||
|  |  | ||||||
| import packageJson from '../../../package.json'; | import packageJson from '../../../package.json'; | ||||||
|  |  | ||||||
| const getServerPackageVersion = (): string | undefined => packageJson.version; | 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; | const getDependencies = (): PackageJsonDependencies => packageJson.dependencies; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import { createTRPCProxyClient, httpBatchLink, loggerLink } from '@trpc/client'; | |||||||
| import { createTRPCNext } from '@trpc/next'; | import { createTRPCNext } from '@trpc/next'; | ||||||
| import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server'; | import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server'; | ||||||
| import superjson from 'superjson'; | import superjson from 'superjson'; | ||||||
|  | import { env } from '~/env'; | ||||||
| import { type RootRouter } from '~/server/api/root'; | import { type RootRouter } from '~/server/api/root'; | ||||||
|  |  | ||||||
| const getTrpcConfiguration = () => ({ | const getTrpcConfiguration = () => ({ | ||||||
| @@ -26,7 +27,7 @@ const getTrpcConfiguration = () => ({ | |||||||
|   links: [ |   links: [ | ||||||
|     loggerLink({ |     loggerLink({ | ||||||
|       enabled: (opts) => |       enabled: (opts) => | ||||||
|         process.env.NODE_ENV === 'development' || |         env.NEXT_PUBLIC_NODE_ENV === 'development' || | ||||||
|         (opts.direction === 'down' && opts.result instanceof Error), |         (opts.direction === 'down' && opts.result instanceof Error), | ||||||
|     }), |     }), | ||||||
|     httpBatchLink({ |     httpBatchLink({ | ||||||
| @@ -37,8 +38,7 @@ const getTrpcConfiguration = () => ({ | |||||||
|  |  | ||||||
| const getBaseUrl = () => { | const getBaseUrl = () => { | ||||||
|   if (typeof window !== 'undefined') return ''; // browser should use relative url |   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:${env.NEXT_PUBLIC_PORT ?? 3000}`; // dev SSR should use localhost | ||||||
|   return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** A set of type-safe react-query hooks for your tRPC API. */ | /** A set of type-safe react-query hooks for your tRPC API. */ | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -1714,6 +1714,28 @@ __metadata: | |||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   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": | "@tabler/icons-react@npm:^2.18.0": | ||||||
|   version: 2.26.0 |   version: 2.26.0 | ||||||
|   resolution: "@tabler/icons-react@npm:2.26.0" |   resolution: "@tabler/icons-react@npm:2.26.0" | ||||||
| @@ -5537,6 +5559,7 @@ __metadata: | |||||||
|     "@nivo/core": ^0.83.0 |     "@nivo/core": ^0.83.0 | ||||||
|     "@nivo/line": ^0.83.0 |     "@nivo/line": ^0.83.0 | ||||||
|     "@react-native-async-storage/async-storage": ^1.18.1 |     "@react-native-async-storage/async-storage": ^1.18.1 | ||||||
|  |     "@t3-oss/env-nextjs": ^0.6.0 | ||||||
|     "@tabler/icons-react": ^2.18.0 |     "@tabler/icons-react": ^2.18.0 | ||||||
|     "@tanstack/query-async-storage-persister": ^4.27.1 |     "@tanstack/query-async-storage-persister": ^4.27.1 | ||||||
|     "@tanstack/query-sync-storage-persister": ^4.27.1 |     "@tanstack/query-sync-storage-persister": ^4.27.1 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user