Merge branch 'main' into dev

This commit is contained in:
Charles GTE
2026-05-21 19:28:57 +02:00
20 changed files with 3122 additions and 285 deletions
+9
View File
@@ -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=
+1 -1
View File
@@ -33,5 +33,5 @@ keywords:
- web-ui
- agent
license: Apache-2.0
version: 1.15.0
version: 1.15.3
date-released: '2026-03-02'
@@ -30,9 +30,11 @@ export default async function RoutePage(props: PageParams<{}>) {
<PageHeader className="flex flex-col">
<div className="flex justify-between">
<PageTitle className="mb-3">Active users</PageTitle>
<PageActions>
<AdminUserAddModal organizations={organizations}/>
</PageActions>
{isPasswordAuthEnabled && (
<PageActions>
<AdminUserAddModal organizations={organizations}/>
</PageActions>
)}
</div>
</PageHeader>
<PageContent className="flex flex-col gap-5">
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "portabase",
"version": "1.15.0",
"version": "1.15.3",
"private": true,
"scripts": {
"dev": "next dev --turbopack -p 8887",
@@ -136,5 +136,5 @@
"typescript": "^5.9.3",
"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,
theme: "dark",
role: "user",
password: ""
};
if (isPasswordAuthEnabled) {
@@ -0,0 +1,2 @@
ALTER TABLE "backups" ALTER COLUMN "file_size" SET DATA TYPE bigint;--> statement-breakpoint
ALTER TABLE "backup_storage" ALTER COLUMN "size" SET DATA TYPE bigint;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -386,6 +386,13 @@
"when": 1778523056918,
"tag": "0054_hesitant_darkhawk",
"breakpoints": true
},
{
"idx": 55,
"version": "7",
"when": 1779342014802,
"tag": "0055_yielding_justin_hammer",
"breakpoints": true
}
]
}
+2 -2
View File
@@ -1,4 +1,4 @@
import {pgTable, text, boolean, timestamp, uuid, integer, pgEnum} from "drizzle-orm/pg-core";
import {pgTable, text, boolean, timestamp, uuid, integer, pgEnum, bigint} from "drizzle-orm/pg-core";
import {Agent, agent, AgentWith} from "./08_agent";
import {Project, project} from "./06_project";
import {relations} from "drizzle-orm";
@@ -37,7 +37,7 @@ export const backup = pgTable(
id: uuid("id").primaryKey().defaultRandom(),
status: statusEnum("status").default("waiting").notNull(),
file: text("file"),
fileSize: integer("file_size"),
fileSize: bigint("file_size", { mode: "number" }),
databaseId: uuid("database_id")
.notNull()
.references(() => database.id, {onDelete: "cascade"}),
+2 -2
View File
@@ -1,4 +1,4 @@
import { pgTable, uuid, text, integer, pgEnum } from "drizzle-orm/pg-core";
import {pgTable, uuid, text, integer, pgEnum, bigint} from "drizzle-orm/pg-core";
import { timestamps } from "@/db/schema/00_common";
import {StorageChannel, storageChannel} from "@/db/schema/12_storage-channel";
import {Backup, backup, Restoration} from "@/db/schema/07_database";
@@ -22,7 +22,7 @@ export const backupStorage = pgTable("backup_storage", {
.references(() => storageChannel.id, { onDelete: "cascade" }),
status: backupStorageStatusEnum("status").notNull().default("pending"),
path: text("path"),
size: integer("size"),
size: bigint("size", { mode: "number" }),
checksum: text("checksum"),
...timestamps,
});
+4
View File
@@ -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<User> {
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,
+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 { 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,
},
});
+2 -2
View File
@@ -1,11 +1,11 @@
export type SignUpUser = {
name: string
email: string
password?: string
password: string
callbackURL?: string
role?: 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
@@ -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(", "));
}
}