fix: default user from init .env variables (#293)

* fix: default user from init en .env variables

* fix: .env.example

---------

Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
This commit is contained in:
Charles GTE
2026-05-21 07:28:05 +02:00
committed by GitHub
co-authored by charles-gauthereau
parent 332540488d
commit 7d463d7da8
14 changed files with 445 additions and 279 deletions
+9
View File
@@ -15,6 +15,15 @@ PROJECT_NAME="Portabase"
PROJECT_URL=http://localhost:8887 PROJECT_URL=http://localhost:8887
PROJECT_SECRET= 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 (email)
SMTP_HOST= SMTP_HOST=
SMTP_PORT= SMTP_PORT=
@@ -30,9 +30,11 @@ export default async function RoutePage(props: PageParams<{}>) {
<PageHeader className="flex flex-col"> <PageHeader className="flex flex-col">
<div className="flex justify-between"> <div className="flex justify-between">
<PageTitle className="mb-3">Active users</PageTitle> <PageTitle className="mb-3">Active users</PageTitle>
<PageActions> {isPasswordAuthEnabled && (
<AdminUserAddModal organizations={organizations}/> <PageActions>
</PageActions> <AdminUserAddModal organizations={organizations}/>
</PageActions>
)}
</div> </div>
</PageHeader> </PageHeader>
<PageContent className="flex flex-col gap-5"> <PageContent className="flex flex-col gap-5">
+1 -1
View File
@@ -136,5 +136,5 @@
"typescript": "^5.9.3", "typescript": "^5.9.3",
"zenstack": "2.14.2" "zenstack": "2.14.2"
}, },
"packageManager": "pnpm@11.1.2+sha512.415a1cc25974731e75455c1468371be74c5aa5fb7621b50d4056d222451609f11412f23fd602e6169f1e060466641f798597e1be961a10688836a67b16569499" "packageManager": "pnpm@11.1.3"
} }
@@ -35,6 +35,7 @@ export const createUserAction = userAction
email: parsedInput.email, email: parsedInput.email,
theme: "dark", theme: "dark",
role: "user", role: "user",
password: ""
}; };
if (isPasswordAuthEnabled) { if (isPasswordAuthEnabled) {
+4
View File
@@ -3,13 +3,17 @@ import {hashPassword} from "better-auth/crypto";
import {db} from "@/db"; import {db} from "@/db";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {User, UserThemeEnum} from "@/db/schema/02_user"; import {User, UserThemeEnum} from "@/db/schema/02_user";
import {assertValidPassword} from "@/utils/password";
export async function createUserDb(data: SignUpUser): Promise<User> { export async function createUserDb(data: SignUpUser): Promise<User> {
assertValidPassword(data.password);
const now = new Date(); const now = new Date();
const userId = crypto.randomUUID(); const userId = crypto.randomUUID();
const [newUser] = await db.insert(drizzleDb.schemas.user).values({ const [newUser] = await db.insert(drizzleDb.schemas.user).values({
...data,
id: userId, id: userId,
name: data.name, name: data.name,
email: data.email, email: data.email,
+134 -127
View File
@@ -1,167 +1,174 @@
import { createEnv } from "@t3-oss/env-nextjs"; import {createEnv} from "@t3-oss/env-nextjs";
import path from "path"; import path from "path";
import { z } from "zod"; import {z} from "zod";
import packageJson from "../package.json" with { type: "json" }; import packageJson from "../package.json" with {type: "json"};
const { version } = packageJson; const {version} = packageJson;
export const env = createEnv({ export const env = createEnv({
server: { server: {
NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(), NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(),
NODE_ENV: z.enum(["development", "production"]).optional(), NODE_ENV: z.enum(["development", "production"]).optional(),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"), 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_NAME: z.string().optional(),
PROJECT_DESCRIPTION: z.string().optional(), PROJECT_DESCRIPTION: z.string().optional(),
PROJECT_URL: z PROJECT_URL: z
.string() .string()
.regex(/^https?:\/\//, "URL must start with http:// or https://"), .regex(/^https?:\/\//, "URL must start with http:// or https://"),
PROJECT_SECRET: z.string(), PROJECT_SECRET: z.string(),
TRUSTED_DOMAINS: z.string().optional(), TRUSTED_DOMAINS: z.string().optional(),
SMTP_PASSWORD: z.string().optional(), SMTP_PASSWORD: z.string().optional(),
SMTP_FROM: z.string().optional(), SMTP_FROM: z.string().optional(),
SMTP_HOST: z.string().optional(), SMTP_HOST: z.string().optional(),
SMTP_PORT: z.string().optional(), SMTP_PORT: z.string().optional(),
SMTP_USER: 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 SMTP_SECURE: z
.enum(["true", "false"]) .enum(["true", "false"])
.transform((val) => val === "true") .transform((val) => val === "true")
.default("true"), .default("true"),
AUTH_GOOGLE_ID: z.string().optional(), AUTH_GOOGLE_ID: z.string().optional(),
AUTH_GOOGLE_SECRET: z.string().optional(), AUTH_GOOGLE_SECRET: z.string().optional(),
AUTH_GITHUB_ID: z.string().optional(), AUTH_GITHUB_ID: z.string().optional(),
AUTH_GITHUB_SECRET: z.string().optional(), AUTH_GITHUB_SECRET: z.string().optional(),
RETENTION_CRON: z RETENTION_CRON: z
.string() .string()
.default( .default(
process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *", process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *",
), ),
CLEANING_HEALTHCHECK_LOGS_CRON: z CLEANING_HEALTHCHECK_LOGS_CRON: z
.string() .string()
.default( .default(
process.env.NODE_ENV === "production" ? "0 * * * *" : "* * * * *", process.env.NODE_ENV === "production" ? "0 * * * *" : "* * * * *",
), ),
HEALTHCHECK_CRON: z HEALTHCHECK_CRON: z
.string() .string()
.default( .default(
process.env.NODE_ENV === "production" ? "0 * * * *" : "* * * * *", 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_SOCIAL_ID: z.string().optional().default("social"),
AUTH_OIDC_TITLE: z.string().optional(), AUTH_SOCIAL_TITLE: z.string().optional(),
AUTH_OIDC_DESC: z.string().optional(), AUTH_SOCIAL_DESC: z.string().optional(),
AUTH_OIDC_ICON: z.string().optional(), AUTH_SOCIAL_ICON: z.string().optional(),
AUTH_OIDC_CLIENT: z.string().optional(), AUTH_SOCIAL_CLIENT: z.string().optional(),
AUTH_OIDC_SECRET: z.string().optional(), AUTH_SOCIAL_SECRET: z.string().optional(),
AUTH_OIDC_ISSUER_URL: z.string().optional(), AUTH_SOCIAL_APPLE_APP_BUNDLE_IDENTIFIER: 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"), ALLOWED_GROUP: z.string().optional(),
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(), 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_SYNC_OIDC_ROLES_ON_LOGIN: z.enum(["true", "false"]).default("true"),
AUTH_SIGNUP_ENABLED: z.string().optional().default("true"), AUTH_ROLE_MAP: z.string().optional(),
AUTH_PASSKEY_ENABLED: z.string().optional().default("false"), 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"), PRIVATE_PATH: z.string().optional(),
AUTH_ROLE_MAP: z.string().optional(), },
AUTH_DEFAULT_ROLE: z.string().optional().default("pending"), client: {
AUTH_ALLOW_LINKING: z.enum(["true", "false"]).default("true"), NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(),
AUTH_ALLOW_UNLINKING: z.enum(["true", "false"]).default("true"), },
runtimeEnv: {
NEXT_PUBLIC_PROJECT_VERSION: version || "Unknown Version",
LOG_LEVEL: process.env.LOG_LEVEL,
PRIVATE_PATH: z.string().optional(), PROJECT_NAME: process.env.PROJECT_NAME,
}, PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION,
client: { PROJECT_URL: process.env.PROJECT_URL,
NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(), PROJECT_SECRET: process.env.PROJECT_SECRET,
},
runtimeEnv: {
NEXT_PUBLIC_PROJECT_VERSION: version || "Unknown Version",
LOG_LEVEL: process.env.LOG_LEVEL,
PROJECT_NAME: process.env.PROJECT_NAME, DATABASE_URL: process.env.DATABASE_URL,
PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION,
PROJECT_URL: process.env.PROJECT_URL,
PROJECT_SECRET: process.env.PROJECT_SECRET,
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, RETENTION_CRON: process.env.RETENTION_CRON,
SMTP_FROM: process.env.SMTP_FROM, CLEANING_HEALTHCHECK_LOGS_CRON: process.env.CLEANING_HEALTHCHECK_LOGS_CRON,
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, AUTH_OIDC_ID: process.env.AUTH_OIDC_ID,
CLEANING_HEALTHCHECK_LOGS_CRON: process.env.CLEANING_HEALTHCHECK_LOGS_CRON, 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_GOOGLE_ID: process.env.AUTH_GOOGLE_ID,
AUTH_OIDC_TITLE: process.env.AUTH_OIDC_TITLE, AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
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_GITHUB_ID: process.env.AUTH_GITHUB_ID,
AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET, AUTH_GITHUB_SECRET: process.env.AUTH_GITHUB_SECRET,
AUTH_GITHUB_ID: process.env.AUTH_GITHUB_ID, AUTH_SOCIAL_ID: process.env.AUTH_SOCIAL_ID,
AUTH_GITHUB_SECRET: process.env.AUTH_GITHUB_SECRET, 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, ALLOWED_GROUP: process.env.ALLOWED_GROUP,
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, 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_SYNC_OIDC_ROLES_ON_LOGIN: process.env.AUTH_SYNC_OIDC_ROLES_ON_LOGIN,
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_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, PRIVATE_PATH:
AUTH_ALLOW_UNLINKING: process.env.AUTH_ALLOW_UNLINKING, process.env.PRIVATE_PATH || path.join(process.cwd(), "private"),
PRIVATE_PATH: AUTH_DEFAULT_USER_NAME: process.env.AUTH_DEFAULT_USER_NAME,
process.env.PRIVATE_PATH || path.join(process.cwd(), "private"), AUTH_DEFAULT_USER: process.env.AUTH_DEFAULT_USER,
}, AUTH_DEFAULT_PASSWORD: process.env.AUTH_DEFAULT_PASSWORD,
},
}); });
+2 -2
View File
@@ -1,11 +1,11 @@
export type SignUpUser = { export type SignUpUser = {
name: string name: string
email: string email: string
password?: string password: string
callbackURL?: string callbackURL?: string
role?: string role?: string
theme: string theme: string
emailVerified?: boolean
} }
-146
View File
@@ -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",
);
}
+14
View File
@@ -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 ====");
}
+50
View File
@@ -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",
);
}
+27
View File
@@ -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);
}
}
+70
View File
@@ -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));
}
});
}
+71
View File
@@ -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.",
);
}
}
}
+57
View File
@@ -25,3 +25,60 @@ export function generateValidPassword(length = 12) {
return passwordChars.join(''); 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(", "));
}
}