refactor: env validation typescript and common package (#1912)
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
// Importing env files here to validate on build
|
// Importing env files here to validate on build
|
||||||
import "@homarr/auth/env.mjs";
|
import "@homarr/auth/env";
|
||||||
import "@homarr/db/env.mjs";
|
import "@homarr/db/env";
|
||||||
import "@homarr/common/env.mjs";
|
import "@homarr/common/env";
|
||||||
|
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
import MillionLint from "@million/lint";
|
import MillionLint from "@million/lint";
|
||||||
import createNextIntlPlugin from "next-intl/plugin";
|
import createNextIntlPlugin from "next-intl/plugin";
|
||||||
|
|
||||||
import "./src/env.mjs";
|
import "./src/env.ts";
|
||||||
|
|
||||||
// Package path does not work... so we need to use relative path
|
// Package path does not work... so we need to use relative path
|
||||||
const withNextIntl = createNextIntlPlugin("../../packages/translation/src/request.ts");
|
const withNextIntl = createNextIntlPlugin("../../packages/translation/src/request.ts");
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import type { AppRouter } from "@homarr/api";
|
|||||||
import { clientApi, getTrpcUrl } from "@homarr/api/client";
|
import { clientApi, getTrpcUrl } from "@homarr/api/client";
|
||||||
import { createHeadersCallbackForSource } from "@homarr/api/shared";
|
import { createHeadersCallbackForSource } from "@homarr/api/shared";
|
||||||
|
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env";
|
||||||
|
|
||||||
const getWebSocketProtocol = () => {
|
const getWebSocketProtocol = () => {
|
||||||
// window is not defined on server side
|
// window is not defined on server side
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { Card, Center, Stack, Text, Title } from "@mantine/core";
|
import { Card, Center, Stack, Text, Title } from "@mantine/core";
|
||||||
|
|
||||||
import { env } from "@homarr/auth/env.mjs";
|
import { env } from "@homarr/auth/env";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { getScopedI18n } from "@homarr/translation/server";
|
import { getScopedI18n } from "@homarr/translation/server";
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { useCategoryActions } from "~/components/board/sections/category/categor
|
|||||||
import { CategoryEditModal } from "~/components/board/sections/category/category-edit-modal";
|
import { CategoryEditModal } from "~/components/board/sections/category/category-edit-modal";
|
||||||
import { useDynamicSectionActions } from "~/components/board/sections/dynamic/dynamic-actions";
|
import { useDynamicSectionActions } from "~/components/board/sections/dynamic/dynamic-actions";
|
||||||
import { HeaderButton } from "~/components/layout/header/button";
|
import { HeaderButton } from "~/components/layout/header/button";
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env";
|
||||||
import { useEditMode, useRequiredBoard } from "./_context";
|
import { useEditMode, useRequiredBoard } from "./_context";
|
||||||
|
|
||||||
export const BoardContentHeaderActions = () => {
|
export const BoardContentHeaderActions = () => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import "~/styles/scroll-area.scss";
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { NextIntlClientProvider } from "next-intl";
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
|
|
||||||
import { env } from "@homarr/auth/env.mjs";
|
import { env } from "@homarr/auth/env";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { ModalProvider } from "@homarr/modals";
|
import { ModalProvider } from "@homarr/modals";
|
||||||
import { Notifications } from "@homarr/notifications";
|
import { Notifications } from "@homarr/notifications";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { IconExclamationCircle } from "@tabler/icons-react";
|
|||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
import { env } from "@homarr/auth/env.mjs";
|
import { env } from "@homarr/auth/env";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { isProviderEnabled } from "@homarr/auth/server";
|
import { isProviderEnabled } from "@homarr/auth/server";
|
||||||
import { everyoneGroup } from "@homarr/definitions";
|
import { everyoneGroup } from "@homarr/definitions";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { db } from "@homarr/db";
|
|||||||
import type { WidgetKind } from "@homarr/definitions";
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
import { widgetImports } from "@homarr/widgets";
|
import { widgetImports } from "@homarr/widgets";
|
||||||
|
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env";
|
||||||
import { WidgetPreviewPageContent } from "./_content";
|
import { WidgetPreviewPageContent } from "./_content";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { createEnv } from "@t3-oss/env-nextjs";
|
import { createEnv } from "@t3-oss/env-nextjs";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { shouldSkipEnvValidation } from "@homarr/common/env-validation";
|
||||||
|
|
||||||
export const env = createEnv({
|
export const env = createEnv({
|
||||||
shared: {
|
shared: {
|
||||||
PORT: z.coerce.number().default(3000),
|
PORT: z.coerce.number().default(3000),
|
||||||
@@ -13,7 +15,7 @@ export const env = createEnv({
|
|||||||
server: {
|
server: {
|
||||||
// Comma separated list of docker hostnames that can be used to connect to query the docker endpoints (localhost:2375,host.docker.internal:2375, ...)
|
// Comma separated list of docker hostnames that can be used to connect to query the docker endpoints (localhost:2375,host.docker.internal:2375, ...)
|
||||||
DOCKER_HOSTNAMES: z.string().optional(),
|
DOCKER_HOSTNAMES: z.string().optional(),
|
||||||
DOCKER_PORTS: z.number().optional(),
|
DOCKER_PORTS: z.string().optional(),
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Specify your client-side environment variables schema here.
|
* Specify your client-side environment variables schema here.
|
||||||
@@ -32,6 +34,5 @@ export const env = createEnv({
|
|||||||
DOCKER_PORTS: process.env.DOCKER_PORTS,
|
DOCKER_PORTS: process.env.DOCKER_PORTS,
|
||||||
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
||||||
},
|
},
|
||||||
skipValidation:
|
skipValidation: shouldSkipEnvValidation(),
|
||||||
Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION) || process.env.npm_lifecycle_event === "lint",
|
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
import { env } from "@homarr/auth/env.mjs";
|
import { env } from "@homarr/auth/env";
|
||||||
|
|
||||||
export const throwIfCredentialsDisabled = () => {
|
export const throwIfCredentialsDisabled = () => {
|
||||||
if (!env.AUTH_PROVIDERS.includes("credentials")) {
|
if (!env.AUTH_PROVIDERS.includes("credentials")) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
import type { Session } from "@homarr/auth";
|
import type { Session } from "@homarr/auth";
|
||||||
import * as env from "@homarr/auth/env.mjs";
|
import * as env from "@homarr/auth/env";
|
||||||
import { createId, eq } from "@homarr/db";
|
import { createId, eq } from "@homarr/db";
|
||||||
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema";
|
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema";
|
||||||
import { createDb } from "@homarr/db/test";
|
import { createDb } from "@homarr/db/test";
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ vi.mock("@homarr/auth", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Mock the env module to return the credentials provider
|
// Mock the env module to return the credentials provider
|
||||||
vi.mock("@homarr/auth/env.mjs", () => {
|
vi.mock("@homarr/auth/env", () => {
|
||||||
return {
|
return {
|
||||||
env: {
|
env: {
|
||||||
AUTH_PROVIDERS: ["credentials"],
|
AUTH_PROVIDERS: ["credentials"],
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ vi.mock("@homarr/auth", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Mock the env module to return the credentials provider
|
// Mock the env module to return the credentials provider
|
||||||
vi.mock("@homarr/auth/env.mjs", () => {
|
vi.mock("@homarr/auth/env", () => {
|
||||||
return {
|
return {
|
||||||
env: {
|
env: {
|
||||||
AUTH_PROVIDERS: ["credentials"],
|
AUTH_PROVIDERS: ["credentials"],
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { SupportedAuthProvider } from "@homarr/definitions";
|
|||||||
|
|
||||||
import { createAdapter } from "./adapter";
|
import { createAdapter } from "./adapter";
|
||||||
import { createSessionCallback } from "./callbacks";
|
import { createSessionCallback } from "./callbacks";
|
||||||
import { env } from "./env.mjs";
|
import { env } from "./env";
|
||||||
import { createSignInEventHandler } from "./events";
|
import { createSignInEventHandler } from "./events";
|
||||||
import { createCredentialsConfiguration, createLdapConfiguration } from "./providers/credentials/credentials-provider";
|
import { createCredentialsConfiguration, createLdapConfiguration } from "./providers/credentials/credentials-provider";
|
||||||
import { EmptyNextAuthProvider } from "./providers/empty/empty-provider";
|
import { EmptyNextAuthProvider } from "./providers/empty/empty-provider";
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { createEnv } from "@t3-oss/env-nextjs";
|
import { createEnv } from "@t3-oss/env-nextjs";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const trueStrings = ["1", "yes", "t", "true"];
|
import { createBooleanSchema, createDurationSchema, shouldSkipEnvValidation } from "@homarr/common/env-validation";
|
||||||
const falseStrings = ["0", "no", "f", "false"];
|
import { supportedAuthProviders } from "@homarr/definitions";
|
||||||
|
|
||||||
const supportedAuthProviders = ["credentials", "oidc", "ldap"];
|
|
||||||
const authProvidersSchema = z
|
const authProvidersSchema = z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
@@ -14,7 +13,7 @@ const authProvidersSchema = z
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.split(",")
|
.split(",")
|
||||||
.filter((provider) => {
|
.filter((provider) => {
|
||||||
if (supportedAuthProviders.includes(provider)) return true;
|
if (supportedAuthProviders.some((supportedProvider) => supportedProvider === provider)) return true;
|
||||||
else if (!provider)
|
else if (!provider)
|
||||||
console.log("One or more of the entries for AUTH_PROVIDER could not be parsed and/or returned null.");
|
console.log("One or more of the entries for AUTH_PROVIDER could not be parsed and/or returned null.");
|
||||||
else console.log(`The value entered for AUTH_PROVIDER "${provider}" is incorrect.`);
|
else console.log(`The value entered for AUTH_PROVIDER "${provider}" is incorrect.`);
|
||||||
@@ -23,41 +22,7 @@ const authProvidersSchema = z
|
|||||||
)
|
)
|
||||||
.default("credentials");
|
.default("credentials");
|
||||||
|
|
||||||
const createDurationSchema = (defaultValue) =>
|
const skipValidation = shouldSkipEnvValidation();
|
||||||
z
|
|
||||||
.string()
|
|
||||||
.regex(/^\d+[smhd]?$/)
|
|
||||||
.default(defaultValue)
|
|
||||||
.transform((duration) => {
|
|
||||||
const lastChar = duration[duration.length - 1];
|
|
||||||
if (!isNaN(Number(lastChar))) {
|
|
||||||
return Number(defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
const multipliers = {
|
|
||||||
s: 1,
|
|
||||||
m: 60,
|
|
||||||
h: 60 * 60,
|
|
||||||
d: 60 * 60 * 24,
|
|
||||||
};
|
|
||||||
const numberDuration = Number(duration.slice(0, -1));
|
|
||||||
const multiplier = multipliers[lastChar];
|
|
||||||
|
|
||||||
return numberDuration * multiplier;
|
|
||||||
});
|
|
||||||
|
|
||||||
const booleanSchema = z
|
|
||||||
.string()
|
|
||||||
.default("false")
|
|
||||||
.transform((value, ctx) => {
|
|
||||||
const normalized = value.trim().toLowerCase();
|
|
||||||
if (trueStrings.includes(normalized)) return true;
|
|
||||||
if (falseStrings.includes(normalized)) return false;
|
|
||||||
|
|
||||||
throw new Error(`Invalid boolean value for ${ctx.path.join(".")}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const skipValidation = Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION);
|
|
||||||
const authProviders = skipValidation ? [] : authProvidersSchema.parse(process.env.AUTH_PROVIDERS);
|
const authProviders = skipValidation ? [] : authProvidersSchema.parse(process.env.AUTH_PROVIDERS);
|
||||||
|
|
||||||
export const env = createEnv({
|
export const env = createEnv({
|
||||||
@@ -71,7 +36,7 @@ export const env = createEnv({
|
|||||||
AUTH_OIDC_CLIENT_ID: z.string().min(1),
|
AUTH_OIDC_CLIENT_ID: z.string().min(1),
|
||||||
AUTH_OIDC_CLIENT_SECRET: z.string().min(1),
|
AUTH_OIDC_CLIENT_SECRET: z.string().min(1),
|
||||||
AUTH_OIDC_CLIENT_NAME: z.string().min(1).default("OIDC"),
|
AUTH_OIDC_CLIENT_NAME: z.string().min(1).default("OIDC"),
|
||||||
AUTH_OIDC_AUTO_LOGIN: booleanSchema,
|
AUTH_OIDC_AUTO_LOGIN: createBooleanSchema(false),
|
||||||
AUTH_OIDC_SCOPE_OVERWRITE: z.string().min(1).default("openid email profile groups"),
|
AUTH_OIDC_SCOPE_OVERWRITE: z.string().min(1).default("openid email profile groups"),
|
||||||
AUTH_OIDC_GROUPS_ATTRIBUTE: z.string().default("groups"), // Is used in the signIn event to assign the correct groups, key is from object of decoded id_token
|
AUTH_OIDC_GROUPS_ATTRIBUTE: z.string().default("groups"), // Is used in the signIn event to assign the correct groups, key is from object of decoded id_token
|
||||||
AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE: z.string().optional(),
|
AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE: z.string().optional(),
|
||||||
@@ -8,7 +8,7 @@ import { groupMembers, groups, users } from "@homarr/db/schema";
|
|||||||
import { colorSchemeCookieKey, everyoneGroup } from "@homarr/definitions";
|
import { colorSchemeCookieKey, everyoneGroup } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
import { env } from "./env.mjs";
|
import { env } from "./env";
|
||||||
import { extractProfileName } from "./providers/oidc/oidc-provider";
|
import { extractProfileName } from "./providers/oidc/oidc-provider";
|
||||||
|
|
||||||
export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["events"], undefined>["signIn"] => {
|
export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["events"], undefined>["signIn"] => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"./client": "./client.ts",
|
"./client": "./client.ts",
|
||||||
"./server": "./server.ts",
|
"./server": "./server.ts",
|
||||||
"./shared": "./shared.ts",
|
"./shared": "./shared.ts",
|
||||||
"./env.mjs": "./env.mjs"
|
"./env": "./env.ts"
|
||||||
},
|
},
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"types": "./index.ts",
|
"types": "./index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||||
|
|
||||||
import { env } from "../env.mjs";
|
import { env } from "../env";
|
||||||
|
|
||||||
export const isProviderEnabled = (provider: SupportedAuthProvider) => {
|
export const isProviderEnabled = (provider: SupportedAuthProvider) => {
|
||||||
// The question mark is placed there because isProviderEnabled is called during static build of about page
|
// The question mark is placed there because isProviderEnabled is called during static build of about page
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { logger } from "@homarr/log";
|
|||||||
import type { validation } from "@homarr/validation";
|
import type { validation } from "@homarr/validation";
|
||||||
import { z } from "@homarr/validation";
|
import { z } from "@homarr/validation";
|
||||||
|
|
||||||
import { env } from "../../../env.mjs";
|
import { env } from "../../../env";
|
||||||
import { LdapClient } from "../ldap-client";
|
import { LdapClient } from "../ldap-client";
|
||||||
|
|
||||||
export const authorizeWithLdapCredentialsAsync = async (
|
export const authorizeWithLdapCredentialsAsync = async (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Client } from "ldapts";
|
|||||||
|
|
||||||
import { objectEntries } from "@homarr/common";
|
import { objectEntries } from "@homarr/common";
|
||||||
|
|
||||||
import { env } from "../../env.mjs";
|
import { env } from "../../env";
|
||||||
|
|
||||||
export interface BindOptions {
|
export interface BindOptions {
|
||||||
distinguishedName: string;
|
distinguishedName: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Provider } from "next-auth/providers";
|
import type { Provider } from "next-auth/providers";
|
||||||
|
|
||||||
import { env } from "../env.mjs";
|
import { env } from "../env";
|
||||||
|
|
||||||
export const filterProviders = (providers: Exclude<Provider, () => unknown>[]) => {
|
export const filterProviders = (providers: Exclude<Provider, () => unknown>[]) => {
|
||||||
// During build this will be undefined, so we default to an empty array
|
// During build this will be undefined, so we default to an empty array
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapte
|
|||||||
import type { OIDCConfig } from "@auth/core/providers";
|
import type { OIDCConfig } from "@auth/core/providers";
|
||||||
import type { Profile } from "@auth/core/types";
|
import type { Profile } from "@auth/core/types";
|
||||||
|
|
||||||
import { env } from "../../env.mjs";
|
import { env } from "../../env";
|
||||||
import { createRedirectUri } from "../../redirect";
|
import { createRedirectUri } from "../../redirect";
|
||||||
|
|
||||||
export const OidcProvider = (headers: ReadonlyHeaders | null): OIDCConfig<Profile> => ({
|
export const OidcProvider = (headers: ReadonlyHeaders | null): OIDCConfig<Profile> => ({
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { createDb } from "@homarr/db/test";
|
|||||||
import { authorizeWithLdapCredentialsAsync } from "../credentials/authorization/ldap-authorization";
|
import { authorizeWithLdapCredentialsAsync } from "../credentials/authorization/ldap-authorization";
|
||||||
import * as ldapClient from "../credentials/ldap-client";
|
import * as ldapClient from "../credentials/ldap-client";
|
||||||
|
|
||||||
vi.mock("../../env.mjs", () => ({
|
vi.mock("../../env", () => ({
|
||||||
env: {
|
env: {
|
||||||
AUTH_LDAP_BIND_DN: "bind_dn",
|
AUTH_LDAP_BIND_DN: "bind_dn",
|
||||||
AUTH_LDAP_BIND_PASSWORD: "bind_password",
|
AUTH_LDAP_BIND_PASSWORD: "bind_password",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { colorSchemeCookieKey, everyoneGroup } from "@homarr/definitions";
|
|||||||
|
|
||||||
import { createSignInEventHandler } from "../events";
|
import { createSignInEventHandler } from "../events";
|
||||||
|
|
||||||
vi.mock("../env.mjs", () => {
|
vi.mock("../env", () => {
|
||||||
return {
|
return {
|
||||||
env: {
|
env: {
|
||||||
AUTH_OIDC_GROUPS_ATTRIBUTE: "someRandomGroupsKey",
|
AUTH_OIDC_GROUPS_ATTRIBUTE: "someRandomGroupsKey",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { randomBytes } from "crypto";
|
|||||||
import { createEnv } from "@t3-oss/env-nextjs";
|
import { createEnv } from "@t3-oss/env-nextjs";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { shouldSkipEnvValidation } from "./src/env-validation";
|
||||||
|
|
||||||
const errorSuffix = `, please generate a 64 character secret in hex format or use the following: "${randomBytes(32).toString("hex")}"`;
|
const errorSuffix = `, please generate a 64 character secret in hex format or use the following: "${randomBytes(32).toString("hex")}"`;
|
||||||
|
|
||||||
export const env = createEnv({
|
export const env = createEnv({
|
||||||
@@ -23,6 +25,5 @@ export const env = createEnv({
|
|||||||
runtimeEnv: {
|
runtimeEnv: {
|
||||||
SECRET_ENCRYPTION_KEY: process.env.SECRET_ENCRYPTION_KEY,
|
SECRET_ENCRYPTION_KEY: process.env.SECRET_ENCRYPTION_KEY,
|
||||||
},
|
},
|
||||||
skipValidation:
|
skipValidation: shouldSkipEnvValidation(),
|
||||||
Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION) || process.env.npm_lifecycle_event === "lint",
|
|
||||||
});
|
});
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
"./types": "./src/types.ts",
|
"./types": "./src/types.ts",
|
||||||
"./server": "./src/server.ts",
|
"./server": "./src/server.ts",
|
||||||
"./client": "./src/client.ts",
|
"./client": "./src/client.ts",
|
||||||
"./env.mjs": "./env.mjs"
|
"./env": "./env.ts",
|
||||||
|
"./env-validation": "./src/env-validation.ts"
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
@@ -30,7 +31,8 @@
|
|||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"next": "15.1.4",
|
"next": "15.1.4",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0"
|
"react-dom": "19.0.0",
|
||||||
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { env } from "../env.mjs";
|
import { env } from "../env";
|
||||||
|
|
||||||
const algorithm = "aes-256-cbc"; //Using AES encryption
|
const algorithm = "aes-256-cbc"; //Using AES encryption
|
||||||
|
|
||||||
|
|||||||
42
packages/common/src/env-validation.ts
Normal file
42
packages/common/src/env-validation.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const trueStrings = ["1", "yes", "t", "true"];
|
||||||
|
const falseStrings = ["0", "no", "f", "false"];
|
||||||
|
|
||||||
|
export const createBooleanSchema = (defaultValue: boolean) =>
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.default(defaultValue.toString())
|
||||||
|
.transform((value, ctx) => {
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
if (trueStrings.includes(normalized)) return true;
|
||||||
|
if (falseStrings.includes(normalized)) return false;
|
||||||
|
|
||||||
|
throw new Error(`Invalid boolean value for ${ctx.path.join(".")}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createDurationSchema = (defaultValue: `${number}${"s" | "m" | "h" | "d"}`) =>
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.regex(/^\d+[smhd]?$/)
|
||||||
|
.default(defaultValue)
|
||||||
|
.transform((duration) => {
|
||||||
|
const lastChar = duration[duration.length - 1] as "s" | "m" | "h" | "d";
|
||||||
|
if (!isNaN(Number(lastChar))) {
|
||||||
|
return Number(defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
const multipliers = {
|
||||||
|
s: 1,
|
||||||
|
m: 60,
|
||||||
|
h: 60 * 60,
|
||||||
|
d: 60 * 60 * 24,
|
||||||
|
};
|
||||||
|
const numberDuration = Number(duration.slice(0, -1));
|
||||||
|
const multiplier = multipliers[lastChar];
|
||||||
|
|
||||||
|
return numberDuration * multiplier;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const shouldSkipEnvValidation = () =>
|
||||||
|
Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION) || process.env.npm_lifecycle_event === "lint";
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { env } from "@homarr/auth/env.mjs";
|
import { env } from "@homarr/auth/env";
|
||||||
import { NEVER } from "@homarr/cron-jobs-core/expressions";
|
import { NEVER } from "@homarr/cron-jobs-core/expressions";
|
||||||
import { db, eq, inArray } from "@homarr/db";
|
import { db, eq, inArray } from "@homarr/db";
|
||||||
import { sessions, users } from "@homarr/db/schema";
|
import { sessions, users } from "@homarr/db/schema";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Config } from "drizzle-kit";
|
import type { Config } from "drizzle-kit";
|
||||||
|
|
||||||
import { env } from "../env.mjs";
|
import { env } from "../env";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
dialect: "mysql",
|
dialect: "mysql",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Config } from "drizzle-kit";
|
import type { Config } from "drizzle-kit";
|
||||||
|
|
||||||
import { env } from "../env.mjs";
|
import { env } from "../env";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
dialect: "sqlite",
|
dialect: "sqlite",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import mysql from "mysql2";
|
|||||||
|
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
import { env } from "./env.mjs";
|
import { env } from "./env";
|
||||||
import * as mysqlSchema from "./schema/mysql";
|
import * as mysqlSchema from "./schema/mysql";
|
||||||
import * as sqliteSchema from "./schema/sqlite";
|
import * as sqliteSchema from "./schema/sqlite";
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { createEnv } from "@t3-oss/env-nextjs";
|
import { createEnv } from "@t3-oss/env-nextjs";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { shouldSkipEnvValidation } from "@homarr/common/env-validation";
|
||||||
|
|
||||||
const drivers = {
|
const drivers = {
|
||||||
betterSqlite3: "better-sqlite3",
|
betterSqlite3: "better-sqlite3",
|
||||||
mysql2: "mysql2",
|
mysql2: "mysql2",
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
const isDriver = (driver) => process.env.DB_DRIVER === driver;
|
const isDriver = (driver: (typeof drivers)[keyof typeof drivers]) => process.env.DB_DRIVER === driver;
|
||||||
const isUsingDbHost = Boolean(process.env.DB_HOST);
|
const isUsingDbHost = Boolean(process.env.DB_HOST);
|
||||||
const onlyAllowUrl = isDriver(drivers.betterSqlite3);
|
const onlyAllowUrl = isDriver(drivers.betterSqlite3);
|
||||||
const urlRequired = onlyAllowUrl || !isUsingDbHost;
|
const urlRequired = onlyAllowUrl || !isUsingDbHost;
|
||||||
@@ -55,6 +57,5 @@ export const env = createEnv({
|
|||||||
DB_NAME: process.env.DB_NAME,
|
DB_NAME: process.env.DB_NAME,
|
||||||
DB_PORT: process.env.DB_PORT,
|
DB_PORT: process.env.DB_PORT,
|
||||||
},
|
},
|
||||||
skipValidation:
|
skipValidation: shouldSkipEnvValidation(),
|
||||||
Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION) || process.env.npm_lifecycle_event === "lint",
|
|
||||||
});
|
});
|
||||||
@@ -3,7 +3,7 @@ import { migrate } from "drizzle-orm/mysql2/migrator";
|
|||||||
import mysql from "mysql2";
|
import mysql from "mysql2";
|
||||||
|
|
||||||
import type { Database } from "../..";
|
import type { Database } from "../..";
|
||||||
import { env } from "../../env.mjs";
|
import { env } from "../../env";
|
||||||
import * as mysqlSchema from "../../schema/mysql";
|
import * as mysqlSchema from "../../schema/mysql";
|
||||||
import { seedDataAsync } from "../seed";
|
import { seedDataAsync } from "../seed";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Database from "better-sqlite3";
|
|||||||
import { drizzle } from "drizzle-orm/better-sqlite3";
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||||
|
|
||||||
import { env } from "../../env.mjs";
|
import { env } from "../../env";
|
||||||
import * as sqliteSchema from "../../schema/sqlite";
|
import * as sqliteSchema from "../../schema/sqlite";
|
||||||
import { seedDataAsync } from "../seed";
|
import { seedDataAsync } from "../seed";
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"./test": "./test/index.ts",
|
"./test": "./test/index.ts",
|
||||||
"./queries": "./queries/index.ts",
|
"./queries": "./queries/index.ts",
|
||||||
"./validationSchemas": "./validationSchemas.ts",
|
"./validationSchemas": "./validationSchemas.ts",
|
||||||
"./env.mjs": "./env.mjs"
|
"./env": "./env.ts"
|
||||||
},
|
},
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"types": "./index.ts",
|
"types": "./index.ts",
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -716,6 +716,9 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: 19.0.0
|
specifier: 19.0.0
|
||||||
version: 19.0.0(react@19.0.0)
|
version: 19.0.0(react@19.0.0)
|
||||||
|
zod:
|
||||||
|
specifier: ^3.24.1
|
||||||
|
version: 3.24.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@homarr/eslint-config':
|
'@homarr/eslint-config':
|
||||||
specifier: workspace:^0.2.0
|
specifier: workspace:^0.2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user