Compare commits

...
24 Commits
Author SHA1 Message Date
charlesgauthereau afd98ae652 chore(release): 1.2.8-rc.1 2026-02-17 08:48:52 +01:00
charlesgauthereau 8f905c8128 fix: stuck backups with ongoing status but deleted, for legacy. 2026-02-17 08:47:57 +01:00
charlesgauthereau f43f30c9b9 chore(release): 1.2.7 2026-02-16 16:40:01 +01:00
charlesgauthereau 6b1bea9b17 Merge remote-tracking branch 'origin/main' 2026-02-16 16:39:31 +01:00
charlesgauthereau cdaed9766e fix: twice email sent by better auth 2026-02-16 16:39:19 +01:00
Charles GTEandGitHub d31a9ab2d2 Merge pull request #87 from Portabase/dev
1.2.7
2026-02-16 16:38:36 +01:00
Killian LarcherandGitHub 07d49749fb Merge pull request #86 from Portabase/fix/test-button-notif
Fix/test button notif
2026-02-16 16:35:44 +01:00
killianlarcher a1f1da8a07 chore(release): 1.2.7-rc.3 2026-02-16 16:17:30 +01:00
killian-larcher 1b791fd287 fix: re-enable Test Button when editing notification channel. 2026-02-16 16:14:41 +01:00
charlesgauthereau 2a4af58a51 Merge remote-tracking branch 'origin/dev' into dev 2026-02-16 15:37:37 +01:00
charlesgauthereau b12f8ca044 chore(release): 1.2.7-rc.2 2026-02-16 15:37:06 +01:00
Charles GTEandGitHub 519b3abd1d Merge pull request #84 from Portabase/fix/twice-email-session
fix: twice email sent by better auth
2026-02-16 15:36:06 +01:00
charlesgauthereau e08a5bd6b6 fix: twice email sent by better auth 2026-02-16 15:34:31 +01:00
Killian LarcherandGitHub f6c597b42d Merge pull request #83 from Portabase/fix/optional-csp
Conditionally enable UPGRADE_INSECURE_REQUESTS based on PROJECT_URL scheme
2026-02-16 15:23:49 +01:00
killianlarcher f3b9032174 chore(release): 1.2.7-rc.1 2026-02-16 13:23:36 +01:00
killianlarcher 44b4ee2953 fix(csp): Conditionally sets UPGRADE_INSECURE_REQUESTS based on the PROJECT_URL scheme. 2026-02-16 13:21:09 +01:00
killianlarcher b77f79f42d fix(csp): Conditionally sets UPGRADE_INSECURE_REQUESTS based on the PROJECT_URL scheme. 2026-02-16 13:18:58 +01:00
Charles GTEandGitHub 8017bf7c8a Merge pull request #80 from Portabase/dev
fix: README.md
2026-02-15 11:50:54 +01:00
charlesgauthereau 85f4cda752 fix: README.md 2026-02-15 11:49:01 +01:00
charlesgauthereau 1fd2f553ed chore(release): 1.2.6 2026-02-15 10:59:43 +01:00
Charles GTEandGitHub d68170eda8 Merge pull request #79 from Portabase/dev
Dev
2026-02-15 10:59:09 +01:00
Charles GTEandGitHub 51f27226e7 Merge branch 'main' into dev 2026-02-15 10:48:37 +01:00
Charles GTEandGitHub b345eda7aa Update README.md 2026-02-14 09:32:02 +01:00
charlesgauthereau 7ab213e5e4 chore(release): 1.2.5 2026-02-13 14:35:11 +01:00
17 changed files with 76 additions and 43 deletions
+1 -3
View File
@@ -2,9 +2,7 @@ title = "Custom gitleaks config"
[allowlist] [allowlist]
# Global allowlist patterns (wont be flagged) # Global allowlist patterns (wont be flagged)
regexes = [ regexes = []
"s3SecretAccessKey: env.S3_SECRET_KEY ?? null",
]
paths = [ paths = [
"src/utils/init.ts" "src/utils/init.ts"
] ]
+2 -2
View File
@@ -26,5 +26,5 @@ keywords:
- web-ui - web-ui
- agent - agent
license: Apache-2.0 license: Apache-2.0
version: 1.2.6-rc.2 version: 1.2.8-rc.1
date-released: "2026-02-15" date-released: "2026-02-17"
+3 -5
View File
@@ -7,7 +7,7 @@
<h3 align="center">Portabase</h3> <h3 align="center">Portabase</h3>
<p align="center" style="margin-top: 20px; font-style: italic;"> <p align="center" style="margin-top: 20px; font-style: italic;">
<i>Portabase is a tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with <a href="https://github.com/Portabase/agent">Portabase agents</a> for managing operations securely and efficiently.</i> <i>Portabase is a tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with <a href="https://github.com/Portabase/agent-rust">Portabase agents</a> for managing operations securely and efficiently.</i>
</p> </p>
@@ -23,7 +23,6 @@
[![Self Hosted](https://img.shields.io/badge/self--hosted-yes-brightgreen)](https://github.com/Portabase/portabase) [![Self Hosted](https://img.shields.io/badge/self--hosted-yes-brightgreen)](https://github.com/Portabase/portabase)
[![Open Source](https://img.shields.io/badge/open%20source-❤️-red)](https://github.com/Portabase/portabase) [![Open Source](https://img.shields.io/badge/open%20source-❤️-red)](https://github.com/Portabase/portabase)
[![NextJS][NextJS]][NextJS-url] [![NextJS][NextJS]][NextJS-url]
[![BetterAuth][BetterAuth]][BetterAuth-url] [![BetterAuth][BetterAuth]][BetterAuth-url]
[![Drizzle][Drizzle]][Drizzle-url] [![Drizzle][Drizzle]][Drizzle-url]
@@ -51,14 +50,13 @@
You have 4 ways to install Portabase: You have 4 ways to install Portabase:
- Automated CLI (recommended) - [details](https://portabase.io/docs/dashboard/setup#cli) - Automated CLI (recommended) - [details](https://portabase.io/docs/dashboard/setup#cli)
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker) - Docker Run - [details](https://portabase.io/docs/dashboard/setup#docker)
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker-compose)
- Kubernetes with Helm (soon) - Kubernetes with Helm (soon)
- Development setup - [details](https://portabase.io/docs/dashboard/setup#development) - Development setup - [details](https://portabase.io/docs/dashboard/setup#development)
**Ensure Docker is installed on your machine before getting started.** **Ensure Docker is installed on your machine before getting started.**
## Contributors ## Contributors
[![Contributors](https://contrib.rocks/image?repo=Portabase/portabase)](https://github.com/Portabase/portabase/graphs/contributors) [![Contributors](https://contrib.rocks/image?repo=Portabase/portabase)](https://github.com/Portabase/portabase/graphs/contributors)
@@ -35,12 +35,10 @@ export default async function RoutePage(props: PageParams<{}>) {
<PageHeader> <PageHeader>
<PageTitle>Notification channels</PageTitle> <PageTitle>Notification channels</PageTitle>
<PageActions> <PageActions>
{/*<NotifierAddEditModal adminView={false}/>*/}
<ChannelAddEditModal kind="notification" adminView={false}/> <ChannelAddEditModal kind="notification" adminView={false}/>
</PageActions> </PageActions>
</PageHeader> </PageHeader>
<PageContent> <PageContent>
{/*<NotificationChannelsSection organizations={organizations} notificationChannels={notificationChannels}/>*/}
<ChannelsSection kind="notification" organizations={organizations} <ChannelsSection kind="notification" organizations={organizations}
channels={notificationChannels}/> channels={notificationChannels}/>
</PageContent> </PageContent>
+1 -1
View File
@@ -4,7 +4,7 @@ services:
# context: . # context: .
# dockerfile: docker/dockerfile/Dockerfile # dockerfile: docker/dockerfile/Dockerfile
# target: prod # target: prod
image: portabase/portabase:latest image: portabase/portabase:1.2.7-rc.1
ports: ports:
- '8887:80' - '8887:80'
environment: environment:
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "portabase", "name": "portabase",
"version": "1.2.6-rc.2", "version": "1.2.8-rc.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack -p 8887", "dev": "next dev --turbopack -p 8887",
+1 -3
View File
@@ -1,5 +1,3 @@
export interface AuthProviderConfig { export interface AuthProviderConfig {
id: "google" | "github" | "credential"; id: "google" | "github" | "credential";
isActive: boolean; isActive: boolean;
@@ -64,7 +62,7 @@ export const PORTABASE_DEFAULT_SETTINGS = {
FORM_ACTION: ["'self'"], FORM_ACTION: ["'self'"],
FRAME_ANCESTORS: ["'none'"], FRAME_ANCESTORS: ["'none'"],
BLOCK_ALL_MIXED_CONTENT: false, BLOCK_ALL_MIXED_CONTENT: false,
UPGRADE_INSECURE_REQUESTS: true, UPGRADE_INSECURE_REQUESTS: process.env.PROJECT_URL?.startsWith("https://") || false,
}, },
PERMISSIONS_POLICY: { PERMISSIONS_POLICY: {
CAMERA: ["()"], CAMERA: ["()"],
+1 -1
View File
@@ -4,7 +4,7 @@ set -e
if [ -z "$1" ]; then if [ -z "$1" ]; then
echo "Usage: ./release <version>" echo "Usage: ./release <version>"
echo "Example: ./release v1.0.0" echo "Example: ./release 1.0.0"
exit 1 exit 1
fi fi
@@ -224,7 +224,7 @@ export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}
<div className="flex justify-between mt-4"> <div className="flex justify-between mt-4">
<div> <div>
{kind == "storage" && ( {(!isCreate || kind == "storage") && (
<ChannelTestButton <ChannelTestButton
kind={kind} kind={kind}
organizationId={organization?.id} organizationId={organization?.id}
@@ -34,7 +34,7 @@ export const ChannelTestButton = ({channel, organizationId, kind}: NotifierTestC
message: `We are testing channel ${channel.name}`, message: `We are testing channel ${channel.name}`,
level: 'info', level: 'info',
}; };
const result = await dispatchNotification(payload, undefined, channel, organizationId); const result = await dispatchNotification(payload, undefined, channel.id, organizationId);
if (result.success) { if (result.success) {
toast.success(result.message); toast.success(result.message);
+3 -17
View File
@@ -15,8 +15,8 @@ export const env = createEnv({
PROJECT_NAME: z.string().optional(), PROJECT_NAME: z.string().optional(),
PROJECT_DESCRIPTION: z.string().optional(), PROJECT_DESCRIPTION: z.string().optional(),
PROJECT_URL: z.string().optional(), PROJECT_URL: z.string().regex(/^https?:\/\//, "URL must start with http:// or https://"),
PROJECT_SECRET: z.string().optional(), PROJECT_SECRET: z.string(),
SMTP_PASSWORD: z.string().optional(), SMTP_PASSWORD: z.string().optional(),
SMTP_FROM: z.string().optional(), SMTP_FROM: z.string().optional(),
@@ -29,20 +29,13 @@ export const env = createEnv({
AUTH_GOOGLE_SECRET: z.string().optional(), AUTH_GOOGLE_SECRET: z.string().optional(),
AUTH_GOOGLE_METHOD: z.boolean().default(false), AUTH_GOOGLE_METHOD: z.boolean().default(false),
S3_ENDPOINT: z.string().optional(),
S3_ACCESS_KEY: z.string().optional(),
S3_SECRET_KEY: z.string().optional(),
S3_BUCKET_NAME: z.string().optional(),
S3_PORT: z.string().optional(),
S3_USE_SSL: z.string().optional(),
STORAGE_TYPE: z.enum(["local", "s3"]).optional(), STORAGE_TYPE: z.enum(["local", "s3"]).optional(),
RETENTION_CRON: z RETENTION_CRON: z
.string() .string()
.default(process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *"), .default(process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *"),
PRIVATE_PATH: z.string(), PRIVATE_PATH: z.string().optional(),
}, },
client: { client: {
@@ -69,13 +62,6 @@ export const env = createEnv({
AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET, AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
AUTH_GOOGLE_METHOD: process.env.AUTH_GOOGLE_METHOD === "true", AUTH_GOOGLE_METHOD: process.env.AUTH_GOOGLE_METHOD === "true",
S3_ENDPOINT: process.env.S3_ENDPOINT,
S3_ACCESS_KEY: process.env.S3_ACCESS_KEY,
S3_SECRET_KEY: process.env.S3_SECRET_KEY,
S3_BUCKET_NAME: process.env.S3_BUCKET_NAME,
S3_PORT: process.env.S3_PORT,
S3_USE_SSL: process.env.S3_USE_SSL,
STORAGE_TYPE: process.env.STORAGE_TYPE, STORAGE_TYPE: process.env.STORAGE_TYPE,
RETENTION_CRON: process.env.RETENTION_CRON, RETENTION_CRON: process.env.RETENTION_CRON,
@@ -19,8 +19,6 @@ export const useUpdateCheck = () => {
staleTime: 1000 * 60 * 60, staleTime: 1000 * 60 * 60,
}); });
console.log("newRelease", newRelease);
useEffect(() => { useEffect(() => {
if (!newRelease) return; if (!newRelease) return;
+15 -2
View File
@@ -192,6 +192,8 @@ export const auth = betterAuth({
session: { session: {
create: { create: {
before: async (session, context) => { before: async (session, context) => {
const userId = session.userId; const userId = session.userId;
let memberships = await db.query.member.findMany({ let memberships = await db.query.member.findMany({
@@ -231,6 +233,9 @@ export const auth = betterAuth({
// } // }
// }, // },
after: async (session) => { after: async (session) => {
console.log("session", session);
const user = await db.query.user.findFirst({ const user = await db.query.user.findFirst({
where: eq(drizzleDb.schemas.user.id, session.userId), where: eq(drizzleDb.schemas.user.id, session.userId),
}); });
@@ -244,6 +249,12 @@ export const auth = betterAuth({
return; return;
} }
const lastDiff = user.lastConnectedAt
? new Date(session.createdAt).getTime() - new Date(user.lastConnectedAt).getTime()
: Infinity;
if (lastDiff < 30000) return;
if (user.role === "pending") return; if (user.role === "pending") return;
const deviceInfo = getDeviceDetails(session.userAgent); const deviceInfo = getDeviceDetails(session.userAgent);
@@ -509,7 +520,8 @@ export const checkSlugOrganization = async (slug: string) => {
}); });
return status; return status;
} catch {} } catch {
}
}; };
export const getActiveMember = async () => { export const getActiveMember = async () => {
@@ -532,5 +544,6 @@ export const setActiveOrganization = async (slug: string) => {
organizationSlug: slug, organizationSlug: slug,
}, },
}); });
} catch {} } catch {
}
}; };
+28
View File
@@ -0,0 +1,28 @@
import {db} from "@/db";
import {and, eq, isNotNull, isNull} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {withUpdatedAt} from "@/db/utils";
export const backupCleanTask = async () => {
try {
const backups = await db.query.backup.findMany({
where: and(
isNotNull(drizzleDb.schemas.backup.deletedAt),
eq(drizzleDb.schemas.backup.status, "ongoing")
)
});
console.log(`Backups to clean: ${backups.length}`);
for (const backup of backups) {
await db.update(drizzleDb.schemas.backup).set(withUpdatedAt({
status: "failed",
}))
.where(eq(drizzleDb.schemas.backup.id, backup.id));
}
} catch (e: any) {
console.error("Backup cleanup failed:", e);
throw e;
}
};
+10
View File
@@ -1,6 +1,7 @@
import cron from "node-cron"; import cron from "node-cron";
import {retentionCleanTask} from "@/lib/tasks/database"; import {retentionCleanTask} from "@/lib/tasks/database";
import {env} from "@/env.mjs"; import {env} from "@/env.mjs";
import {backupCleanTask} from "@/lib/tasks/cleaning";
export const retentionJob = cron.schedule(env.RETENTION_CRON, async () => { export const retentionJob = cron.schedule(env.RETENTION_CRON, async () => {
@@ -11,3 +12,12 @@ export const retentionJob = cron.schedule(env.RETENTION_CRON, async () => {
console.error(`[CRON] Error:`, err); console.error(`[CRON] Error:`, err);
} }
}); });
export const cleaningJob = cron.schedule("* * * * *", async () => {
try {
console.log("Cleaning Job : Starting task");
await backupCleanTask();
} catch (err) {
console.error(`[CRON] Error:`, err);
}
});
-1
View File
@@ -3,7 +3,6 @@ import {getMasterServerKeyContent} from "@/features/keys/keys.action";
export async function generateEdgeKey(serverUrl: string, agentId: string): Promise<string> { export async function generateEdgeKey(serverUrl: string, agentId: string): Promise<string> {
const masterKey = getMasterServerKeyContent() const masterKey = getMasterServerKeyContent()
console.log("Master server key: ", masterKey)
const edgeKeyData = { const edgeKeyData = {
serverUrl, serverUrl,
agentId, agentId,
+8 -1
View File
@@ -2,7 +2,7 @@ import {env} from "@/env.mjs";
import {db, makeMigration} from "@/db"; import {db, makeMigration} from "@/db";
import {eq} from "drizzle-orm"; import {eq} from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {retentionJob} from "@/lib/tasks"; import {cleaningJob, retentionJob} from "@/lib/tasks";
import {generateRSAKeys, getOrCreateMasterKey} from "@/utils/rsa-keys"; import {generateRSAKeys, getOrCreateMasterKey} from "@/utils/rsa-keys";
import {StorageProviderKind} from "@/features/storages/types"; import {StorageProviderKind} from "@/features/storages/types";
@@ -17,6 +17,7 @@ export async function init() {
await createSettingsIfNotExist() await createSettingsIfNotExist()
console.log("====Initialization completed===="); console.log("====Initialization completed====");
await setupCronJobs() await setupCronJobs()
await setupCleaningJobs()
} }
async function setupCronJobs() { async function setupCronJobs() {
@@ -25,6 +26,12 @@ async function setupCronJobs() {
console.log("==== Cron job started ===="); console.log("==== Cron job started ====");
} }
async function setupCleaningJobs() {
console.log("==== Setting up Cleaning Jobs ====");
cleaningJob.start();
console.log("==== Cleaning job started ====");
}
async function createSettingsIfNotExist() { async function createSettingsIfNotExist() {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const systemSettingsValues = { const systemSettingsValues = {