mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e32539e02c | ||
|
|
46ba931a6c | ||
|
|
4bac5143d6 | ||
|
|
bcb1674491 | ||
|
|
5074748055 | ||
|
|
6fa11f834b | ||
|
|
b546556d08 | ||
|
|
379a85f6f6 | ||
|
|
92d2c14d6a | ||
|
|
a6c1f7d3f2 | ||
|
|
6fa6187cc4 | ||
|
|
848e7a1609 | ||
|
|
3e8c0a2896 | ||
|
|
11900b1500 | ||
|
|
5668a1411f | ||
|
|
6db60f111d | ||
|
|
5c682addc9 | ||
|
|
24810cc818 | ||
|
|
8caeb11da5 | ||
|
|
5e07b446fc | ||
|
|
760388307c | ||
|
|
eebd8500ee | ||
|
|
12112afa37 | ||
|
|
cddb0a7e05 | ||
|
|
21b317e4f1 |
+4
-15
@@ -1,18 +1,18 @@
|
|||||||
# Environnement
|
# Environnement
|
||||||
NODE_ENV=production
|
NODE_ENV=development
|
||||||
|
|
||||||
# Base de données
|
# Base de données
|
||||||
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
|
DATABASE_URL=postgresql://devuser:changeme@localhost:5432/devdb?schema=public
|
||||||
|
|
||||||
# Projet
|
# Projet
|
||||||
PROJECT_NAME="Portabase"
|
PROJECT_NAME="Portabase"
|
||||||
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
||||||
PROJECT_URL=http://app.portabase.io
|
PROJECT_URL=http://localhost:8887
|
||||||
PROJECT_SECRET=
|
PROJECT_SECRET=
|
||||||
|
|
||||||
# SMTP (email)
|
# SMTP (email)
|
||||||
SMTP_HOST=
|
SMTP_HOST=
|
||||||
SMTP_PORT=587
|
SMTP_PORT=
|
||||||
SMTP_USER=
|
SMTP_USER=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
SMTP_FROM=
|
SMTP_FROM=
|
||||||
@@ -22,16 +22,5 @@ AUTH_GOOGLE_ID=
|
|||||||
AUTH_GOOGLE_SECRET=
|
AUTH_GOOGLE_SECRET=
|
||||||
AUTH_GOOGLE_METHOD=
|
AUTH_GOOGLE_METHOD=
|
||||||
|
|
||||||
# S3
|
|
||||||
S3_ENDPOINT=http://app.s3.portabase.io
|
|
||||||
S3_ACCESS_KEY=
|
|
||||||
S3_SECRET_KEY=
|
|
||||||
S3_BUCKET_NAME=portabase
|
|
||||||
S3_PORT=9000
|
|
||||||
S3_USE_SSL=true
|
|
||||||
|
|
||||||
# Storage Type (s3, local)
|
|
||||||
STORAGE_TYPE=local
|
|
||||||
|
|
||||||
# Retention
|
# Retention
|
||||||
RETENTION_CRON="* * * * *"
|
RETENTION_CRON="* * * * *"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
.idea
|
.idea
|
||||||
|
.vscode
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
|
|||||||
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/portabase.iml" filepath="$PROJECT_DIR$/.idea/portabase.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Generated
-12
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="JAVA_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
||||||
<exclude-output />
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/backend" isTestSource="false" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
Generated
-6
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Vendored
-3
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"WillLuke.nextjs.hasPrompted": true
|
|
||||||
}
|
|
||||||
+2
-2
@@ -26,5 +26,5 @@ keywords:
|
|||||||
- web-ui
|
- web-ui
|
||||||
- agent
|
- agent
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
version: 1.2.0-rc.1
|
version: 1.2.1-rc.2
|
||||||
date-released: "2026-01-18"
|
date-released: "2026-01-19"
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ export async function POST(
|
|||||||
const generatedId = formData.get("generatedId") as string | null;
|
const generatedId = formData.get("generatedId") as string | null;
|
||||||
const method = formData.get("method") as string | null;
|
const method = formData.get("method") as string | null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (!generatedId || !isUuidv4(generatedId)) {
|
if (!generatedId || !isUuidv4(generatedId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "generatedId is not a valid UUID"},
|
{error: "generatedId is not a valid UUID"},
|
||||||
@@ -113,6 +116,7 @@ export async function POST(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "File is required for successful backup"},
|
{error: "File is required for successful backup"},
|
||||||
@@ -127,7 +131,11 @@ export async function POST(
|
|||||||
const fileName = `${uuid}${fileExtension}`;
|
const fileName = `${uuid}${fileExtension}`;
|
||||||
const buffer = Buffer.from(await decryptedFile.arrayBuffer());
|
const buffer = Buffer.from(await decryptedFile.arrayBuffer());
|
||||||
|
|
||||||
await storeBackupFiles(backup, database, buffer, fileName)
|
|
||||||
|
console.log(extension)
|
||||||
|
|
||||||
|
|
||||||
|
const storageResults = await storeBackupFiles(backup, database, buffer, fileName)
|
||||||
eventEmitter.emit('modification', {update: true});
|
eventEmitter.emit('modification', {update: true});
|
||||||
|
|
||||||
await sendNotificationsBackupRestore(database, "success_backup");
|
await sendNotificationsBackupRestore(database, "success_backup");
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {Database} from "@/db/schema/07_database";
|
|||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db as dbClient} from "@/db";
|
import {db as dbClient} from "@/db";
|
||||||
import {and, eq, inArray} from "drizzle-orm";
|
import {and, eq, inArray} from "drizzle-orm";
|
||||||
import {EDbmsSchema} from "@/db/schema/types";
|
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
||||||
import {ServerActionResult} from "@/types/action-type";
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
import {SafeActionResult} from "next-safe-action";
|
import {SafeActionResult} from "next-safe-action";
|
||||||
import {ZodString} from "zod";
|
import {ZodString} from "zod";
|
||||||
@@ -50,6 +50,12 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
{status: 500}
|
{status: 500}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!dbmsEnumSchema.safeParse(db.dbms).success) {
|
||||||
|
console.log(`Database type not available: ${db.dbms}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const [databaseCreated] = await dbClient
|
const [databaseCreated] = await dbClient
|
||||||
.insert(drizzleDb.schemas.database)
|
.insert(drizzleDb.schemas.database)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.2.0-rc.1",
|
"version": "1.2.1-rc.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 8887",
|
"dev": "next dev --turbopack -p 8887",
|
||||||
@@ -74,6 +74,7 @@
|
|||||||
"nodemailer": "^7.0.3",
|
"nodemailer": "^7.0.3",
|
||||||
"npm-check-updates": "^18.0.1",
|
"npm-check-updates": "^18.0.1",
|
||||||
"pg": "^8.16.0",
|
"pg": "^8.16.0",
|
||||||
|
"prettier": "^3.8.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-day-picker": "9.7.0",
|
"react-day-picker": "9.7.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
|||||||
Generated
+8
-5
@@ -188,6 +188,9 @@ importers:
|
|||||||
pg:
|
pg:
|
||||||
specifier: ^8.16.0
|
specifier: ^8.16.0
|
||||||
version: 8.16.3
|
version: 8.16.3
|
||||||
|
prettier:
|
||||||
|
specifier: ^3.8.0
|
||||||
|
version: 3.8.0
|
||||||
react:
|
react:
|
||||||
specifier: ^19.2.0
|
specifier: ^19.2.0
|
||||||
version: 19.2.3
|
version: 19.2.3
|
||||||
@@ -5669,8 +5672,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
prettier@3.7.4:
|
prettier@3.8.0:
|
||||||
resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
|
resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -9473,7 +9476,7 @@ snapshots:
|
|||||||
'@react-email/render@1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
'@react-email/render@1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
html-to-text: 9.0.5
|
html-to-text: 9.0.5
|
||||||
prettier: 3.7.4
|
prettier: 3.8.0
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
react-dom: 19.2.3(react@19.2.3)
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
react-promise-suspense: 0.3.4
|
react-promise-suspense: 0.3.4
|
||||||
@@ -9481,7 +9484,7 @@ snapshots:
|
|||||||
'@react-email/render@2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
'@react-email/render@2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
html-to-text: 9.0.5
|
html-to-text: 9.0.5
|
||||||
prettier: 3.7.4
|
prettier: 3.8.0
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
react-dom: 19.2.3(react@19.2.3)
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
|
|
||||||
@@ -12798,7 +12801,7 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
prettier@3.7.4: {}
|
prettier@3.8.0: {}
|
||||||
|
|
||||||
pretty-bytes@6.1.1: {}
|
pretty-bytes@6.1.1: {}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -1,27 +0,0 @@
|
|||||||
import {cn} from "@/lib/utils";
|
|
||||||
|
|
||||||
export type ConnectionCircleProps = {
|
|
||||||
date?: Date | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ConnectionCircle = ({date}: ConnectionCircleProps) => {
|
|
||||||
let style = "bg-gray-300 border-gray-400";
|
|
||||||
|
|
||||||
if (date instanceof Date && !isNaN(date.getTime())) {
|
|
||||||
const now = Date.now();
|
|
||||||
const timestamp = date.getTime();
|
|
||||||
const interval_seconds = (now - timestamp) / 1000;
|
|
||||||
|
|
||||||
if (interval_seconds < 55) {
|
|
||||||
style = "bg-green-400 border-green-600";
|
|
||||||
} else if (interval_seconds <= 60) {
|
|
||||||
style = "bg-orange-400 border-orange-600";
|
|
||||||
} else {
|
|
||||||
style = "bg-red-400 border-red-600";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn("Invalid date passed to ConnectionCircle:", date);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={cn("w-5 h-5 rounded-full border-4", style)}/>;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import {cn} from "@/lib/utils";
|
||||||
|
|
||||||
|
export type ConnectionIndicatorProps = {
|
||||||
|
date?: Date | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConnectionIndicator = ({date}: ConnectionIndicatorProps) => {
|
||||||
|
let style = "bg-gray-300";
|
||||||
|
|
||||||
|
if (date instanceof Date && !isNaN(date.getTime())) {
|
||||||
|
const intervalSeconds = (Date.now() - date.getTime()) / 1000;
|
||||||
|
|
||||||
|
if (intervalSeconds < 55) {
|
||||||
|
style = "bg-green-500";
|
||||||
|
} else if (intervalSeconds <= 60) {
|
||||||
|
style = "bg-orange-400";
|
||||||
|
} else {
|
||||||
|
style = "bg-red-500";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-3 h-3">
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"absolute -inset-0.25 rounded-full opacity-60 animate-ping",
|
||||||
|
style
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
animationDuration: "2s",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative w-3 h-3 rounded-full shadow-sm animate-pulse",
|
||||||
|
style
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
animationDuration: "2s",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+32
-7
@@ -25,8 +25,8 @@ import {
|
|||||||
ChannelKind,
|
ChannelKind,
|
||||||
renderChannelForm
|
renderChannelForm
|
||||||
} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||||
import {storageTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/storage";
|
import {storageProviders} from "@/components/wrappers/dashboard/admin/channels/helpers/storage";
|
||||||
import {notificationTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/notification";
|
import {notificationProviders} from "@/components/wrappers/dashboard/admin/channels/helpers/notification";
|
||||||
import {
|
import {
|
||||||
ChannelTestButton
|
ChannelTestButton
|
||||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-test-button";
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-test-button";
|
||||||
@@ -94,7 +94,7 @@ export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}
|
|||||||
});
|
});
|
||||||
|
|
||||||
const provider = form.watch("provider");
|
const provider = form.watch("provider");
|
||||||
const channelTypes = kind == "notification" ? notificationTypes : storageTypes
|
const channelTypes = kind == "notification" ? notificationProviders : storageProviders
|
||||||
const selectedProviderDetails = channelTypes.find(t => t.value === provider);
|
const selectedProviderDetails = channelTypes.find(t => t.value === provider);
|
||||||
|
|
||||||
if (isCreate && !provider) {
|
if (isCreate && !provider) {
|
||||||
@@ -106,18 +106,43 @@ export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}
|
|||||||
<Card
|
<Card
|
||||||
key={type.value}
|
key={type.value}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-center justify-center gap-3 p-4 cursor-pointer hover:bg-accent/50 hover:border-primary/50 transition-all",
|
"relative flex flex-col items-center justify-center gap-3 p-4 transition-all",
|
||||||
|
type.preview
|
||||||
|
? "cursor-not-allowed bg-gray-200 border-gray-300 opacity-70"
|
||||||
|
: "cursor-pointer hover:bg-accent/50 hover:border-primary/50"
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (type.preview) return;
|
||||||
form.setValue("provider", type.value as any);
|
form.setValue("provider", type.value as any);
|
||||||
form.setValue("config", {});
|
form.setValue("config", {});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="h-10 w-10 bg-secondary rounded-full flex items-center justify-center">
|
{type.preview && (
|
||||||
<Icon className="h-6 w-6 text-foreground"/>
|
<div
|
||||||
|
className="absolute bottom-0 right-0 overflow-hidden w-20 h-20 pointer-events-none">
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 right-0 w-38 h-6 flex items-center justify-center
|
||||||
|
transform -rotate-45 translate-x-14 -translate-y-4"
|
||||||
|
style={{backgroundColor: "#FE6702"}}
|
||||||
|
>
|
||||||
|
<span className="text-white text-[8px] pr-3 font-medium text-center w-full">coming soon</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={cn(
|
||||||
|
"h-10 w-10 rounded-full flex items-center justify-center",
|
||||||
|
type.preview ? "bg-gray-400" : "bg-secondary"
|
||||||
|
)}>
|
||||||
|
<Icon className={cn("h-6 w-6 text-foreground", type.preview && "text-gray-700")}/>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-sm">{type.label}</span>
|
<span className={cn(
|
||||||
|
"font-medium text-sm align-middle text-center",
|
||||||
|
type.preview ? "text-gray-500" : "text-foreground"
|
||||||
|
)}>
|
||||||
|
{type.label}
|
||||||
|
</span>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,11 +20,10 @@ import {
|
|||||||
import {
|
import {
|
||||||
NotifierWebhookForm
|
NotifierWebhookForm
|
||||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/webhook.form";
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/webhook.form";
|
||||||
import {notificationTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/notification";
|
import {
|
||||||
import {storageTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/storage";
|
notificationProviders,
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
} from "@/components/wrappers/dashboard/admin/channels/helpers/notification";
|
||||||
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
import {storageProviders} from "@/components/wrappers/dashboard/admin/channels/helpers/storage";
|
||||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
|
||||||
import {ForwardRefExoticComponent, JSX, RefAttributes, SVGProps} from "react";
|
import {ForwardRefExoticComponent, JSX, RefAttributes, SVGProps} from "react";
|
||||||
import {LucideProps} from "lucide-react";
|
import {LucideProps} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
@@ -45,19 +44,21 @@ export function getChannelTextBasedOnKind(kind: ChannelKind) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type ProviderIconTypes = {
|
export type ProviderIconTypes = {
|
||||||
value: string
|
value: string
|
||||||
label: string
|
label: string
|
||||||
icon: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>
|
icon: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>
|
||||||
|
preview?: boolean
|
||||||
} | {
|
} | {
|
||||||
value: string
|
value: string
|
||||||
label: string
|
label: string
|
||||||
icon: (props: SVGProps<SVGSVGElement>) => JSX.Element
|
icon: (props: SVGProps<SVGSVGElement>) => JSX.Element
|
||||||
|
preview?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerIcons: ProviderIconTypes[] = [
|
export const providerIcons: ProviderIconTypes[] = [
|
||||||
...notificationTypes,
|
...notificationProviders,
|
||||||
...storageTypes,
|
...storageProviders,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import {Mail} from "lucide-react";
|
import {Mail} from "lucide-react";
|
||||||
import type { SVGProps } from 'react';
|
import type { SVGProps } from 'react';
|
||||||
|
import {ProviderIconTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const notificationTypes = [
|
|
||||||
|
export const notificationProviders: ProviderIconTypes[] = [
|
||||||
{value: "smtp", label: "Email", icon: Mail},
|
{value: "smtp", label: "Email", icon: Mail},
|
||||||
{value: "slack", label: "Slack", icon: SlackIcon},
|
{value: "slack", label: "Slack", icon: SlackIcon},
|
||||||
{value: "discord", label: "Discord", icon: DiscordIcon},
|
{value: "discord", label: "Discord", icon: DiscordIcon},
|
||||||
@@ -11,6 +13,7 @@ export const notificationTypes = [
|
|||||||
{value: "gotify", label: "Gotify", icon: GotifyIcon},
|
{value: "gotify", label: "Gotify", icon: GotifyIcon},
|
||||||
{value: "ntfy", label: "ntfy.sh", icon: NtfyIcon},
|
{value: "ntfy", label: "ntfy.sh", icon: NtfyIcon},
|
||||||
{value: "webhook", label: "Webhook", icon: WebhookIcon},
|
{value: "webhook", label: "Webhook", icon: WebhookIcon},
|
||||||
|
{value: "microsoft-teams", label: "Microsoft Teams", icon: MSTeamsIcon, preview: true}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -481,3 +484,49 @@ export function WebhookIcon(props: SVGProps<SVGSVGElement>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function MSTeamsIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (<svg xmlns="http://www.w3.org/2000/svg" width={256} height={256} viewBox="0 0 256 239" {...props}>
|
||||||
|
<defs>
|
||||||
|
<path id="logosMicrosoftTeams0"
|
||||||
|
d="M136.93 64.476v12.8a32.674 32.674 0 0 1-5.953-.952a38.698 38.698 0 0 1-26.79-22.742h21.848c6.008.022 10.872 4.887 10.895 10.894Z"
|
||||||
|
opacity=".2"/>
|
||||||
|
</defs>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="logosMicrosoftTeams1" x1="17.372%" x2="82.628%" y1="-6.51%" y2="106.51%">
|
||||||
|
<stop offset="0%" stop-color="#5A62C3"/>
|
||||||
|
<stop offset="50%" stop-color="#4D55BD"/>
|
||||||
|
<stop offset="100%" stop-color="#3940AB"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path fill="#5059C9"
|
||||||
|
d="M178.563 89.302h66.125c6.248 0 11.312 5.065 11.312 11.312v60.231c0 22.96-18.613 41.574-41.573 41.574h-.197c-22.96.003-41.576-18.607-41.579-41.568V95.215a5.912 5.912 0 0 1 5.912-5.913Z"/>
|
||||||
|
<circle cx="223.256" cy="50.605" r="26.791" fill="#5059C9"/>
|
||||||
|
<circle cx="139.907" cy="38.698" r="38.698" fill="#7B83EB"/>
|
||||||
|
<path fill="#7B83EB"
|
||||||
|
d="M191.506 89.302H82.355c-6.173.153-11.056 5.276-10.913 11.449v68.697c-.862 37.044 28.445 67.785 65.488 68.692c37.043-.907 66.35-31.648 65.489-68.692v-68.697c.143-6.173-4.74-11.296-10.913-11.449Z"/>
|
||||||
|
<path
|
||||||
|
d="M142.884 89.302v96.268a10.96 10.96 0 0 1-6.787 10.062c-1.3.55-2.697.833-4.108.833H76.68c-.774-1.965-1.488-3.93-2.084-5.953a72.509 72.509 0 0 1-3.155-21.076v-68.703c-.143-6.163 4.732-11.278 10.895-11.43h60.547Z"
|
||||||
|
opacity=".1"/>
|
||||||
|
<path
|
||||||
|
d="M136.93 89.302v102.222c0 1.411-.283 2.808-.833 4.108a10.96 10.96 0 0 1-10.062 6.787H79.48c-1.012-1.965-1.965-3.93-2.798-5.954a59.049 59.049 0 0 1-2.084-5.953a72.508 72.508 0 0 1-3.155-21.076v-68.703c-.143-6.163 4.732-11.278 10.895-11.43h54.593Z"
|
||||||
|
opacity=".2"/>
|
||||||
|
<path
|
||||||
|
d="M136.93 89.302v90.315c-.045 5.998-4.896 10.85-10.895 10.895H74.597a72.508 72.508 0 0 1-3.155-21.076v-68.703c-.143-6.163 4.732-11.278 10.895-11.43h54.593Z"
|
||||||
|
opacity=".2"/>
|
||||||
|
<path
|
||||||
|
d="M130.977 89.302v90.315c-.046 5.998-4.897 10.85-10.895 10.895H74.597a72.508 72.508 0 0 1-3.155-21.076v-68.703c-.143-6.163 4.732-11.278 10.895-11.43h48.64Z"
|
||||||
|
opacity=".2"/>
|
||||||
|
<path
|
||||||
|
d="M142.884 58.523v18.753c-1.012.06-1.965.12-2.977.12c-1.012 0-1.965-.06-2.977-.12a32.674 32.674 0 0 1-5.953-.952a38.698 38.698 0 0 1-26.791-22.742a33.082 33.082 0 0 1-1.905-5.954h29.708c6.007.023 10.872 4.887 10.895 10.895Z"
|
||||||
|
opacity=".1"/>
|
||||||
|
<use href="#logosMicrosoftTeams0" opacity=".2"/>
|
||||||
|
<use href="#logosMicrosoftTeams0" opacity=".2"/>
|
||||||
|
<path d="M130.977 64.476v11.848a38.698 38.698 0 0 1-26.791-22.743h15.896c6.008.023 10.872 4.888 10.895 10.895Z"
|
||||||
|
opacity=".2"/>
|
||||||
|
<path fill="url(#logosMicrosoftTeams1)"
|
||||||
|
d="M10.913 53.581h109.15c6.028 0 10.914 4.886 10.914 10.913v109.151c0 6.027-4.886 10.913-10.913 10.913H10.913C4.886 184.558 0 179.672 0 173.645V64.495C0 58.466 4.886 53.58 10.913 53.58Z"/>
|
||||||
|
<path fill="#FFF" d="M94.208 95.125h-21.82v59.416H58.487V95.125H36.769V83.599h57.439z"/>
|
||||||
|
</svg>)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import {Server} from "lucide-react";
|
import {Server} from "lucide-react";
|
||||||
import type {SVGProps} from "react";
|
import type {SVGProps} from "react";
|
||||||
|
import {ProviderIconTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||||
|
|
||||||
export const storageTypes = [
|
export const storageProviders: ProviderIconTypes[] = [
|
||||||
{value: "local", label: "Local", icon: Server},
|
{value: "local", label: "Local", icon: Server},
|
||||||
{value: "s3", label: "s3", icon: S3Icon},
|
{value: "s3", label: "s3", icon: S3Icon},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ export function usersListColumns(): ColumnDef<User>[] {
|
|||||||
<TableCell
|
<TableCell
|
||||||
className="text-right text-muted-foreground">{formatLocalizedDate(row.original.createdAt)}</TableCell>
|
className="text-right text-muted-foreground">{formatLocalizedDate(row.original.createdAt)}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{/*{row.original.lastConnectedAt && (*/}
|
{row.original.lastConnectedAt && (
|
||||||
{/* <TableRow>*/}
|
<TableRow>
|
||||||
{/* <TableCell>Last connected</TableCell>*/}
|
<TableCell>Last connected</TableCell>
|
||||||
{/* <TableCell className="text-right text-muted-foreground">*/}
|
<TableCell className="text-right text-muted-foreground">
|
||||||
{/* {formatLocalizedDate(row.original.lastConnectedAt)} ({timeAgo(row.original.lastConnectedAt, locale)})*/}
|
{formatLocalizedDate(row.original.lastConnectedAt)} ({timeAgo(row.original.lastConnectedAt)})
|
||||||
{/* </TableCell>*/}
|
</TableCell>
|
||||||
{/* </TableRow>*/}
|
</TableRow>
|
||||||
{/*)}*/}
|
)}
|
||||||
{row.original.lastChangedPasswordAt && (
|
{row.original.lastChangedPasswordAt && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Last password change</TableCell>
|
<TableCell>Last password change</TableCell>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {Card} from "@/components/ui/card";
|
import {Card} from "@/components/ui/card";
|
||||||
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
|
import {ConnectionIndicator} from "@/components/wrappers/common/connection-indicator";
|
||||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||||
import {AgentWith} from "@/db/schema/08_agent";
|
import {AgentWith} from "@/db/schema/08_agent";
|
||||||
import {Activity, ChevronRight, Copy, Check, Fingerprint, Server, Database, ShieldCheck} from "lucide-react";
|
import {Activity, ChevronRight, Copy, Check, Fingerprint, Server, Database} from "lucide-react";
|
||||||
import {Badge} from "@/components/ui/badge";
|
import {Badge} from "@/components/ui/badge";
|
||||||
import {truncateWords} from "@/utils/text";
|
import {truncateWords} from "@/utils/text";
|
||||||
import {useIsMobile} from "@/hooks/use-mobile";
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
@@ -86,7 +86,7 @@ export const AgentCard = (props: agentCardProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="scale-110">
|
<div className="scale-110">
|
||||||
<ConnectionCircle date={agent.lastContact}/>
|
<ConnectionIndicator date={agent.lastContact}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ChevronRight className="w-5 h-5 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all" />
|
<ChevronRight className="w-5 h-5 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all" />
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ type ProfileModalProps = {
|
|||||||
export const ProfileModal = ({ user, sessions, currentSession, accounts, open, onOpenChange, providers }: ProfileModalProps) => {
|
export const ProfileModal = ({ user, sessions, currentSession, accounts, open, onOpenChange, providers }: ProfileModalProps) => {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="w-[95vw] h-[90vh] max-w-md lg:max-w-[1000px] lg:h-[800px] p-0 overflow-hidden flex flex-col outline-none gap-0 rounded-xl bg-background">
|
<DialogContent className="w-[95vw] h-[90vh] max-w-md lg:max-w-[1000px] lg:h-[800px] pb-6 overflow-hidden flex flex-col outline-none gap-0 rounded-xl bg-background">
|
||||||
<DialogHeader className="sr-only">
|
<DialogHeader className="sr-only">
|
||||||
<DialogTitle>Settings</DialogTitle>
|
<DialogTitle>Settings</DialogTitle>
|
||||||
<DialogDescription>Manage your account settings</DialogDescription>
|
<DialogDescription>Manage your account settings</DialogDescription>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
|||||||
fileInput.onchange = handleImageUpload;
|
fileInput.onchange = handleImageUpload;
|
||||||
fileInput.click();
|
fileInput.click();
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-100 hover:bg-gray-500 hover:bg-opacity-50 rounded-full w-24 h-24 lg:w-32 lg:h-32"
|
className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-30 hover:bg-gray-500 hover:bg-opacity-50 rounded-full w-24 h-24 lg:w-32 lg:h-32"
|
||||||
>
|
>
|
||||||
<UploadIcon className="w-12 h-12 lg:w-16 lg:h-16 text-primary"/>
|
<UploadIcon className="w-12 h-12 lg:w-16 lg:h-16 text-primary"/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Link from "next/link";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {Card} from "@/components/ui/card";
|
import {Card} from "@/components/ui/card";
|
||||||
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
|
import {ConnectionIndicator} from "@/components/wrappers/common/connection-indicator";
|
||||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||||
import {Database} from "@/db/schema/07_database";
|
import {Database} from "@/db/schema/07_database";
|
||||||
import {ChevronRight, Activity, Fingerprint, Copy, Check} from "lucide-react";
|
import {ChevronRight, Activity, Fingerprint, Copy, Check} from "lucide-react";
|
||||||
@@ -60,7 +60,7 @@ export const DatabaseCard = (props: databaseCardProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end gap-3">
|
<div className="flex flex-col items-end gap-3">
|
||||||
<div className="scale-100 origin-right">
|
<div className="scale-100 origin-right">
|
||||||
<ConnectionCircle date={database.lastContact}/>
|
<ConnectionIndicator date={database.lastContact}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user" ADD COLUMN "lastConnectedAt" timestamp;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user" ADD COLUMN "lastConnectedAt" timestamp;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TYPE "public"."dbms_status" ADD VALUE 'mongodb';
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -204,6 +204,20 @@
|
|||||||
"when": 1768733221276,
|
"when": 1768733221276,
|
||||||
"tag": "0028_graceful_ben_parker",
|
"tag": "0028_graceful_ben_parker",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 29,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1768743644834,
|
||||||
|
"tag": "0029_lowly_white_tiger",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 30,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1768763883339,
|
||||||
|
"tag": "0030_dizzy_morlocks",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ import {boolean, integer, pgEnum, pgTable, text, timestamp, uuid} from "drizzle-
|
|||||||
import {createSelectSchema} from "drizzle-zod";
|
import {createSelectSchema} from "drizzle-zod";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {project} from "./06_project";
|
import {project} from "./06_project";
|
||||||
import {member, OrganizationMember} from "@/db/schema/04_member";
|
import {member} from "@/db/schema/04_member";
|
||||||
import {invitation} from "@/db/schema/05_invitation";
|
import {invitation} from "@/db/schema/05_invitation";
|
||||||
import {organization} from "@/db/schema/03_organization";
|
import {organization} from "@/db/schema/03_organization";
|
||||||
import {Account as BetterAuthAccount} from "better-auth";
|
import {Account as BetterAuthAccount} from "better-auth";
|
||||||
@@ -23,6 +23,7 @@ export const user = pgTable("user", {
|
|||||||
banned: boolean("banned"),
|
banned: boolean("banned"),
|
||||||
banReason: text("ban_reason"),
|
banReason: text("ban_reason"),
|
||||||
banExpires: timestamp("ban_expires"),
|
banExpires: timestamp("ban_expires"),
|
||||||
|
lastConnectedAt: timestamp(),
|
||||||
lastChangedPasswordAt: timestamp(),
|
lastChangedPasswordAt: timestamp(),
|
||||||
twoFactorEnabled: boolean("two_factor_enabled").default(false),
|
twoFactorEnabled: boolean("two_factor_enabled").default(false),
|
||||||
...timestamps
|
...timestamps
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { pgEnum } from "drizzle-orm/pg-core";
|
import {pgEnum} from "drizzle-orm/pg-core";
|
||||||
import { createSelectSchema } from "drizzle-zod";
|
import {createSelectSchema} from "drizzle-zod";
|
||||||
import { z } from "zod";
|
import {z} from "zod";
|
||||||
|
|
||||||
export const dbmsEnum = pgEnum("dbms_status", ["postgresql", "mysql"]);
|
export const dbmsEnum = pgEnum("dbms_status", ["postgresql", "mysql", "mongodb"]);
|
||||||
export const statusEnum = pgEnum("status", ["waiting", "ongoing", "failed", "success"]);
|
export const statusEnum = pgEnum("status", ["waiting", "ongoing", "failed", "success"]);
|
||||||
export const typeStorageEnum = pgEnum("type_storage", ["local", "s3"]);
|
export const typeStorageEnum = pgEnum("type_storage", ["local", "s3"]);
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ export const retentionCleanTask = async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
console.log(`Retention databases number: ${databases.length}`);
|
||||||
for (const db of databases) {
|
for (const db of databases) {
|
||||||
if (!db.retentionPolicy) continue; // no policy = skip
|
if (!db.retentionPolicy) continue;
|
||||||
await enforceRetention(db.id, db.retentionPolicy);
|
await enforceRetention(db.id, db.retentionPolicy);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -33,7 +33,7 @@ export async function enforceRetention(
|
|||||||
databaseId: string,
|
databaseId: string,
|
||||||
policy: typeof retentionPolicy.$inferSelect
|
policy: typeof retentionPolicy.$inferSelect
|
||||||
) {
|
) {
|
||||||
|
console.log(`Retention started for ${databaseId}`);
|
||||||
switch (policy.type) {
|
switch (policy.type) {
|
||||||
case "count":
|
case "count":
|
||||||
await enforceRetentionCount(databaseId, policy.count ?? 7);
|
await enforceRetentionCount(databaseId, policy.count ?? 7);
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import * as drizzleDb from "@/db";
|
|||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {and, desc, eq, isNull} from "drizzle-orm";
|
import {and, desc, eq, isNull} from "drizzle-orm";
|
||||||
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
|
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
|
||||||
|
import {toast} from "sonner";
|
||||||
|
|
||||||
export async function enforceRetentionCount(databaseId: string, count: number) {
|
export async function enforceRetentionCount(databaseId: string, count: number) {
|
||||||
|
console.log(`[Retention Count] - ${databaseId} : started`);
|
||||||
const backups = await db.query.backup.findMany({
|
const backups = await db.query.backup.findMany({
|
||||||
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseId), isNull(drizzleDb.schemas.backup.deletedAt)),
|
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseId), isNull(drizzleDb.schemas.backup.deletedAt)),
|
||||||
orderBy: desc(drizzleDb.schemas.backup.createdAt),
|
orderBy: desc(drizzleDb.schemas.backup.createdAt),
|
||||||
@@ -16,14 +18,20 @@ export async function enforceRetentionCount(databaseId: string, count: number) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const toDelete = backups.slice(count); // keep first `count`, delete rest
|
const toDelete = backups.slice(count);
|
||||||
|
console.log(`[Retention Count] - ${databaseId} : ${toDelete.length} backups to delete`);
|
||||||
|
|
||||||
for (const b of toDelete) {
|
for (const b of toDelete) {
|
||||||
await deleteBackupCronAction({
|
const result = await deleteBackupCronAction({
|
||||||
backupId: b.id,
|
backupId: b.id,
|
||||||
databaseId: b.databaseId,
|
databaseId: b.databaseId,
|
||||||
file: b.file ?? "",
|
|
||||||
projectSlug: b.database.project?.slug!
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const inner = result?.data;
|
||||||
|
if (inner?.success) {
|
||||||
|
console.log(`[Retention Count] - (databaseId:${b.databaseId}) - (backupId: ${b.id}) : successfully deleted`);
|
||||||
|
} else {
|
||||||
|
console.log(`[Retention Count] - (databaseId:${b.databaseId}) - (backupId: ${b.id}) : an error occurred - ${inner?.actionError?.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import * as drizzleDb from "@/db";
|
|||||||
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
|
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
|
||||||
|
|
||||||
export async function enforceRetentionDays(databaseId: string, days: number) {
|
export async function enforceRetentionDays(databaseId: string, days: number) {
|
||||||
|
console.log(`Enforce Retention Days starting for ${databaseId}`);
|
||||||
const cutoff = new Date(Date.now() - days * 86400000);
|
const cutoff = new Date(Date.now() - days * 86400000);
|
||||||
|
|
||||||
const expiredBackups = await db.query.backup.findMany({
|
const expiredBackups = await db.query.backup.findMany({
|
||||||
@@ -22,13 +23,18 @@ export async function enforceRetentionDays(databaseId: string, days: number) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const backup of expiredBackups) {
|
for (const backup of expiredBackups) {
|
||||||
await deleteBackupCronAction({
|
|
||||||
|
const result = await deleteBackupCronAction({
|
||||||
backupId: backup.id,
|
backupId: backup.id,
|
||||||
databaseId: backup.databaseId,
|
databaseId: backup.databaseId,
|
||||||
file: backup.file ?? "",
|
|
||||||
projectSlug: backup.database.project?.slug!
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const inner = result?.data;
|
||||||
|
if (inner?.success) {
|
||||||
|
console.log(`[Retention Days] - (databaseId:${backup.databaseId}) - (backupId: ${backup.id}) : successfully deleted`);
|
||||||
|
} else {
|
||||||
|
console.log(`[Retention Days] - (databaseId:${backup.databaseId}) - (backupId: ${backup.id}) : an error occurred - ${inner?.actionError?.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export async function enforceRetentionGFS(databaseId: string, gfsSettings: {
|
|||||||
monthly: number;
|
monthly: number;
|
||||||
yearly: number;
|
yearly: number;
|
||||||
}) {
|
}) {
|
||||||
|
console.log(`Enforce Retention GFS starting for ${databaseId}`);
|
||||||
|
|
||||||
const backups = await db.query.backup.findMany({
|
const backups = await db.query.backup.findMany({
|
||||||
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseId), isNull(drizzleDb.schemas.backup.deletedAt)),
|
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseId), isNull(drizzleDb.schemas.backup.deletedAt)),
|
||||||
orderBy: desc(drizzleDb.schemas.backup.createdAt),
|
orderBy: desc(drizzleDb.schemas.backup.createdAt),
|
||||||
@@ -30,15 +32,7 @@ export async function enforceRetentionGFS(databaseId: string, gfsSettings: {
|
|||||||
if (b.createdAt >= subDays(now, gfsSettings.daily)) toKeep.add(b.id);
|
if (b.createdAt >= subDays(now, gfsSettings.daily)) toKeep.add(b.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
// WEEKLY
|
const weekStartDates = Array.from({length: gfsSettings.weekly}, (_, i) => startOfWeek(subWeeks(now, i), {weekStartsOn: 1}));
|
||||||
// const weekStartDates = Array.from({length: gfsSettings.weekly}, (_, i) => startOfWeek(subWeeks(now, i)));
|
|
||||||
// weekStartDates.forEach((weekStart) => {
|
|
||||||
// const backupOfWeek = backups.find(
|
|
||||||
// (b) => b.createdAt >= weekStart && b.createdAt < subWeeks(weekStart, -1)
|
|
||||||
// );
|
|
||||||
// if (backupOfWeek) toKeep.add(backupOfWeek.id);
|
|
||||||
// });
|
|
||||||
const weekStartDates = Array.from({length: gfsSettings.weekly}, (_, i) => startOfWeek(subWeeks(now, i), { weekStartsOn: 1 }));
|
|
||||||
weekStartDates.forEach((weekStart) => {
|
weekStartDates.forEach((weekStart) => {
|
||||||
const backupOfWeek = backups.find(
|
const backupOfWeek = backups.find(
|
||||||
(b) => b.createdAt >= weekStart && b.createdAt < subWeeks(weekStart, -1)
|
(b) => b.createdAt >= weekStart && b.createdAt < subWeeks(weekStart, -1)
|
||||||
@@ -46,9 +40,6 @@ export async function enforceRetentionGFS(databaseId: string, gfsSettings: {
|
|||||||
if (backupOfWeek) toKeep.add(backupOfWeek.id);
|
if (backupOfWeek) toKeep.add(backupOfWeek.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// MONTHLY
|
// MONTHLY
|
||||||
const monthStartDates = Array.from({length: gfsSettings.monthly}, (_, i) => startOfMonth(subMonths(now, i)));
|
const monthStartDates = Array.from({length: gfsSettings.monthly}, (_, i) => startOfMonth(subMonths(now, i)));
|
||||||
monthStartDates.forEach((monthStart) => {
|
monthStartDates.forEach((monthStart) => {
|
||||||
@@ -71,12 +62,17 @@ export async function enforceRetentionGFS(databaseId: string, gfsSettings: {
|
|||||||
for (const b of backups) {
|
for (const b of backups) {
|
||||||
if (!toKeep.has(b.id)) {
|
if (!toKeep.has(b.id)) {
|
||||||
|
|
||||||
await deleteBackupCronAction({
|
const result = await deleteBackupCronAction({
|
||||||
backupId: b.id,
|
backupId: b.id,
|
||||||
databaseId: b.databaseId,
|
databaseId: b.databaseId,
|
||||||
file: b.file ?? "",
|
|
||||||
projectSlug: b.database.project?.slug!
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const inner = result?.data;
|
||||||
|
if (inner?.success) {
|
||||||
|
console.log(`[Retention GFS] - (databaseId:${b.databaseId}) - (backupId: ${b.id}) : successfully deleted`);
|
||||||
|
} else {
|
||||||
|
console.log(`[Retention GFS] - (databaseId:${b.databaseId}) - (backupId: ${b.id}) : an error occurred - ${inner?.actionError?.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,17 +16,16 @@ export const deleteBackupCronAction = action
|
|||||||
z.object({
|
z.object({
|
||||||
backupId: z.string(),
|
backupId: z.string(),
|
||||||
databaseId: z.string(),
|
databaseId: z.string(),
|
||||||
projectSlug: z.string(),
|
|
||||||
file: z.string(),
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.action(async ({parsedInput}): Promise<ServerActionResult<Backup>> => {
|
.action(async ({parsedInput}): Promise<ServerActionResult<Backup>> => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const backup = await db.query.backup.findFirst({
|
const backup = await db.query.backup.findFirst({
|
||||||
where: and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.backupId))
|
where: and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (!backup) {
|
if (!backup) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -44,17 +43,6 @@ export const deleteBackupCronAction = action
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (!backupStorages) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
actionError: {
|
|
||||||
message: "Backup storage not found.",
|
|
||||||
status: 404,
|
|
||||||
messageParams: {backupId: parsedInput.backupId},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const backupStorage of backupStorages) {
|
for (const backupStorage of backupStorages) {
|
||||||
|
|
||||||
await db
|
await db
|
||||||
@@ -83,7 +71,7 @@ export const deleteBackupCronAction = action
|
|||||||
.set(withUpdatedAt({
|
.set(withUpdatedAt({
|
||||||
deletedAt: new Date(),
|
deletedAt: new Date(),
|
||||||
}))
|
}))
|
||||||
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.backupId)))
|
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user