From 7d463d7da817d4fa09b3815b90eccd87a1497f6a Mon Sep 17 00:00:00 2001 From: Charles GTE Date: Thu, 21 May 2026 07:28:05 +0200 Subject: [PATCH] fix: default user from init .env variables (#293) * fix: default user from init en .env variables * fix: .env.example --------- Co-authored-by: charles-gauthereau --- .env.example | 9 + .../dashboard/(admin)/admin/users/page.tsx | 8 +- package.json | 2 +- .../dashboard/admin/users/user.action.ts | 1 + src/db/services/user.ts | 4 + src/env.mjs | 261 +++++++++--------- src/types/auth.ts | 4 +- src/utils/init.ts | 146 ---------- src/utils/init/cron.ts | 14 + src/utils/init/index.ts | 50 ++++ src/utils/init/organization.ts | 27 ++ src/utils/init/setting.ts | 70 +++++ src/utils/init/user.ts | 71 +++++ src/utils/password.ts | 57 ++++ 14 files changed, 445 insertions(+), 279 deletions(-) delete mode 100644 src/utils/init.ts create mode 100644 src/utils/init/cron.ts create mode 100644 src/utils/init/index.ts create mode 100644 src/utils/init/organization.ts create mode 100644 src/utils/init/setting.ts create mode 100644 src/utils/init/user.ts diff --git a/.env.example b/.env.example index 21888ecb..4008667e 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,15 @@ PROJECT_NAME="Portabase" PROJECT_URL=http://localhost:8887 PROJECT_SECRET= +AUTH_DEFAULT_USER_NAME="Portabase Admin" +AUTH_DEFAULT_USER=user@example.com +# Password must contain at least 8 characters +# Password must contain at least 1 number +# Password must contain at least 1 lowercase letter +# Password must contain at least 1 uppercase letter +# Password must contain at least 1 special character +AUTH_DEFAULT_PASSWORD=testPASS123456! + # SMTP (email) SMTP_HOST= SMTP_PORT= diff --git a/app/(customer)/dashboard/(admin)/admin/users/page.tsx b/app/(customer)/dashboard/(admin)/admin/users/page.tsx index ec7f8feb..899e7f4c 100644 --- a/app/(customer)/dashboard/(admin)/admin/users/page.tsx +++ b/app/(customer)/dashboard/(admin)/admin/users/page.tsx @@ -30,9 +30,11 @@ export default async function RoutePage(props: PageParams<{}>) {
Active users - - - + {isPasswordAuthEnabled && ( + + + + )}
diff --git a/package.json b/package.json index 53032d7d..5aa8d6da 100644 --- a/package.json +++ b/package.json @@ -136,5 +136,5 @@ "typescript": "^5.9.3", "zenstack": "2.14.2" }, - "packageManager": "pnpm@11.1.2+sha512.415a1cc25974731e75455c1468371be74c5aa5fb7621b50d4056d222451609f11412f23fd602e6169f1e060466641f798597e1be961a10688836a67b16569499" + "packageManager": "pnpm@11.1.3" } diff --git a/src/components/wrappers/dashboard/admin/users/user.action.ts b/src/components/wrappers/dashboard/admin/users/user.action.ts index 23b136b8..f2d18cb1 100644 --- a/src/components/wrappers/dashboard/admin/users/user.action.ts +++ b/src/components/wrappers/dashboard/admin/users/user.action.ts @@ -35,6 +35,7 @@ export const createUserAction = userAction email: parsedInput.email, theme: "dark", role: "user", + password: "" }; if (isPasswordAuthEnabled) { diff --git a/src/db/services/user.ts b/src/db/services/user.ts index a230b310..f847e15d 100644 --- a/src/db/services/user.ts +++ b/src/db/services/user.ts @@ -3,13 +3,17 @@ import {hashPassword} from "better-auth/crypto"; import {db} from "@/db"; import * as drizzleDb from "@/db"; import {User, UserThemeEnum} from "@/db/schema/02_user"; +import {assertValidPassword} from "@/utils/password"; export async function createUserDb(data: SignUpUser): Promise { + assertValidPassword(data.password); + const now = new Date(); const userId = crypto.randomUUID(); const [newUser] = await db.insert(drizzleDb.schemas.user).values({ + ...data, id: userId, name: data.name, email: data.email, diff --git a/src/env.mjs b/src/env.mjs index 6a5d926a..e27ac90e 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -1,167 +1,174 @@ -import { createEnv } from "@t3-oss/env-nextjs"; +import {createEnv} from "@t3-oss/env-nextjs"; import path from "path"; -import { z } from "zod"; -import packageJson from "../package.json" with { type: "json" }; +import {z} from "zod"; +import packageJson from "../package.json" with {type: "json"}; -const { version } = packageJson; +const {version} = packageJson; export const env = createEnv({ - server: { - NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(), + server: { + NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(), - NODE_ENV: z.enum(["development", "production"]).optional(), - LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"), + NODE_ENV: z.enum(["development", "production"]).optional(), + LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"), - DATABASE_URL: z.string().url().optional(), + DATABASE_URL: z.url().optional(), - PROJECT_NAME: z.string().optional(), - PROJECT_DESCRIPTION: z.string().optional(), - PROJECT_URL: z - .string() - .regex(/^https?:\/\//, "URL must start with http:// or https://"), - PROJECT_SECRET: z.string(), + PROJECT_NAME: z.string().optional(), + PROJECT_DESCRIPTION: z.string().optional(), + PROJECT_URL: z + .string() + .regex(/^https?:\/\//, "URL must start with http:// or https://"), + PROJECT_SECRET: z.string(), - TRUSTED_DOMAINS: z.string().optional(), + TRUSTED_DOMAINS: z.string().optional(), - SMTP_PASSWORD: z.string().optional(), - SMTP_FROM: z.string().optional(), - SMTP_HOST: z.string().optional(), - SMTP_PORT: z.string().optional(), - SMTP_USER: z.string().optional(), + SMTP_PASSWORD: z.string().optional(), + SMTP_FROM: z.string().optional(), + SMTP_HOST: z.string().optional(), + SMTP_PORT: z.string().optional(), + SMTP_USER: z.string().optional(), + + AUTH_DEFAULT_USER_NAME: z.string().optional(), + AUTH_DEFAULT_USER: z.string().optional(), + AUTH_DEFAULT_PASSWORD: z.string().optional(), - SMTP_SECURE: z - .enum(["true", "false"]) - .transform((val) => val === "true") - .default("true"), + SMTP_SECURE: z + .enum(["true", "false"]) + .transform((val) => val === "true") + .default("true"), - AUTH_GOOGLE_ID: z.string().optional(), - AUTH_GOOGLE_SECRET: z.string().optional(), + AUTH_GOOGLE_ID: z.string().optional(), + AUTH_GOOGLE_SECRET: z.string().optional(), - AUTH_GITHUB_ID: z.string().optional(), - AUTH_GITHUB_SECRET: z.string().optional(), + AUTH_GITHUB_ID: z.string().optional(), + AUTH_GITHUB_SECRET: z.string().optional(), - RETENTION_CRON: z - .string() - .default( - process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *", - ), + RETENTION_CRON: z + .string() + .default( + process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *", + ), - CLEANING_HEALTHCHECK_LOGS_CRON: z - .string() - .default( - process.env.NODE_ENV === "production" ? "0 * * * *" : "* * * * *", - ), + CLEANING_HEALTHCHECK_LOGS_CRON: z + .string() + .default( + process.env.NODE_ENV === "production" ? "0 * * * *" : "* * * * *", + ), - HEALTHCHECK_CRON: z - .string() - .default( - process.env.NODE_ENV === "production" ? "0 * * * *" : "* * * * *", - ), + HEALTHCHECK_CRON: z + .string() + .default( + process.env.NODE_ENV === "production" ? "0 * * * *" : "* * * * *", + ), + AUTH_OIDC_ID: z.string().optional().default("oidc"), + AUTH_OIDC_TITLE: z.string().optional(), + AUTH_OIDC_DESC: z.string().optional(), + AUTH_OIDC_ICON: z.string().optional(), + AUTH_OIDC_CLIENT: z.string().optional(), + AUTH_OIDC_SECRET: z.string().optional(), + AUTH_OIDC_ISSUER_URL: z.string().optional(), + AUTH_OIDC_HOST: z.string().optional(), + AUTH_OIDC_SCOPES: z.string().optional(), + AUTH_OIDC_DISCOVERY_ENDPOINT: z.string().optional(), + AUTH_OIDC_JWKS_ENDPOINT: z.string().optional(), + AUTH_OIDC_PKCE: z.string().optional(), - AUTH_OIDC_ID: z.string().optional().default("oidc"), - AUTH_OIDC_TITLE: z.string().optional(), - AUTH_OIDC_DESC: z.string().optional(), - AUTH_OIDC_ICON: z.string().optional(), - AUTH_OIDC_CLIENT: z.string().optional(), - AUTH_OIDC_SECRET: z.string().optional(), - AUTH_OIDC_ISSUER_URL: z.string().optional(), - AUTH_OIDC_HOST: z.string().optional(), - AUTH_OIDC_SCOPES: z.string().optional(), - AUTH_OIDC_DISCOVERY_ENDPOINT: z.string().optional(), - AUTH_OIDC_JWKS_ENDPOINT: z.string().optional(), - AUTH_OIDC_PKCE: z.string().optional(), + AUTH_SOCIAL_ID: z.string().optional().default("social"), + AUTH_SOCIAL_TITLE: z.string().optional(), + AUTH_SOCIAL_DESC: z.string().optional(), + AUTH_SOCIAL_ICON: z.string().optional(), + AUTH_SOCIAL_CLIENT: z.string().optional(), + AUTH_SOCIAL_SECRET: z.string().optional(), + AUTH_SOCIAL_APPLE_APP_BUNDLE_IDENTIFIER: z.string().optional(), - AUTH_SOCIAL_ID: z.string().optional().default("social"), - AUTH_SOCIAL_TITLE: z.string().optional(), - AUTH_SOCIAL_DESC: z.string().optional(), - AUTH_SOCIAL_ICON: z.string().optional(), - AUTH_SOCIAL_CLIENT: z.string().optional(), - AUTH_SOCIAL_SECRET: z.string().optional(), - AUTH_SOCIAL_APPLE_APP_BUNDLE_IDENTIFIER: z.string().optional(), + ALLOWED_GROUP: z.string().optional(), - ALLOWED_GROUP: z.string().optional(), + AUTH_EMAIL_PASSWORD_ENABLED: z.string().optional().default("true"), + AUTH_SIGNUP_ENABLED: z.string().optional().default("true"), + AUTH_PASSKEY_ENABLED: z.string().optional().default("false"), - AUTH_EMAIL_PASSWORD_ENABLED: z.string().optional().default("true"), - AUTH_SIGNUP_ENABLED: z.string().optional().default("true"), - AUTH_PASSKEY_ENABLED: z.string().optional().default("false"), + AUTH_SYNC_OIDC_ROLES_ON_LOGIN: z.enum(["true", "false"]).default("true"), + AUTH_ROLE_MAP: z.string().optional(), + AUTH_DEFAULT_ROLE: z.string().optional().default("pending"), + AUTH_ALLOW_LINKING: z.enum(["true", "false"]).default("true"), + AUTH_ALLOW_UNLINKING: z.enum(["true", "false"]).default("true"), - AUTH_SYNC_OIDC_ROLES_ON_LOGIN: z.enum(["true", "false"]).default("true"), - AUTH_ROLE_MAP: z.string().optional(), - AUTH_DEFAULT_ROLE: z.string().optional().default("pending"), - AUTH_ALLOW_LINKING: z.enum(["true", "false"]).default("true"), - AUTH_ALLOW_UNLINKING: z.enum(["true", "false"]).default("true"), + PRIVATE_PATH: z.string().optional(), + }, + client: { + NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(), + }, + runtimeEnv: { + NEXT_PUBLIC_PROJECT_VERSION: version || "Unknown Version", + LOG_LEVEL: process.env.LOG_LEVEL, - PRIVATE_PATH: z.string().optional(), - }, - client: { - NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(), - }, - runtimeEnv: { - NEXT_PUBLIC_PROJECT_VERSION: version || "Unknown Version", - LOG_LEVEL: process.env.LOG_LEVEL, + PROJECT_NAME: process.env.PROJECT_NAME, + PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION, + PROJECT_URL: process.env.PROJECT_URL, + PROJECT_SECRET: process.env.PROJECT_SECRET, - PROJECT_NAME: process.env.PROJECT_NAME, - PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION, - PROJECT_URL: process.env.PROJECT_URL, - PROJECT_SECRET: process.env.PROJECT_SECRET, + DATABASE_URL: process.env.DATABASE_URL, - DATABASE_URL: process.env.DATABASE_URL, + TRUSTED_DOMAINS: process.env.TRUSTED_DOMAINS, - TRUSTED_DOMAINS: process.env.TRUSTED_DOMAINS, + SMTP_PASSWORD: process.env.SMTP_PASSWORD, + SMTP_FROM: process.env.SMTP_FROM, + SMTP_HOST: process.env.SMTP_HOST, + SMTP_PORT: process.env.SMTP_PORT, + SMTP_USER: process.env.SMTP_USER, + SMTP_SECURE: process.env.SMTP_SECURE, - SMTP_PASSWORD: process.env.SMTP_PASSWORD, - SMTP_FROM: process.env.SMTP_FROM, - SMTP_HOST: process.env.SMTP_HOST, - SMTP_PORT: process.env.SMTP_PORT, - SMTP_USER: process.env.SMTP_USER, - SMTP_SECURE: process.env.SMTP_SECURE, + RETENTION_CRON: process.env.RETENTION_CRON, + CLEANING_HEALTHCHECK_LOGS_CRON: process.env.CLEANING_HEALTHCHECK_LOGS_CRON, - RETENTION_CRON: process.env.RETENTION_CRON, - CLEANING_HEALTHCHECK_LOGS_CRON: process.env.CLEANING_HEALTHCHECK_LOGS_CRON, + AUTH_OIDC_ID: process.env.AUTH_OIDC_ID, + AUTH_OIDC_TITLE: process.env.AUTH_OIDC_TITLE, + AUTH_OIDC_DESC: process.env.AUTH_OIDC_DESC, + AUTH_OIDC_ICON: process.env.AUTH_OIDC_ICON, + AUTH_OIDC_CLIENT: process.env.AUTH_OIDC_CLIENT, + AUTH_OIDC_SECRET: process.env.AUTH_OIDC_SECRET, + AUTH_OIDC_ISSUER_URL: process.env.AUTH_OIDC_ISSUER_URL, + AUTH_OIDC_HOST: process.env.AUTH_OIDC_HOST, + AUTH_OIDC_SCOPES: process.env.AUTH_OIDC_SCOPES, + AUTH_OIDC_DISCOVERY_ENDPOINT: process.env.AUTH_OIDC_DISCOVERY_ENDPOINT, + AUTH_OIDC_JWKS_ENDPOINT: process.env.AUTH_OIDC_JWKS_ENDPOINT, + AUTH_OIDC_PKCE: process.env.AUTH_OIDC_PKCE, - AUTH_OIDC_ID: process.env.AUTH_OIDC_ID, - AUTH_OIDC_TITLE: process.env.AUTH_OIDC_TITLE, - AUTH_OIDC_DESC: process.env.AUTH_OIDC_DESC, - AUTH_OIDC_ICON: process.env.AUTH_OIDC_ICON, - AUTH_OIDC_CLIENT: process.env.AUTH_OIDC_CLIENT, - AUTH_OIDC_SECRET: process.env.AUTH_OIDC_SECRET, - AUTH_OIDC_ISSUER_URL: process.env.AUTH_OIDC_ISSUER_URL, - AUTH_OIDC_HOST: process.env.AUTH_OIDC_HOST, - AUTH_OIDC_SCOPES: process.env.AUTH_OIDC_SCOPES, - AUTH_OIDC_DISCOVERY_ENDPOINT: process.env.AUTH_OIDC_DISCOVERY_ENDPOINT, - AUTH_OIDC_JWKS_ENDPOINT: process.env.AUTH_OIDC_JWKS_ENDPOINT, - AUTH_OIDC_PKCE: process.env.AUTH_OIDC_PKCE, + AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID, + AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET, - AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID, - AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET, + AUTH_GITHUB_ID: process.env.AUTH_GITHUB_ID, + AUTH_GITHUB_SECRET: process.env.AUTH_GITHUB_SECRET, - AUTH_GITHUB_ID: process.env.AUTH_GITHUB_ID, - AUTH_GITHUB_SECRET: process.env.AUTH_GITHUB_SECRET, + AUTH_SOCIAL_ID: process.env.AUTH_SOCIAL_ID, + AUTH_SOCIAL_TITLE: process.env.AUTH_SOCIAL_TITLE, + AUTH_SOCIAL_DESC: process.env.AUTH_SOCIAL_DESC, + AUTH_SOCIAL_ICON: process.env.AUTH_SOCIAL_ICON, + AUTH_SOCIAL_CLIENT: process.env.AUTH_SOCIAL_CLIENT, + AUTH_SOCIAL_SECRET: process.env.AUTH_SOCIAL_SECRET, - AUTH_SOCIAL_ID: process.env.AUTH_SOCIAL_ID, - AUTH_SOCIAL_TITLE: process.env.AUTH_SOCIAL_TITLE, - AUTH_SOCIAL_DESC: process.env.AUTH_SOCIAL_DESC, - AUTH_SOCIAL_ICON: process.env.AUTH_SOCIAL_ICON, - AUTH_SOCIAL_CLIENT: process.env.AUTH_SOCIAL_CLIENT, - AUTH_SOCIAL_SECRET: process.env.AUTH_SOCIAL_SECRET, + ALLOWED_GROUP: process.env.ALLOWED_GROUP, - ALLOWED_GROUP: process.env.ALLOWED_GROUP, + AUTH_EMAIL_PASSWORD_ENABLED: process.env.AUTH_EMAIL_PASSWORD_ENABLED, + AUTH_SIGNUP_ENABLED: process.env.AUTH_SIGNUP_ENABLED, + AUTH_PASSKEY_ENABLED: process.env.AUTH_PASSKEY_ENABLED, - AUTH_EMAIL_PASSWORD_ENABLED: process.env.AUTH_EMAIL_PASSWORD_ENABLED, - AUTH_SIGNUP_ENABLED: process.env.AUTH_SIGNUP_ENABLED, - AUTH_PASSKEY_ENABLED: process.env.AUTH_PASSKEY_ENABLED, + AUTH_SYNC_OIDC_ROLES_ON_LOGIN: process.env.AUTH_SYNC_OIDC_ROLES_ON_LOGIN, - AUTH_SYNC_OIDC_ROLES_ON_LOGIN: process.env.AUTH_SYNC_OIDC_ROLES_ON_LOGIN, + AUTH_ROLE_MAP: process.env.AUTH_ROLE_MAP, - AUTH_ROLE_MAP: process.env.AUTH_ROLE_MAP, + AUTH_ALLOW_LINKING: process.env.AUTH_ALLOW_LINKING, + AUTH_ALLOW_UNLINKING: process.env.AUTH_ALLOW_UNLINKING, - AUTH_ALLOW_LINKING: process.env.AUTH_ALLOW_LINKING, - AUTH_ALLOW_UNLINKING: process.env.AUTH_ALLOW_UNLINKING, + PRIVATE_PATH: + process.env.PRIVATE_PATH || path.join(process.cwd(), "private"), - PRIVATE_PATH: - process.env.PRIVATE_PATH || path.join(process.cwd(), "private"), - }, + AUTH_DEFAULT_USER_NAME: process.env.AUTH_DEFAULT_USER_NAME, + AUTH_DEFAULT_USER: process.env.AUTH_DEFAULT_USER, + AUTH_DEFAULT_PASSWORD: process.env.AUTH_DEFAULT_PASSWORD, + }, }); diff --git a/src/types/auth.ts b/src/types/auth.ts index 41139d71..d3727eef 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -1,11 +1,11 @@ - export type SignUpUser = { name: string email: string - password?: string + password: string callbackURL?: string role?: string theme: string + emailVerified?: boolean } diff --git a/src/utils/init.ts b/src/utils/init.ts deleted file mode 100644 index d3bc859e..00000000 --- a/src/utils/init.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { env } from "@/env.mjs"; -import { db, makeMigration } from "@/db"; -import { eq } from "drizzle-orm"; -import * as drizzleDb from "@/db"; -import {cleaningHealthcheckLogsJob, cleaningJob, healthcheckAgentAndDatabaseJob, retentionJob} from "@/lib/tasks"; -import { generateRSAKeys, getOrCreateMasterKey } from "@/utils/rsa-keys"; -import { StorageProviderKind } from "@/features/storages/types"; -import {logger} from "@/lib/logger"; -import {withUpdatedAt} from "@/db/utils"; - -const log = logger.child({module: "init"}); - -export async function init() { - consoleAscii(); - - log.info("====Init Functions===="); - await getOrCreateMasterKey(); - await generateRSAKeys(); - await makeMigration(); - await createDefaultOrganization(); - await createSettingsIfNotExist(); - log.info("====Initialization completed===="); - await setupCronJobs(); - - if ( - (env.AUTH_GOOGLE_ID && env.AUTH_GOOGLE_SECRET) || - (env.AUTH_GITHUB_ID && env.AUTH_GITHUB_SECRET) - ) { - log.warn( - { - deprecated: true, - provider: "oauth_env", - message: "You have set up OAuth credentials in your environment variables, but the format is now different. Please update your environment variables to use the new format. For example, if you were using AUTH_GOOGLE_ID and AUTH_GOOGLE_SECRET, you should now use AUTH_SOCIAL_GOOGLE_CLIENT and AUTH_SOCIAL_GOOGLE_SECRET. Please refer to the documentation for more details. (https://portabase.io/docs/dashboard/auth/oauth2/setup#dynamic-providers)" - }, - "Deprecated OAuth environment variables detected", - ); - } -} - -async function setupCronJobs() { - - log.info("==== Setting up Cron Jobs ===="); - retentionJob.start(); - cleaningJob.start(); - cleaningHealthcheckLogsJob.start(); - healthcheckAgentAndDatabaseJob.start(); - log.info("==== Cron jobs started ===="); -} - -async function createSettingsIfNotExist() { - await db.transaction(async (tx) => { - - const systemSettingsValues = { - name: "system", - smtpPassword: env.SMTP_PASSWORD ?? null, - smtpFrom: env.SMTP_FROM ?? null, - smtpHost: env.SMTP_HOST ?? null, - smtpPort: env.SMTP_PORT ?? null, - smtpUser: env.SMTP_USER ?? null, - smtpSecure: env.SMTP_SECURE ?? false, - }; - - const [systemSetting] = await tx - .select() - .from(drizzleDb.schemas.setting) - .where(eq(drizzleDb.schemas.setting.name, "system")) - .limit(1); - - const [finalSystemSetting] = systemSetting - ? await tx - .update(drizzleDb.schemas.setting) - .set(systemSettingsValues) - .where(eq(drizzleDb.schemas.setting.name, "system")) - .returning() - : await tx - .insert(drizzleDb.schemas.setting) - .values(systemSettingsValues) - .returning(); - - const localStorageValues = { - provider: "local" as StorageProviderKind, - enabled: true, - name: "System", - config: {}, - }; - - const [existingLocalStorage] = await tx - .select() - .from(drizzleDb.schemas.storageChannel) - .where(eq(drizzleDb.schemas.storageChannel.provider, "local")) - .limit(1); - - const [localStorage] = existingLocalStorage - ? await tx - .update(drizzleDb.schemas.storageChannel) - .set(localStorageValues) - .where(eq(drizzleDb.schemas.storageChannel.provider, "local")) - .returning() - : await tx - .insert(drizzleDb.schemas.storageChannel) - .values(localStorageValues) - .returning(); - - if (!finalSystemSetting.defaultStorageChannelId) { - await tx - .update(drizzleDb.schemas.setting) - .set(withUpdatedAt({ defaultStorageChannelId: localStorage.id })) - .where(eq(drizzleDb.schemas.setting.id, finalSystemSetting.id)); - } - }); -} - -async function createDefaultOrganization() { - const defaultOrganizationConf = { - slug: "default", - name: "Default Organization", - createdAt: new Date(), - }; - - const [existing] = await db - .select() - .from(drizzleDb.schemas.organization) - .where(eq(drizzleDb.schemas.organization.slug, "default")) - .limit(1); - - if (!existing) { - log.info("==== Creating default Organization... ===="); - await db - .insert(drizzleDb.schemas.organization) - .values(defaultOrganizationConf); - } -} - -function consoleAscii() { - console.log( - " \n" + - " ____ __ __ \n" + - " / __ \\____ _____/ /_____ _/ /_ ____ _________ \n" + - " / /_/ / __ \\/ ___/ __/ __ / __ \\/ __ / ___/ _ \\ \n" + - " / ____/ /_/ / / / /_/ /_/ / /_/ / /_/ (__ ) __/ \n" + - " /_/ \\____/_/ \\__/\\__,_/_.___/\\__,_/____/\\___/ \n" + - " \n" + - ` Community Edition v${env.NEXT_PUBLIC_PROJECT_VERSION} \n ` + - " \n", - ); -} diff --git a/src/utils/init/cron.ts b/src/utils/init/cron.ts new file mode 100644 index 00000000..7db2077f --- /dev/null +++ b/src/utils/init/cron.ts @@ -0,0 +1,14 @@ +import {cleaningHealthcheckLogsJob, cleaningJob, healthcheckAgentAndDatabaseJob, retentionJob} from "@/lib/tasks"; +import {logger} from "@/lib/logger"; + +const log = logger.child({module: "init/cron"}); + + +export async function setupCronJobs() { + log.info("==== Setting up Cron Jobs ===="); + retentionJob.start(); + cleaningJob.start(); + cleaningHealthcheckLogsJob.start(); + healthcheckAgentAndDatabaseJob.start(); + log.info("==== Cron jobs started ===="); +} \ No newline at end of file diff --git a/src/utils/init/index.ts b/src/utils/init/index.ts new file mode 100644 index 00000000..9108fb8e --- /dev/null +++ b/src/utils/init/index.ts @@ -0,0 +1,50 @@ +import { env } from "@/env.mjs"; +import { makeMigration } from "@/db"; +import { generateRSAKeys, getOrCreateMasterKey } from "@/utils/rsa-keys"; +import {logger} from "@/lib/logger"; +import {setupCronJobs} from "@/utils/init/cron"; +import {createSettingsIfNotExist} from "@/utils/init/setting"; +import {createDefaultOrganization} from "@/utils/init/organization"; +import {createDefaultUser} from "@/utils/init/user"; + +const log = logger.child({module: "init"}); + +export async function init() { + consoleAscii(); + log.info("====Init Functions===="); + await getOrCreateMasterKey(); + await generateRSAKeys(); + await makeMigration(); + await createDefaultOrganization(); + await createSettingsIfNotExist(); + await createDefaultUser(); + log.info("====Initialization completed===="); + await setupCronJobs(); + if ( + (env.AUTH_GOOGLE_ID && env.AUTH_GOOGLE_SECRET) || + (env.AUTH_GITHUB_ID && env.AUTH_GITHUB_SECRET) + ) { + log.warn( + { + deprecated: true, + provider: "oauth_env", + message: "You have set up OAuth credentials in your environment variables, but the format is now different. Please update your environment variables to use the new format. For example, if you were using AUTH_GOOGLE_ID and AUTH_GOOGLE_SECRET, you should now use AUTH_SOCIAL_GOOGLE_CLIENT and AUTH_SOCIAL_GOOGLE_SECRET. Please refer to the documentation for more details. (https://portabase.io/docs/dashboard/auth/oauth2/setup#dynamic-providers)" + }, + "Deprecated OAuth environment variables detected", + ); + } +} + +function consoleAscii() { + console.log( + " \n" + + " ____ __ __ \n" + + " / __ \\____ _____/ /_____ _/ /_ ____ _________ \n" + + " / /_/ / __ \\/ ___/ __/ __ / __ \\/ __ / ___/ _ \\ \n" + + " / ____/ /_/ / / / /_/ /_/ / /_/ / /_/ (__ ) __/ \n" + + " /_/ \\____/_/ \\__/\\__,_/_.___/\\__,_/____/\\___/ \n" + + " \n" + + ` Community Edition v${env.NEXT_PUBLIC_PROJECT_VERSION} \n ` + + " \n", + ); +} diff --git a/src/utils/init/organization.ts b/src/utils/init/organization.ts new file mode 100644 index 00000000..1058cd24 --- /dev/null +++ b/src/utils/init/organization.ts @@ -0,0 +1,27 @@ +import {db} from "@/db"; +import * as drizzleDb from "@/db"; +import {eq} from "drizzle-orm"; +import {logger} from "@/lib/logger"; + +const log = logger.child({module: "init/organization"}); + +export async function createDefaultOrganization() { + const defaultOrganizationConf = { + slug: "default", + name: "Default Organization", + createdAt: new Date(), + }; + + const [existing] = await db + .select() + .from(drizzleDb.schemas.organization) + .where(eq(drizzleDb.schemas.organization.slug, "default")) + .limit(1); + + if (!existing) { + log.info("==== Creating default Organization... ===="); + await db + .insert(drizzleDb.schemas.organization) + .values(defaultOrganizationConf); + } +} \ No newline at end of file diff --git a/src/utils/init/setting.ts b/src/utils/init/setting.ts new file mode 100644 index 00000000..04b095d3 --- /dev/null +++ b/src/utils/init/setting.ts @@ -0,0 +1,70 @@ +import {db} from "@/db"; +import {env} from "@/env.mjs"; +import * as drizzleDb from "@/db"; +import {eq} from "drizzle-orm"; +import {StorageProviderKind} from "@/features/storages/types"; +import {withUpdatedAt} from "@/db/utils"; + + +export async function createSettingsIfNotExist() { + await db.transaction(async (tx) => { + + const systemSettingsValues = { + name: "system", + smtpPassword: env.SMTP_PASSWORD ?? null, + smtpFrom: env.SMTP_FROM ?? null, + smtpHost: env.SMTP_HOST ?? null, + smtpPort: env.SMTP_PORT ?? null, + smtpUser: env.SMTP_USER ?? null, + smtpSecure: env.SMTP_SECURE ?? false, + }; + + const [systemSetting] = await tx + .select() + .from(drizzleDb.schemas.setting) + .where(eq(drizzleDb.schemas.setting.name, "system")) + .limit(1); + + const [finalSystemSetting] = systemSetting + ? await tx + .update(drizzleDb.schemas.setting) + .set(systemSettingsValues) + .where(eq(drizzleDb.schemas.setting.name, "system")) + .returning() + : await tx + .insert(drizzleDb.schemas.setting) + .values(systemSettingsValues) + .returning(); + + const localStorageValues = { + provider: "local" as StorageProviderKind, + enabled: true, + name: "System", + config: {}, + }; + + const [existingLocalStorage] = await tx + .select() + .from(drizzleDb.schemas.storageChannel) + .where(eq(drizzleDb.schemas.storageChannel.provider, "local")) + .limit(1); + + const [localStorage] = existingLocalStorage + ? await tx + .update(drizzleDb.schemas.storageChannel) + .set(localStorageValues) + .where(eq(drizzleDb.schemas.storageChannel.provider, "local")) + .returning() + : await tx + .insert(drizzleDb.schemas.storageChannel) + .values(localStorageValues) + .returning(); + + if (!finalSystemSetting.defaultStorageChannelId) { + await tx + .update(drizzleDb.schemas.setting) + .set(withUpdatedAt({ defaultStorageChannelId: localStorage.id })) + .where(eq(drizzleDb.schemas.setting.id, finalSystemSetting.id)); + } + }); +} \ No newline at end of file diff --git a/src/utils/init/user.ts b/src/utils/init/user.ts new file mode 100644 index 00000000..d5c653e5 --- /dev/null +++ b/src/utils/init/user.ts @@ -0,0 +1,71 @@ +import {db} from "@/db"; +import * as drizzleDb from "@/db"; +import {count, eq} from "drizzle-orm"; +import {logger} from "@/lib/logger"; +import type { SignUpUser } from "@/types/auth"; +import {env} from "@/env.mjs"; +import {createUserDb} from "@/db/services/user"; + +const log = logger.child({module: "init/user"}); + +export async function createDefaultUser() { + + const hasRequiredEnv = + env.AUTH_DEFAULT_USER && + env.AUTH_DEFAULT_PASSWORD && + env.AUTH_DEFAULT_USER_NAME; + + if (!hasRequiredEnv) { + log.info( + "Default admin creation skipped: missing environment variables.", + ); + return; + } + + log.info("Checking default super admin..."); + + const existingSuperAdmin = await db.query.user.findFirst({ + where: eq(drizzleDb.schemas.user.role, "superadmin"), + }); + + if (existingSuperAdmin) { + log.info( + `CreateDefaultUser skipped: a superadmin already exists (${existingSuperAdmin.email}).`, + ); + return; + } + + const userData: SignUpUser = { + name: env.AUTH_DEFAULT_USER_NAME!, + email: env.AUTH_DEFAULT_USER!, + password: env.AUTH_DEFAULT_PASSWORD!, + theme: "system", + role: "superadmin", + emailVerified: true, + }; + + + const newUser = await createUserDb(userData); + + if (newUser) { + log.info(`Default super admin created (${userData.email}).`); + + const defaultOrgSlug = "default"; + const defaultOrg = await db.query.organization.findFirst({ + where: eq(drizzleDb.schemas.organization.slug, defaultOrgSlug), + }); + + if (defaultOrg) { + await db.insert(drizzleDb.schemas.member).values({ + userId: newUser.id, + organizationId: defaultOrg.id, + role: "owner", + }); + } else { + log.warn( + "Default organization not found. Cannot assign member.", + ); + } + + } +} diff --git a/src/utils/password.ts b/src/utils/password.ts index 53b07108..f88fe7e4 100644 --- a/src/utils/password.ts +++ b/src/utils/password.ts @@ -24,4 +24,61 @@ export function generateValidPassword(length = 12) { } return passwordChars.join(''); +} + + +export interface PasswordValidationResult { + valid: boolean; + errors: string[]; + score: number; +} + +const PASSWORD_RULES = [ + { + key: "minLength", + regex: /.{8,}/, + message: "Password must contain at least 8 characters", + }, + { + key: "number", + regex: /[0-9]/, + message: "Password must contain at least 1 number", + }, + { + key: "lowercase", + regex: /[a-z]/, + message: "Password must contain at least 1 lowercase letter", + }, + { + key: "uppercase", + regex: /[A-Z]/, + message: "Password must contain at least 1 uppercase letter", + }, + { + key: "specialChar", + regex: /[^a-zA-Z0-9]/, + message: "Password must contain at least 1 special character", + }, +]; + +export function validatePassword( + password: string, +): PasswordValidationResult { + const failedRules = PASSWORD_RULES.filter( + (rule) => !rule.regex.test(password), + ); + + return { + valid: failedRules.length === 0, + errors: failedRules.map((r) => r.message), + score: PASSWORD_RULES.length - failedRules.length, + }; +} + +export function assertValidPassword(password: string) { + const result = validatePassword(password); + + if (!result.valid) { + throw new Error(result.errors.join(", ")); + } } \ No newline at end of file