mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-27 00:40:58 +01:00
refactor: env validation typescript and common package (#1912)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { env } from "@homarr/auth/env.mjs";
|
||||
import { env } from "@homarr/auth/env";
|
||||
|
||||
export const throwIfCredentialsDisabled = () => {
|
||||
if (!env.AUTH_PROVIDERS.includes("credentials")) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
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 { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
@@ -24,7 +24,7 @@ vi.mock("@homarr/auth", async () => {
|
||||
});
|
||||
|
||||
// Mock the env module to return the credentials provider
|
||||
vi.mock("@homarr/auth/env.mjs", () => {
|
||||
vi.mock("@homarr/auth/env", () => {
|
||||
return {
|
||||
env: {
|
||||
AUTH_PROVIDERS: ["credentials"],
|
||||
|
||||
@@ -28,7 +28,7 @@ vi.mock("@homarr/auth", async () => {
|
||||
});
|
||||
|
||||
// Mock the env module to return the credentials provider
|
||||
vi.mock("@homarr/auth/env.mjs", () => {
|
||||
vi.mock("@homarr/auth/env", () => {
|
||||
return {
|
||||
env: {
|
||||
AUTH_PROVIDERS: ["credentials"],
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||
|
||||
import { createAdapter } from "./adapter";
|
||||
import { createSessionCallback } from "./callbacks";
|
||||
import { env } from "./env.mjs";
|
||||
import { env } from "./env";
|
||||
import { createSignInEventHandler } from "./events";
|
||||
import { createCredentialsConfiguration, createLdapConfiguration } from "./providers/credentials/credentials-provider";
|
||||
import { EmptyNextAuthProvider } from "./providers/empty/empty-provider";
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { createEnv } from "@t3-oss/env-nextjs";
|
||||
import { z } from "zod";
|
||||
|
||||
const trueStrings = ["1", "yes", "t", "true"];
|
||||
const falseStrings = ["0", "no", "f", "false"];
|
||||
import { createBooleanSchema, createDurationSchema, shouldSkipEnvValidation } from "@homarr/common/env-validation";
|
||||
import { supportedAuthProviders } from "@homarr/definitions";
|
||||
|
||||
const supportedAuthProviders = ["credentials", "oidc", "ldap"];
|
||||
const authProvidersSchema = z
|
||||
.string()
|
||||
.min(1)
|
||||
@@ -14,7 +13,7 @@ const authProvidersSchema = z
|
||||
.toLowerCase()
|
||||
.split(",")
|
||||
.filter((provider) => {
|
||||
if (supportedAuthProviders.includes(provider)) return true;
|
||||
if (supportedAuthProviders.some((supportedProvider) => supportedProvider === provider)) return true;
|
||||
else if (!provider)
|
||||
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.`);
|
||||
@@ -23,41 +22,7 @@ const authProvidersSchema = z
|
||||
)
|
||||
.default("credentials");
|
||||
|
||||
const createDurationSchema = (defaultValue) =>
|
||||
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 skipValidation = shouldSkipEnvValidation();
|
||||
const authProviders = skipValidation ? [] : authProvidersSchema.parse(process.env.AUTH_PROVIDERS);
|
||||
|
||||
export const env = createEnv({
|
||||
@@ -71,7 +36,7 @@ export const env = createEnv({
|
||||
AUTH_OIDC_CLIENT_ID: z.string().min(1),
|
||||
AUTH_OIDC_CLIENT_SECRET: z.string().min(1),
|
||||
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_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(),
|
||||
@@ -8,7 +8,7 @@ import { groupMembers, groups, users } from "@homarr/db/schema";
|
||||
import { colorSchemeCookieKey, everyoneGroup } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { env } from "./env.mjs";
|
||||
import { env } from "./env";
|
||||
import { extractProfileName } from "./providers/oidc/oidc-provider";
|
||||
|
||||
export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["events"], undefined>["signIn"] => {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"./client": "./client.ts",
|
||||
"./server": "./server.ts",
|
||||
"./shared": "./shared.ts",
|
||||
"./env.mjs": "./env.mjs"
|
||||
"./env": "./env.ts"
|
||||
},
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||
|
||||
import { env } from "../env.mjs";
|
||||
import { env } from "../env";
|
||||
|
||||
export const isProviderEnabled = (provider: SupportedAuthProvider) => {
|
||||
// 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 { z } from "@homarr/validation";
|
||||
|
||||
import { env } from "../../../env.mjs";
|
||||
import { env } from "../../../env";
|
||||
import { LdapClient } from "../ldap-client";
|
||||
|
||||
export const authorizeWithLdapCredentialsAsync = async (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Client } from "ldapts";
|
||||
|
||||
import { objectEntries } from "@homarr/common";
|
||||
|
||||
import { env } from "../../env.mjs";
|
||||
import { env } from "../../env";
|
||||
|
||||
export interface BindOptions {
|
||||
distinguishedName: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Provider } from "next-auth/providers";
|
||||
|
||||
import { env } from "../env.mjs";
|
||||
import { env } from "../env";
|
||||
|
||||
export const filterProviders = (providers: Exclude<Provider, () => unknown>[]) => {
|
||||
// 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 { Profile } from "@auth/core/types";
|
||||
|
||||
import { env } from "../../env.mjs";
|
||||
import { env } from "../../env";
|
||||
import { createRedirectUri } from "../../redirect";
|
||||
|
||||
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 * as ldapClient from "../credentials/ldap-client";
|
||||
|
||||
vi.mock("../../env.mjs", () => ({
|
||||
vi.mock("../../env", () => ({
|
||||
env: {
|
||||
AUTH_LDAP_BIND_DN: "bind_dn",
|
||||
AUTH_LDAP_BIND_PASSWORD: "bind_password",
|
||||
|
||||
@@ -11,7 +11,7 @@ import { colorSchemeCookieKey, everyoneGroup } from "@homarr/definitions";
|
||||
|
||||
import { createSignInEventHandler } from "../events";
|
||||
|
||||
vi.mock("../env.mjs", () => {
|
||||
vi.mock("../env", () => {
|
||||
return {
|
||||
env: {
|
||||
AUTH_OIDC_GROUPS_ATTRIBUTE: "someRandomGroupsKey",
|
||||
|
||||
@@ -2,6 +2,8 @@ import { randomBytes } from "crypto";
|
||||
import { createEnv } from "@t3-oss/env-nextjs";
|
||||
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")}"`;
|
||||
|
||||
export const env = createEnv({
|
||||
@@ -23,6 +25,5 @@ export const env = createEnv({
|
||||
runtimeEnv: {
|
||||
SECRET_ENCRYPTION_KEY: process.env.SECRET_ENCRYPTION_KEY,
|
||||
},
|
||||
skipValidation:
|
||||
Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION) || process.env.npm_lifecycle_event === "lint",
|
||||
skipValidation: shouldSkipEnvValidation(),
|
||||
});
|
||||
@@ -9,7 +9,8 @@
|
||||
"./types": "./src/types.ts",
|
||||
"./server": "./src/server.ts",
|
||||
"./client": "./src/client.ts",
|
||||
"./env.mjs": "./env.mjs"
|
||||
"./env": "./env.ts",
|
||||
"./env-validation": "./src/env-validation.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
@@ -30,7 +31,8 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"next": "15.1.4",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
"react-dom": "19.0.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { env } from "../env.mjs";
|
||||
import { env } from "../env";
|
||||
|
||||
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 { db, eq, inArray } from "@homarr/db";
|
||||
import { sessions, users } from "@homarr/db/schema";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Config } from "drizzle-kit";
|
||||
|
||||
import { env } from "../env.mjs";
|
||||
import { env } from "../env";
|
||||
|
||||
export default {
|
||||
dialect: "mysql",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Config } from "drizzle-kit";
|
||||
|
||||
import { env } from "../env.mjs";
|
||||
import { env } from "../env";
|
||||
|
||||
export default {
|
||||
dialect: "sqlite",
|
||||
|
||||
@@ -7,7 +7,7 @@ import mysql from "mysql2";
|
||||
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { env } from "./env.mjs";
|
||||
import { env } from "./env";
|
||||
import * as mysqlSchema from "./schema/mysql";
|
||||
import * as sqliteSchema from "./schema/sqlite";
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { createEnv } from "@t3-oss/env-nextjs";
|
||||
import { z } from "zod";
|
||||
|
||||
import { shouldSkipEnvValidation } from "@homarr/common/env-validation";
|
||||
|
||||
const drivers = {
|
||||
betterSqlite3: "better-sqlite3",
|
||||
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 onlyAllowUrl = isDriver(drivers.betterSqlite3);
|
||||
const urlRequired = onlyAllowUrl || !isUsingDbHost;
|
||||
@@ -55,6 +57,5 @@ export const env = createEnv({
|
||||
DB_NAME: process.env.DB_NAME,
|
||||
DB_PORT: process.env.DB_PORT,
|
||||
},
|
||||
skipValidation:
|
||||
Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION) || process.env.npm_lifecycle_event === "lint",
|
||||
skipValidation: shouldSkipEnvValidation(),
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { migrate } from "drizzle-orm/mysql2/migrator";
|
||||
import mysql from "mysql2";
|
||||
|
||||
import type { Database } from "../..";
|
||||
import { env } from "../../env.mjs";
|
||||
import { env } from "../../env";
|
||||
import * as mysqlSchema from "../../schema/mysql";
|
||||
import { seedDataAsync } from "../seed";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Database from "better-sqlite3";
|
||||
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||
|
||||
import { env } from "../../env.mjs";
|
||||
import { env } from "../../env";
|
||||
import * as sqliteSchema from "../../schema/sqlite";
|
||||
import { seedDataAsync } from "../seed";
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"./test": "./test/index.ts",
|
||||
"./queries": "./queries/index.ts",
|
||||
"./validationSchemas": "./validationSchemas.ts",
|
||||
"./env.mjs": "./env.mjs"
|
||||
"./env": "./env.ts"
|
||||
},
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
|
||||
Reference in New Issue
Block a user