mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6952ac5f48 | ||
|
|
45acc781f2 | ||
|
|
9c2359e87b | ||
|
|
8dcb406790 | ||
|
|
a3d0a6e1df | ||
|
|
8edf0ca742 | ||
|
|
568d9612c5 | ||
|
|
6548945b2f | ||
|
|
4b1d0aba74 | ||
|
|
c726346258 | ||
|
|
0677f14ac5 | ||
|
|
d346f5cdc4 | ||
|
|
1ce08448ec | ||
|
|
77fb1095f1 | ||
|
|
29dd597c7a | ||
|
|
7943f3fd4b | ||
|
|
a63c29b943 | ||
|
|
f4d72090a3 | ||
|
|
8b379067a1 | ||
|
|
896b65f2a5 | ||
|
|
79d725cd12 | ||
|
|
dfa5b42467 | ||
|
|
e6b63a4570 | ||
|
|
eb13fce8e9 | ||
|
|
f70d86f394 | ||
|
|
8f44071069 | ||
|
|
79832f0d0b | ||
|
|
f89b8d3ac3 | ||
|
|
40247efddd | ||
|
|
40d3228db6 | ||
|
|
383564f988 | ||
|
|
fb2f7791fd |
@@ -81,3 +81,6 @@ TRUSTED_DOMAINS="http://localhost:8887, http://localhost:3055, http://localhost:
|
||||
|
||||
# Default to false
|
||||
#TUSD_BEHIND_PROXY=true
|
||||
|
||||
#Skip the onboarding on fresh instance
|
||||
#SKIP_ONBOARDING=true
|
||||
|
||||
+1
-5
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"plugin:tailwindcss/recommended"
|
||||
],
|
||||
"extends": ["next/core-web-vitals", "plugin:tailwindcss/recommended"],
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react-hooks/rules-of-hooks": "off"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
tags: ${{ steps.prep.outputs.image }}
|
||||
target: ${{ inputs.target }}
|
||||
cache-from: type=gha,scope=build-${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }},ignore-error=true
|
||||
|
||||
- name: Save image name for manifest
|
||||
run: echo "${{ steps.prep.outputs.image }}" > image.txt
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
gcs-data/
|
||||
gcs-storage/
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
@@ -28,6 +30,8 @@ yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
**/.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@@ -60,3 +64,4 @@ certificates
|
||||
.codex
|
||||
/docs
|
||||
.worktrees
|
||||
.superpowers
|
||||
+1
-1
@@ -33,5 +33,5 @@ keywords:
|
||||
- web-ui
|
||||
- agent
|
||||
license: Apache-2.0
|
||||
version: 1.20.1
|
||||
version: 1.21.3
|
||||
date-released: '2026-03-02'
|
||||
|
||||
@@ -78,6 +78,7 @@ You have 5 ways to install Portabase:
|
||||
| **Valkey** | ✅ Stable | 7.2+ | No |
|
||||
| **Firebird** | ✅ Stable | 3.0, 4.0, 5.0 | Yes |
|
||||
| **MSSQL Server** | ✅ Stable | 2017, 2019, 2022, Azure SQL | Yes |
|
||||
| **Docker Volume** | ✅ Stable | Docker Engine 20.10+ | Yes |
|
||||
|
||||
See the [Database Servers documentation](https://portabase.io/docs/agent/db) for version-specific backup and restore details.
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { CardContent, CardHeader } from "@/components/ui/card";
|
||||
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { ForgotPasswordForm } from "@/features/auth/forgot-password-form";
|
||||
import { ForgotPasswordForm } from "@/features/auth/components/forgot-password-form";
|
||||
import { env } from "@/env.mjs";
|
||||
import { CardAuth } from "@/features/layout/card-auth";
|
||||
import { CardAuth } from "@/features/layout/components/card-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function RoutePage(props: { searchParams: Promise<{ callbackUrl: string | undefined }> }) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {CardContent, CardHeader} from "@/components/ui/card";
|
||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import {GuardForm} from "@/features/auth/guard-form";
|
||||
import {GuardForm} from "@/features/auth/components/guard-form";
|
||||
import {cookies} from "next/headers";
|
||||
import {redirect} from "next/navigation";
|
||||
import {CardAuth} from "@/features/layout/card-auth";
|
||||
import {CardAuth} from "@/features/layout/components/card-auth";
|
||||
|
||||
export default async function GuardPage() {
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from "react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
import { AuthLogoSection } from "@/features/auth/auth-logo-section";
|
||||
import { isOnboardingDone } from "@/db/services/setting";
|
||||
import { AuthLogoSection } from "@/features/auth/components/auth-logo-section";
|
||||
import { env } from "@/env.mjs";
|
||||
import { Heart } from "lucide-react";
|
||||
|
||||
export default async function Layout({
|
||||
@@ -9,6 +11,10 @@ export default async function Layout({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
if (env.SKIP_ONBOARDING !== "true" && !(await isOnboardingDone())) {
|
||||
redirect("/welcome");
|
||||
}
|
||||
|
||||
const user = await currentUser();
|
||||
|
||||
if (user && !user.banned && user.role !== "pending") {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { LoginForm } from "@/features/auth/login-form";
|
||||
import { LoginForm } from "@/features/auth/components/login-form";
|
||||
import { Metadata } from "next";
|
||||
import { SUPPORTED_PROVIDERS } from "@/lib/auth/config";
|
||||
import { SocialAuthButtons } from "@/features/auth/social-buttons";
|
||||
import { SocialAuthButtons } from "@/features/auth/components/social-buttons";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { CardContent, CardHeader } from "@/components/ui/card";
|
||||
import Link from "next/link";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { CardAuth } from "@/features/layout/card-auth";
|
||||
import { CardAuth } from "@/features/layout/components/card-auth";
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PageParams } from "@/types/next";
|
||||
import { RegisterForm } from "@/features/auth/register-form";
|
||||
import { RegisterForm } from "@/features/auth/components/register-form";
|
||||
import { Metadata } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { ResetPasswordForm } from "@/features/auth/reset-password-form";
|
||||
import { ResetPasswordForm } from "@/features/auth/components/reset-password-form";
|
||||
import { redirect } from "next/navigation";
|
||||
import { auth } from "@/lib/auth/auth";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
|
||||
import { CardAuth } from "@/features/layout/card-auth";
|
||||
import { CardAuth } from "@/features/layout/components/card-auth";
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) {
|
||||
|
||||
@@ -3,8 +3,8 @@ import {eq} from "drizzle-orm";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page} from "@/features/layout/page";
|
||||
import {OrganizationManagement} from "@/features/organizations/admin-organization-management";
|
||||
import {Page} from "@/features/layout/components/page";
|
||||
import {OrganizationManagement} from "@/features/organizations/components/admin-organization-management";
|
||||
import {buildOrganizationWithMembers} from "@/utils/common";
|
||||
import {isUUID} from "@/utils/text";
|
||||
import {user} from "@/db/schema/02_user";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||
import {db} from "@/db";
|
||||
import {
|
||||
AdminOrganizationAddModal
|
||||
} from "@/features/organizations/admin-organization-add-modal";
|
||||
import {AdminOrganizationList} from "@/features/organizations/admin-organization-list";
|
||||
} from "@/features/organizations/components/admin-organization-add-modal";
|
||||
import {AdminOrganizationList} from "@/features/organizations/components/admin-organization-list";
|
||||
import {isNull} from "drizzle-orm";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||
import {db} from "@/db";
|
||||
import {logger} from "@/lib/logger";
|
||||
import {notFound} from "next/navigation";
|
||||
import {SettingsTabs} from "@/features/settings/settings-tabs";
|
||||
import {SettingsTabs} from "@/features/settings/components/settings-tabs";
|
||||
import {desc, isNull} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||
import {db} from "@/db";
|
||||
import {desc, isNull} from "drizzle-orm";
|
||||
import {AdminUserList} from "@/features/users/admin-user-list";
|
||||
import {AdminUserAddModal} from "@/features/users/admin-user-add-modal";
|
||||
import {AdminUserList} from "@/features/users/components/admin-user-list";
|
||||
import {AdminUserAddModal} from "@/features/users/components/admin-user-add-modal";
|
||||
import {SUPPORTED_PROVIDERS} from "@/lib/auth/config";
|
||||
import {getSettings} from "@/db/services/setting";
|
||||
import {resolveAvatarUrl} from "@/utils/resolve-avatar-url";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
const users = await db.query.user.findMany({
|
||||
where: (fields) => isNull(fields.deletedAt),
|
||||
with: {
|
||||
accounts: true
|
||||
},
|
||||
orderBy: (fields) => desc(fields.createdAt),
|
||||
const [settings, users] = await Promise.all([
|
||||
getSettings(),
|
||||
db.query.user.findMany({
|
||||
where: (fields) => isNull(fields.deletedAt),
|
||||
with: { accounts: true },
|
||||
orderBy: (fields) => desc(fields.createdAt),
|
||||
}),
|
||||
]);
|
||||
|
||||
const avatarUrls = Object.fromEntries(
|
||||
users.map((u) => [u.id, resolveAvatarUrl(u, settings)])
|
||||
);
|
||||
|
||||
});
|
||||
const organizations = await db.query.organization.findMany({
|
||||
with: {
|
||||
members: true,
|
||||
@@ -38,7 +45,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
</div>
|
||||
</PageHeader>
|
||||
<PageContent className="flex flex-col gap-5">
|
||||
<AdminUserList users={users} isPasswordAuthEnabled={isPasswordAuthEnabled}/>
|
||||
<AdminUserList users={users} isPasswordAuthEnabled={isPasswordAuthEnabled} avatarUrls={avatarUrls}/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -4,17 +4,17 @@ import {
|
||||
PageContent,
|
||||
PageDescription,
|
||||
PageTitle,
|
||||
} from "@/features/layout/page";
|
||||
} from "@/features/layout/components/page";
|
||||
import { db } from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {eq, isNull} from "drizzle-orm";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ButtonDeleteAgent } from "@/features/agents/agent-delete-button";
|
||||
import { ButtonDeleteAgent } from "@/features/agents/components/agent-delete-button";
|
||||
import { capitalizeFirstLetter } from "@/utils/text";
|
||||
import { generateEdgeKey } from "@/utils/edge_key";
|
||||
import { getServerUrl } from "@/utils/get-server-url";
|
||||
import { AgentContentPage } from "@/features/agents/agent-content";
|
||||
import { AgentDialog } from "@/features/agents/agent-dialog";
|
||||
import { AgentContentPage } from "@/features/agents/components/agent-content";
|
||||
import { AgentDialog } from "@/features/agents/components/agent-dialog";
|
||||
|
||||
|
||||
export default async function RoutePage(
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {AgentCard} from "@/features/agents/agent-card";
|
||||
import {AgentCard} from "@/features/agents/components/agent-card";
|
||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||
import {notFound} from "next/navigation";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {and, desc, eq, isNull, not} from "drizzle-orm";
|
||||
import {Metadata} from "next";
|
||||
import {AgentDialog} from "@/features/agents/agent-dialog";
|
||||
import {AgentDialog} from "@/features/agents/components/agent-dialog";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Agents",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||
import {Metadata} from "next";
|
||||
import {db} from "@/db";
|
||||
import {notificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {desc, isNull} from "drizzle-orm";
|
||||
import {ChannelsSection} from "@/features/channel/channels-section";
|
||||
import {ChannelAddEditModal} from "@/features/channel/channel-add-edit-modal";
|
||||
import {ChannelsSection} from "@/features/channel/components/channels-section";
|
||||
import {ChannelAddEditModal} from "@/features/channel/components/channel-add-edit-modal";
|
||||
import * as drizzleDb from "@/db";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||
import {Metadata} from "next";
|
||||
import {NotificationLogsList} from "@/features/notifications/notification-logs-list";
|
||||
import {NotificationLogsList} from "@/features/notifications/components/notification-logs-list";
|
||||
import {notFound} from "next/navigation";
|
||||
import {getNotificationHistory} from "@/db/services/notification-log";
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||
import {Metadata} from "next";
|
||||
import {ChannelsSection} from "@/features/channel/channels-section";
|
||||
import {ChannelsSection} from "@/features/channel/components/channels-section";
|
||||
import {db} from "@/db";
|
||||
import {desc, eq, isNull} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||
import {ChannelAddEditModal} from "@/features/channel/channel-add-edit-modal";
|
||||
import {ChannelAddEditModal} from "@/features/channel/components/channel-add-edit-modal";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Storage Channels",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||
import {notFound} from "next/navigation";
|
||||
import {getOrganization} from "@/lib/auth/auth";
|
||||
import {Metadata} from "next";
|
||||
import {db} from "@/db";
|
||||
import {MigrationTool} from "@/features/migration/migration-tool";
|
||||
import {MigrationTool} from "@/features/migration/components/migration-tool";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Projects",
|
||||
|
||||
+118
-103
@@ -1,122 +1,137 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {notFound, redirect} from "next/navigation";
|
||||
import {Page} from "@/features/layout/page";
|
||||
import {db} from "@/db";
|
||||
import {eq, and, inArray} from "drizzle-orm";
|
||||
import { PageParams } from "@/types/next";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { Page } from "@/features/layout/components/page";
|
||||
import { db } from "@/db";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {getOrganizationProjectDatabases} from "@/db/services/project";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import {BackupModalProvider} from "@/features/database/backup-modal-context";
|
||||
import {DatabaseContent} from "@/features/database/database-content";
|
||||
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
|
||||
import {LogsModalProvider} from "@/features/logs/logs-modal-context";
|
||||
import { getOrganizationProjectDatabases } from "@/db/services/project";
|
||||
import { getActiveMember, getOrganization } from "@/lib/auth/auth";
|
||||
import { BackupModalProvider } from "@/features/database/components/backup-modal-context";
|
||||
import { DatabaseContent } from "@/features/database/components/database-content";
|
||||
import { getHealthLast12hLogs } from "@/db/services/healthcheck";
|
||||
import { LogsModalProvider } from "@/features/logs/components/logs-modal-context";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{
|
||||
export default async function RoutePage(
|
||||
props: PageParams<{
|
||||
projectId: string;
|
||||
databaseId: string
|
||||
}>) {
|
||||
const {projectId, databaseId} = await props.params;
|
||||
databaseId: string;
|
||||
}>,
|
||||
) {
|
||||
const { projectId, databaseId } = await props.params;
|
||||
|
||||
const organization = await getOrganization({});
|
||||
const activeMember = await getActiveMember()
|
||||
const organization = await getOrganization({});
|
||||
const activeMember = await getActiveMember();
|
||||
|
||||
if (!organization || !activeMember) {
|
||||
notFound();
|
||||
}
|
||||
if (!organization || !activeMember) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const databasesProject = await getOrganizationProjectDatabases({
|
||||
organizationSlug: organization.slug,
|
||||
projectId: projectId
|
||||
})
|
||||
const databasesProject = await getOrganizationProjectDatabases({
|
||||
organizationSlug: organization.slug,
|
||||
projectId: projectId,
|
||||
});
|
||||
|
||||
const dbItem = await db.query.database.findFirst({
|
||||
where: and(inArray(drizzleDb.schemas.backup.id, databasesProject.ids ?? []), eq(drizzleDb.schemas.database.id, databaseId), eq(drizzleDb.schemas.database.projectId, projectId)),
|
||||
const dbItem = await db.query.database.findFirst({
|
||||
where: and(
|
||||
inArray(drizzleDb.schemas.backup.id, databasesProject.ids ?? []),
|
||||
eq(drizzleDb.schemas.database.id, databaseId),
|
||||
eq(drizzleDb.schemas.database.projectId, projectId),
|
||||
),
|
||||
with: {
|
||||
project: true,
|
||||
retentionPolicy: true,
|
||||
alertPolicies: true,
|
||||
storagePolicies: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dbItem) {
|
||||
redirect("/dashboard/projects");
|
||||
}
|
||||
|
||||
const backups = await db.query.backup.findMany({
|
||||
where: eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
|
||||
with: {
|
||||
restorations: true,
|
||||
storages: {
|
||||
with: {
|
||||
project: true,
|
||||
retentionPolicy: true,
|
||||
alertPolicies: true,
|
||||
storagePolicies: true,
|
||||
}
|
||||
});
|
||||
|
||||
if (!dbItem) {
|
||||
redirect("/dashboard/projects");
|
||||
}
|
||||
|
||||
const backups = await db.query.backup.findMany({
|
||||
where: eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
|
||||
with: {
|
||||
restorations: true,
|
||||
storages: {
|
||||
with: {
|
||||
storageChannel: true
|
||||
}
|
||||
},
|
||||
logs: true
|
||||
storageChannel: true,
|
||||
},
|
||||
orderBy: (b, {desc}) => [desc(b.createdAt)],
|
||||
});
|
||||
},
|
||||
logs: true,
|
||||
},
|
||||
orderBy: (b, { desc }) => [desc(b.createdAt)],
|
||||
});
|
||||
|
||||
const restorations = await db.query.restoration.findMany({
|
||||
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
|
||||
with: {
|
||||
logs: true
|
||||
},
|
||||
orderBy: (r, {desc}) => [desc(r.createdAt)],
|
||||
});
|
||||
const restorations = await db.query.restoration.findMany({
|
||||
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
|
||||
with: {
|
||||
logs: true,
|
||||
},
|
||||
orderBy: (r, { desc }) => [desc(r.createdAt)],
|
||||
});
|
||||
|
||||
const isAlreadyBackup = backups.some((b) => b.status === "waiting" || b.status === "ongoing");
|
||||
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
||||
//const isAlreadyBackup = backups.some((b) => b.status === "waiting" || b.status === "ongoing");
|
||||
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
||||
|
||||
const totalBackups = await db.select({count: drizzleDb.schemas.backup.id})
|
||||
.from(drizzleDb.schemas.backup)
|
||||
.where(eq(drizzleDb.schemas.backup.databaseId, dbItem.id))
|
||||
.then(rows => rows.length);
|
||||
const totalBackups = await db
|
||||
.select({ count: drizzleDb.schemas.backup.id })
|
||||
.from(drizzleDb.schemas.backup)
|
||||
.where(eq(drizzleDb.schemas.backup.databaseId, dbItem.id))
|
||||
.then((rows) => rows.length);
|
||||
|
||||
const availableBackups = backups.filter(b => !b.deletedAt).length;
|
||||
const availableBackups = backups.filter((b) => !b.deletedAt).length;
|
||||
|
||||
const successfulBackups = await db.select({count: drizzleDb.schemas.backup.id})
|
||||
.from(drizzleDb.schemas.backup)
|
||||
.where(and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
|
||||
eq(drizzleDb.schemas.backup.status, "success")
|
||||
))
|
||||
.then(rows => rows.length);
|
||||
const successfulBackups = await db
|
||||
.select({ count: drizzleDb.schemas.backup.id })
|
||||
.from(drizzleDb.schemas.backup)
|
||||
.where(
|
||||
and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
|
||||
eq(drizzleDb.schemas.backup.status, "success"),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows.length);
|
||||
|
||||
const [settings] = await db
|
||||
.select()
|
||||
.from(drizzleDb.schemas.setting)
|
||||
.where(eq(drizzleDb.schemas.setting.name, "system"))
|
||||
.limit(1);
|
||||
if (!settings) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||
if (!settings) {
|
||||
notFound();
|
||||
}
|
||||
const databaseHealthLogs = dbItem
|
||||
? await getHealthLast12hLogs({ id: dbItem.id })
|
||||
: [];
|
||||
|
||||
const databaseHealthLogs = dbItem ? await getHealthLast12hLogs({id: dbItem.id}) : []
|
||||
const successRate =
|
||||
totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||
|
||||
//const isMember = activeMember?.role === "member";
|
||||
|
||||
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||
|
||||
const isMember = activeMember?.role === "member";
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<LogsModalProvider>
|
||||
<BackupModalProvider>
|
||||
<DatabaseContent
|
||||
activeMember={activeMember}
|
||||
settings={settings}
|
||||
database={dbItem}
|
||||
databaseHealthLogs={databaseHealthLogs}
|
||||
isAlreadyRestore={isAlreadyRestore}
|
||||
restorations={restorations}
|
||||
backups={backups}
|
||||
totalBackups={totalBackups}
|
||||
availableBackups={availableBackups}
|
||||
successRate={successRate}
|
||||
organizationId={organization.id}
|
||||
activeOrganizationChannels={[]}
|
||||
activeOrganizationStorageChannels={[]}
|
||||
/>
|
||||
</BackupModalProvider>
|
||||
</LogsModalProvider>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Page>
|
||||
<LogsModalProvider>
|
||||
<BackupModalProvider>
|
||||
<DatabaseContent
|
||||
activeMember={activeMember}
|
||||
settings={settings}
|
||||
database={dbItem}
|
||||
databaseHealthLogs={databaseHealthLogs}
|
||||
isAlreadyRestore={isAlreadyRestore}
|
||||
restorations={restorations}
|
||||
backups={backups}
|
||||
totalBackups={totalBackups}
|
||||
availableBackups={availableBackups}
|
||||
successRate={successRate}
|
||||
organizationId={organization.id}
|
||||
activeOrganizationChannels={[]}
|
||||
activeOrganizationStorageChannels={[]}
|
||||
/>
|
||||
</BackupModalProvider>
|
||||
</LogsModalProvider>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,108 +1,115 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageContent, PageTitle} from "@/features/layout/page";
|
||||
import {
|
||||
ButtonDeleteProject
|
||||
} from "@/features/projects/project-delete-button";
|
||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
||||
import {ProjectDatabaseCard} from "@/features/projects/project-database-card";
|
||||
import {notFound, redirect} from "next/navigation";
|
||||
import {db} from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import { PageParams } from "@/types/next";
|
||||
import { Page, PageContent, PageTitle } from "@/features/layout/components/page";
|
||||
import { ButtonDeleteProject } from "@/features/projects/components/project-delete-button";
|
||||
import { CardsWithPagination } from "@/components/common/cards-with-pagination";
|
||||
import { ProjectDatabaseCard } from "@/features/projects/components/project-database-card";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { db } from "@/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getActiveMember, getOrganization } from "@/lib/auth/auth";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
import {ProjectDialog} from "@/features/projects/project-dialog";
|
||||
import {ProjectWith} from "@/db/schema/06_project";
|
||||
import {isUuidv4} from "@/utils/verify-uuid";
|
||||
import {getOrganizationAvailableDatabases} from "@/db/services/database";
|
||||
import { capitalizeFirstLetter, isUUID } from "@/utils/text";
|
||||
import { ProjectDialog } from "@/features/projects/components/project-dialog";
|
||||
import { ProjectWith } from "@/db/schema/06_project";
|
||||
import { getOrganizationAvailableDatabases } from "@/db/services/database";
|
||||
|
||||
export default async function RoutePage(
|
||||
props: PageParams<{
|
||||
projectId: string;
|
||||
}>,
|
||||
) {
|
||||
const { projectId } = await props.params;
|
||||
|
||||
export default async function RoutePage(props: PageParams<{
|
||||
projectId: string
|
||||
}>) {
|
||||
const {
|
||||
projectId
|
||||
} = await props.params;
|
||||
if (!isUUID(projectId)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (!isUuidv4(projectId)) {
|
||||
notFound()
|
||||
}
|
||||
const organization = await getOrganization({});
|
||||
const activeMember = await getActiveMember();
|
||||
|
||||
const organization = await getOrganization({});
|
||||
const activeMember = await getActiveMember()
|
||||
if (!organization) {
|
||||
notFound();
|
||||
}
|
||||
const org = await db.query.organization.findFirst({
|
||||
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
notFound();
|
||||
}
|
||||
const org = await db.query.organization.findFirst({
|
||||
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
|
||||
});
|
||||
if (!org) notFound();
|
||||
|
||||
if (!org) notFound();
|
||||
const proj = await db.query.project.findFirst({
|
||||
where: (proj, { and, eq, not }) =>
|
||||
and(
|
||||
eq(proj.id, projectId),
|
||||
eq(proj.organizationId, org.id),
|
||||
not(eq(proj.isArchived, true)),
|
||||
),
|
||||
with: {
|
||||
databases: true,
|
||||
},
|
||||
});
|
||||
|
||||
const proj = await db.query.project.findFirst({
|
||||
where: (proj, {
|
||||
and,
|
||||
eq,
|
||||
not
|
||||
}) => and(eq(proj.id, projectId), eq(proj.organizationId, org.id), not(eq(proj.isArchived, true))),
|
||||
with: {
|
||||
databases: true,
|
||||
},
|
||||
});
|
||||
if (!proj) {
|
||||
redirect("/dashboard/projects");
|
||||
}
|
||||
|
||||
if (!proj) {
|
||||
redirect("/dashboard/projects");
|
||||
}
|
||||
const availableDatabases = await getOrganizationAvailableDatabases(
|
||||
organization.id,
|
||||
proj.id,
|
||||
);
|
||||
const isMember = activeMember?.role === "member";
|
||||
|
||||
const availableDatabases = await getOrganizationAvailableDatabases(organization.id, proj.id)
|
||||
const isMember = activeMember?.role === "member";
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="justify-between gap-2 sm:flex">
|
||||
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||
<div className="min-w-full md:min-w-fit ">
|
||||
{capitalizeFirstLetter(proj.name)}
|
||||
</div>
|
||||
{!isMember && (
|
||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||
<div className="flex items-center gap-2">
|
||||
<ProjectDialog
|
||||
databases={availableDatabases}
|
||||
organization={org}
|
||||
project={proj as ProjectWith}
|
||||
isEdit={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PageTitle>
|
||||
return (
|
||||
<Page>
|
||||
<div className="justify-between gap-2 sm:flex">
|
||||
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||
<div className="min-w-full md:min-w-fit ">
|
||||
{capitalizeFirstLetter(proj.name)}
|
||||
</div>
|
||||
{!isMember && (
|
||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||
<div className="flex items-center gap-2">
|
||||
<ProjectDialog
|
||||
databases={availableDatabases}
|
||||
organization={org}
|
||||
project={proj as ProjectWith}
|
||||
isEdit={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonDeleteProject
|
||||
projectId={projectId}
|
||||
text={"Delete Project"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PageContent className="flex flex-col w-full h-full">
|
||||
{proj.databases.length > 0 ? (
|
||||
<CardsWithPagination
|
||||
data={[...proj.databases].sort((a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
)}
|
||||
organizationSlug={organization.slug}
|
||||
// @ts-ignore
|
||||
cardItem={ProjectDatabaseCard}
|
||||
cardsPerPage={20}
|
||||
numberOfColumns={3}
|
||||
pageSizeOptions={[10, 20, 50]}
|
||||
extendedProps={proj}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground py-20">
|
||||
<p className="text-lg font-medium">No databases found</p>
|
||||
<p className="text-sm mt-2">You haven’t added any databases to this project yet.</p>
|
||||
</div>
|
||||
)}
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</PageTitle>
|
||||
</div>
|
||||
<PageContent className="flex flex-col w-full h-full">
|
||||
{proj.databases.length > 0 ? (
|
||||
<CardsWithPagination
|
||||
data={[...proj.databases].sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() -
|
||||
new Date(a.createdAt).getTime(),
|
||||
)}
|
||||
organizationSlug={organization.slug}
|
||||
// @ts-ignore
|
||||
cardItem={ProjectDatabaseCard}
|
||||
cardsPerPage={20}
|
||||
numberOfColumns={3}
|
||||
pageSizeOptions={[10, 20, 50]}
|
||||
extendedProps={proj}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground py-20">
|
||||
<p className="text-lg font-medium">No databases found</p>
|
||||
<p className="text-sm mt-2">
|
||||
You haven’t added any databases to this project yet.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {ProjectCard} from "@/features/projects/project-card";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||
import {ProjectCard} from "@/features/projects/components/project-card";
|
||||
import {db} from "@/db";
|
||||
import {notFound} from "next/navigation";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import {EmptyStatePlaceholder} from "@/components/common/empty-state-placeholder";
|
||||
import {Metadata} from "next";
|
||||
import {ProjectDialog} from "@/features/projects/project-dialog";
|
||||
import {ProjectDialog} from "@/features/projects/components/project-dialog";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
import {getOrganizationAvailableDatabases} from "@/db/services/database";
|
||||
|
||||
|
||||
@@ -4,17 +4,17 @@ import {
|
||||
PageContent,
|
||||
PageDescription,
|
||||
PageTitle,
|
||||
} from "@/features/layout/page";
|
||||
} from "@/features/layout/components/page";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {notFound} from "next/navigation";
|
||||
import {ButtonDeleteAgent} from "@/features/agents/agent-delete-button";
|
||||
import {ButtonDeleteAgent} from "@/features/agents/components/agent-delete-button";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
import {generateEdgeKey} from "@/utils/edge_key";
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
import {AgentContentPage} from "@/features/agents/agent-content";
|
||||
import {AgentDialog} from "@/features/agents/agent-dialog";
|
||||
import {AgentContentPage} from "@/features/agents/components/agent-content";
|
||||
import {AgentDialog} from "@/features/agents/components/agent-dialog";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||
|
||||
@@ -4,17 +4,17 @@ import {
|
||||
PageContent,
|
||||
PageHeader,
|
||||
PageTitle,
|
||||
} from "@/features/layout/page";
|
||||
} from "@/features/layout/components/page";
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import {notFound} from "next/navigation";
|
||||
import {Metadata} from "next";
|
||||
import {OrganizationTabs} from "@/features/organizations/organization-tabs";
|
||||
import {OrganizationTabs} from "@/features/organizations/components/organization-tabs";
|
||||
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
||||
import {DeleteOrganizationButton} from "@/features/organizations/organization-delete-button";
|
||||
import {EditOrganizationDialog} from "@/features/organizations/organization-edit-dialog";
|
||||
import {DeleteOrganizationButton} from "@/features/organizations/components/organization-delete-button";
|
||||
import {EditOrganizationDialog} from "@/features/organizations/components/organization-edit-dialog";
|
||||
import {db} from "@/db";
|
||||
import {isNull} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {EvolutionLineChart} from "@/features/statistics/evolution-line-chart";
|
||||
import {PercentageLineChart} from "@/features/statistics/percentage-line-chart";
|
||||
import {EvolutionLineChart} from "@/features/statistics/components/evolution-line-chart";
|
||||
import {PercentageLineChart} from "@/features/statistics/components/percentage-line-chart";
|
||||
import {notFound} from "next/navigation";
|
||||
import {db} from "@/db";
|
||||
import {and, asc, count, eq, inArray} from "drizzle-orm";
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/components/page";
|
||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {Building2, Database, DatabaseBackup, Folder, RefreshCcw, Server, Workflow} from "lucide-react";
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
import {notFound} from "next/navigation";
|
||||
import {db} from "@/db";
|
||||
import {and, asc, eq, inArray} from "drizzle-orm";
|
||||
|
||||
import {and, asc, eq, inArray, not} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {listOrganizations} from "@/lib/auth/auth";
|
||||
import {Metadata} from "next";
|
||||
@@ -23,7 +24,9 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
const organizationIds = organizations.map(project => project.id);
|
||||
|
||||
const agents = await db.query.agent.findMany({});
|
||||
const agents = await db.query.agent.findMany({
|
||||
where: not(eq(drizzleDb.schemas.agent.isArchived, true)),
|
||||
});
|
||||
|
||||
const projects = await db.query.project.findMany({
|
||||
where: and(inArray(drizzleDb.schemas.project.organizationId, organizationIds), eq(drizzleDb.schemas.project.isArchived, false)),
|
||||
|
||||
@@ -2,21 +2,25 @@ import { ReactNode } from "react";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { AppSidebar } from "@/features/layout/app-sidebar";
|
||||
import { Header } from "@/features/layout/header";
|
||||
import { AppSidebar } from "@/features/layout/components/app-sidebar";
|
||||
import { Header } from "@/features/layout/components/header";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
import { ThemeMetaUpdater } from "@/features/theme/theme-meta-updater";
|
||||
import { ModeToggle } from "@/features/theme/mode-toggle";
|
||||
import { UpdateNotification } from "@/features/updates/update-notification";
|
||||
import { isOnboardingDone } from "@/db/services/setting";
|
||||
import { env } from "@/env.mjs";
|
||||
import { ModeToggle } from "@/features/theme/components/mode-toggle";
|
||||
import { UpdateNotification } from "@/features/updates/components/update-notification";
|
||||
|
||||
export default async function Layout({ children }: { children: ReactNode }) {
|
||||
if (env.SKIP_ONBOARDING !== "true" && !(await isOnboardingDone())) {
|
||||
redirect("/welcome");
|
||||
}
|
||||
|
||||
const user = await currentUser();
|
||||
if (!user) redirect("/login");
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<div className="flex flex-col lg:flex-row w-full">
|
||||
<ThemeMetaUpdater />
|
||||
<AppSidebar updateNotification={<UpdateNotification />} />
|
||||
<SidebarInset>
|
||||
<Header actions={<ModeToggle />} />
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getCurrentOrganizationSlug } from "@/features/organizations/organization-cookie";
|
||||
import { getCurrentOrganizationSlug } from "@/features/organizations/utils/organization-cookie";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
import { isOnboardingDone } from "@/db/services/setting";
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
export default async function Index() {
|
||||
if (env.SKIP_ONBOARDING !== "true" && !(await isOnboardingDone())) {
|
||||
redirect("/welcome");
|
||||
}
|
||||
|
||||
const user = await currentUser();
|
||||
if (user) {
|
||||
const currentOrganizationSlug = await getCurrentOrganizationSlug();
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { OnboardingProvider } from "@onboardjs/react";
|
||||
import { onboardingSteps } from "@/features/onboarding/onboarding-steps";
|
||||
import { OnboardingShell } from "@/features/onboarding/onboarding-shell";
|
||||
import type { OnboardingFlowData } from "@/features/onboarding/types";
|
||||
|
||||
type Props = {
|
||||
initialStepId: string;
|
||||
initialFlowData: Partial<OnboardingFlowData>;
|
||||
};
|
||||
|
||||
export const OnboardingClient = ({ initialStepId, initialFlowData }: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<OnboardingProvider
|
||||
steps={onboardingSteps}
|
||||
initialStepId={initialStepId}
|
||||
initialContext={{ flowData: initialFlowData }}
|
||||
onFlowComplete={async () => {
|
||||
router.push("/dashboard/home");
|
||||
}}
|
||||
>
|
||||
<OnboardingShell />
|
||||
</OnboardingProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { resolveOnboardingState } from "@/features/onboarding/onboarding-state";
|
||||
import { OnboardingClient } from "./onboarding-client";
|
||||
|
||||
export default async function WelcomePage() {
|
||||
const result = await resolveOnboardingState();
|
||||
|
||||
if ("redirect" in result) {
|
||||
redirect(result.redirect);
|
||||
}
|
||||
|
||||
return (
|
||||
<OnboardingClient
|
||||
key={result.stepId}
|
||||
initialStepId={result.stepId}
|
||||
initialFlowData={result.flowData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import {getDatabaseOrThrow, withAgentCheck} from "./helpers";
|
||||
import {Backup} from "@/db/schema/07_database";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {eventEmitter} from "@/lib/event";
|
||||
import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers";
|
||||
import {EventKind} from "@/features/notifications/notifications.types";
|
||||
import {sendNotificationsBackupRestore} from "@/features/notifications/utils/notifications.helpers";
|
||||
import {EventKind} from "@/features/notifications/types";
|
||||
import {logger} from "@/lib/logger";
|
||||
import {JobLogEntry} from "@/features/logs/types";
|
||||
|
||||
|
||||
@@ -1,79 +1,86 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import { NextResponse } from "next/server";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {db} from "@/db";
|
||||
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
||||
import {isUuidv4} from "@/utils/verify-uuid";
|
||||
import {eventEmitter} from "@/lib/event";
|
||||
import {logger} from "@/lib/logger";
|
||||
import { db } from "@/db";
|
||||
import { getDatabaseOrThrow, withAgentCheck } from "../../helpers";
|
||||
import { eventEmitter } from "@/lib/event";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { isUUID } from "@/utils/text";
|
||||
|
||||
const log = logger.child({module: "api/agent/backup/upload/init"});
|
||||
const log = logger.child({ module: "api/agent/backup/upload/init" });
|
||||
|
||||
export type Body = {
|
||||
generatedId: string
|
||||
storageChannelId: string
|
||||
backupId: string
|
||||
}
|
||||
export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
||||
params: Promise<{ agentId: string }>,
|
||||
agent: any
|
||||
}) => {
|
||||
generatedId: string;
|
||||
storageChannelId: string;
|
||||
backupId: string;
|
||||
};
|
||||
export const POST = withAgentCheck(
|
||||
async (
|
||||
request: Request,
|
||||
{
|
||||
params,
|
||||
agent,
|
||||
}: {
|
||||
params: Promise<{ agentId: string }>;
|
||||
agent: any;
|
||||
},
|
||||
) => {
|
||||
try {
|
||||
const body: Body = await request.json();
|
||||
const body: Body = await request.json();
|
||||
|
||||
log.info({data: body}, "Body for backup upload init");
|
||||
log.info({ data: body }, "Body for backup upload init");
|
||||
|
||||
const generatedId = body.generatedId;
|
||||
const storageChannelId = body.storageChannelId;
|
||||
const backupId = body.backupId;
|
||||
|
||||
if (!generatedId || !isUuidv4(generatedId)) {
|
||||
return NextResponse.json(
|
||||
{error: "generatedId is not a valid UUID"},
|
||||
{status: 400}
|
||||
);
|
||||
}
|
||||
|
||||
const database = await getDatabaseOrThrow(generatedId);
|
||||
|
||||
const backup = await db.query.backup.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.id, backupId),
|
||||
eq(drizzleDb.schemas.backup.databaseId, database.id),
|
||||
),
|
||||
});
|
||||
|
||||
if (!backup) {
|
||||
return NextResponse.json(
|
||||
{error: "Unable to find the corresponding backup"},
|
||||
{status: 404}
|
||||
);
|
||||
}
|
||||
|
||||
const [backupStorage] = await db
|
||||
.insert(drizzleDb.schemas.backupStorage)
|
||||
.values({
|
||||
backupId: backup.id,
|
||||
storageChannelId: storageChannelId,
|
||||
status: "pending",
|
||||
})
|
||||
.returning();
|
||||
|
||||
eventEmitter.emit('modification', {update: true});
|
||||
const generatedId = body.generatedId;
|
||||
const storageChannelId = body.storageChannelId;
|
||||
const backupId = body.backupId;
|
||||
|
||||
if (!generatedId || !isUUID(generatedId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: "Backup storage successfully created",
|
||||
backupStorage: backupStorage
|
||||
},
|
||||
{status: 200}
|
||||
{ error: "generatedId is not a valid UUID" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const database = await getDatabaseOrThrow(generatedId);
|
||||
|
||||
const backup = await db.query.backup.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.id, backupId),
|
||||
eq(drizzleDb.schemas.backup.databaseId, database.id),
|
||||
),
|
||||
});
|
||||
|
||||
if (!backup) {
|
||||
return NextResponse.json(
|
||||
{ error: "Unable to find the corresponding backup" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
const [backupStorage] = await db
|
||||
.insert(drizzleDb.schemas.backupStorage)
|
||||
.values({
|
||||
backupId: backup.id,
|
||||
storageChannelId: storageChannelId,
|
||||
status: "pending",
|
||||
})
|
||||
.returning();
|
||||
|
||||
eventEmitter.emit("modification", { update: true });
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: "Backup storage successfully created",
|
||||
backupStorage: backupStorage,
|
||||
},
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
log.error({error: error}, "Error in POST for INIT backup");
|
||||
return NextResponse.json(
|
||||
{error: "Internal server error"},
|
||||
{status: 500}
|
||||
);
|
||||
log.error({ error: error }, "Error in POST for INIT backup");
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,110 +1,124 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import {isUuidv4} from "@/utils/verify-uuid";
|
||||
import { NextResponse } from "next/server";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {db as dbClient, db} from "@/db";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers";
|
||||
import {logger} from "@/lib/logger";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {JobLogEntry} from "@/features/logs/types";
|
||||
import { db as dbClient, db } from "@/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { sendNotificationsBackupRestore } from "@/features/notifications/utils/notifications.helpers";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { withUpdatedAt } from "@/db/utils";
|
||||
import { JobLogEntry } from "@/features/logs/types";
|
||||
import { isUUID } from "@/utils/text";
|
||||
|
||||
const log = logger.child({module: "api/agent/restore"});
|
||||
const log = logger.child({ module: "api/agent/restore" });
|
||||
|
||||
export type BodyResultRestore = {
|
||||
generatedId: string
|
||||
status: string
|
||||
logs: JobLogEntry[]
|
||||
durationMs: number
|
||||
}
|
||||
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
|
||||
|
||||
generatedId: string;
|
||||
status: string;
|
||||
logs: JobLogEntry[];
|
||||
durationMs: number;
|
||||
};
|
||||
type RestorationStatus = "waiting" | "ongoing" | "failed" | "success";
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{params}: { params: Promise<{ agentId: string }> }
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ agentId: string }> },
|
||||
) {
|
||||
try {
|
||||
const agentId = (await params).agentId;
|
||||
const body: BodyResultRestore = await request.json();
|
||||
|
||||
try {
|
||||
|
||||
const agentId = (await params).agentId
|
||||
const body: BodyResultRestore = await request.json();
|
||||
|
||||
|
||||
if (!isUuidv4(body.generatedId)) {
|
||||
return NextResponse.json(
|
||||
{error: "generatedId is not a valid uuid"},
|
||||
{status: 500}
|
||||
);
|
||||
}
|
||||
|
||||
const agent = await db.query.agent.findFirst({
|
||||
where: and(eq(drizzleDb.schemas.agent.id, agentId), eq(drizzleDb.schemas.agent.isArchived, false)),
|
||||
})
|
||||
if (!agent) {
|
||||
return NextResponse.json({error: "Agent not found"}, {status: 404})
|
||||
}
|
||||
|
||||
const database = await db.query.database.findFirst({
|
||||
where: eq(drizzleDb.schemas.database.agentDatabaseId, body.generatedId),
|
||||
with: {
|
||||
alertPolicies: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!database) {
|
||||
return NextResponse.json({error: "Database associated with generatedId provided not found"}, {status: 404})
|
||||
}
|
||||
|
||||
const restoration = await db.query.restoration.findFirst({
|
||||
where: and(eq(drizzleDb.schemas.restoration.status, "ongoing"), eq(drizzleDb.schemas.restoration.databaseId, database.id),)
|
||||
})
|
||||
|
||||
if (!restoration) {
|
||||
return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404})
|
||||
}
|
||||
|
||||
const [restorationUpdated] = await db
|
||||
.update(drizzleDb.schemas.restoration)
|
||||
.set(withUpdatedAt({status: body.status as RestorationStatus, durationMs: body.durationMs}))
|
||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id)).returning();
|
||||
|
||||
|
||||
const logsToInsert = body.logs.map((entry) => ({
|
||||
backupId: null,
|
||||
restorationId: restorationUpdated.id,
|
||||
|
||||
loggedAt: new Date(entry.timestamp),
|
||||
|
||||
entryType: entry.type,
|
||||
level: entry.level,
|
||||
|
||||
message: entry.message,
|
||||
command: entry.command ?? null,
|
||||
output: entry.output ?? null,
|
||||
|
||||
exitCode: entry.exit_code ?? null,
|
||||
durationMs: entry.duration_ms ?? null,
|
||||
}));
|
||||
|
||||
if (logsToInsert.length > 0) {
|
||||
await dbClient
|
||||
.insert(drizzleDb.schemas.jobLog)
|
||||
.values(logsToInsert);
|
||||
}
|
||||
|
||||
await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore");
|
||||
|
||||
const response = {
|
||||
status: true,
|
||||
message: "Restoration successfully updated"
|
||||
}
|
||||
|
||||
return Response.json(response, {status: 200})
|
||||
} catch (error) {
|
||||
log.error({error: error}, "Error in POST handler")
|
||||
return NextResponse.json(
|
||||
{error: 'Internal server error'},
|
||||
{status: 500}
|
||||
);
|
||||
if (!isUUID(body.generatedId)) {
|
||||
return NextResponse.json(
|
||||
{ error: "generatedId is not a valid uuid" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const agent = await db.query.agent.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.agent.id, agentId),
|
||||
eq(drizzleDb.schemas.agent.isArchived, false),
|
||||
),
|
||||
});
|
||||
if (!agent) {
|
||||
return NextResponse.json({ error: "Agent not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const database = await db.query.database.findFirst({
|
||||
where: eq(drizzleDb.schemas.database.agentDatabaseId, body.generatedId),
|
||||
with: {
|
||||
alertPolicies: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!database) {
|
||||
return NextResponse.json(
|
||||
{ error: "Database associated with generatedId provided not found" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
const restoration = await db.query.restoration.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.restoration.status, "ongoing"),
|
||||
eq(drizzleDb.schemas.restoration.databaseId, database.id),
|
||||
),
|
||||
});
|
||||
|
||||
if (!restoration) {
|
||||
return NextResponse.json(
|
||||
{ error: "Unable to fin the corresponding restoration" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
const [restorationUpdated] = await db
|
||||
.update(drizzleDb.schemas.restoration)
|
||||
.set(
|
||||
withUpdatedAt({
|
||||
status: body.status as RestorationStatus,
|
||||
durationMs: body.durationMs,
|
||||
}),
|
||||
)
|
||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id))
|
||||
.returning();
|
||||
|
||||
const logsToInsert = body.logs.map((entry) => ({
|
||||
backupId: null,
|
||||
restorationId: restorationUpdated.id,
|
||||
|
||||
loggedAt: new Date(entry.timestamp),
|
||||
|
||||
entryType: entry.type,
|
||||
level: entry.level,
|
||||
|
||||
message: entry.message,
|
||||
command: entry.command ?? null,
|
||||
output: entry.output ?? null,
|
||||
|
||||
exitCode: entry.exit_code ?? null,
|
||||
durationMs: entry.duration_ms ?? null,
|
||||
}));
|
||||
|
||||
if (logsToInsert.length > 0) {
|
||||
await dbClient.insert(drizzleDb.schemas.jobLog).values(logsToInsert);
|
||||
}
|
||||
|
||||
await sendNotificationsBackupRestore(
|
||||
database,
|
||||
body.status == "failed" ? "error_restore" : "success_restore",
|
||||
);
|
||||
|
||||
const response = {
|
||||
status: true,
|
||||
message: "Restoration successfully updated",
|
||||
};
|
||||
|
||||
return Response.json(response, { status: 200 });
|
||||
} catch (error) {
|
||||
log.error({ error: error }, "Error in POST handler");
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import {Body} from "./route";
|
||||
import {isUuidv4} from "@/utils/verify-uuid";
|
||||
import {Agent} from "@/db/schema/08_agent";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {db, db as dbClient} from "@/db";
|
||||
import {and, eq, inArray} from "drizzle-orm";
|
||||
import {and, eq, inArray, desc, sql} from "drizzle-orm";
|
||||
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import type {StorageInput} from "@/features/storages/storages.types";
|
||||
import {dispatchStorage} from "@/features/storages/storages.dispatch";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {logger} from "@/lib/logger";
|
||||
import {isUUID} from "@/utils/text";
|
||||
import {StorageInput} from "@/features/storages/types";
|
||||
import {dispatchStorage} from "@/features/storages/utils/storages.dispatch";
|
||||
|
||||
const log = logger.child({module: "api/agent/status/helpers"});
|
||||
|
||||
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date, settings: Setting) {
|
||||
const databasesResponse = [];
|
||||
|
||||
const formatDatabase = (database: DatabaseWith, backupAction: boolean, restoreAction: boolean, UrlBackup: string | null, storages: PingDatabaseStorageChannels[], urlMeta: string | null) => ({
|
||||
const formatDatabase = (database: DatabaseWith, backupAction: boolean, restoreAction: boolean, UrlBackup: string | null, storages: PingDatabaseStorageChannels[], urlMeta: string | null, backupSize: number | null) => ({
|
||||
generatedId: database.agentDatabaseId,
|
||||
dbms: database.dbms,
|
||||
storages: storages,
|
||||
@@ -31,7 +31,8 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
restore: {
|
||||
action: restoreAction,
|
||||
file: UrlBackup,
|
||||
metaFile: urlMeta
|
||||
metaFile: urlMeta,
|
||||
size: backupSize
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -49,9 +50,10 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
let restoreAction: boolean = false
|
||||
let urlBackup: string | null = null;
|
||||
let urlMeta: string | null = null
|
||||
let backupSize: number | null = null
|
||||
|
||||
if (!existingDatabase) {
|
||||
if (!isUuidv4(db.generatedId)) {
|
||||
if (!isUUID(db.generatedId)) {
|
||||
return NextResponse.json(
|
||||
{error: "generatedId is not a valid uuid"},
|
||||
{status: 500}
|
||||
@@ -90,7 +92,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
|
||||
const storages = await getDatabaseStorageChannels(databaseCreated.id)
|
||||
|
||||
databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup, storages, null));
|
||||
databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup, storages, null, null));
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -121,7 +123,11 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
|
||||
inArray(drizzleDb.schemas.backup.status, ["waiting", "ongoing"])
|
||||
)
|
||||
),
|
||||
orderBy: [
|
||||
sql`case when ${drizzleDb.schemas.backup.status} = 'waiting' then 0 else 1 end`,
|
||||
desc(drizzleDb.schemas.backup.createdAt)
|
||||
]
|
||||
})
|
||||
|
||||
const restoration = await dbClient.query.restoration.findFirst({
|
||||
@@ -180,6 +186,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
if (result.success) {
|
||||
urlBackup = result.url ?? null;
|
||||
urlMeta = resultMeta.url ?? null
|
||||
backupSize = restoration.backupStorage.size
|
||||
} else {
|
||||
await dbClient
|
||||
.update(drizzleDb.schemas.restoration)
|
||||
@@ -205,7 +212,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||
}
|
||||
const storages = await getDatabaseStorageChannels(databaseUpdated.id)
|
||||
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup, storages, urlMeta));
|
||||
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup, storages, urlMeta, backupSize));
|
||||
}
|
||||
}
|
||||
return databasesResponse;
|
||||
|
||||
@@ -1,99 +1,107 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import {handleDatabases} from "./helpers";
|
||||
import { NextResponse } from "next/server";
|
||||
import { handleDatabases } from "./helpers";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {db} from "@/db";
|
||||
import {EDbmsSchema} from "@/db/schema/types";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {isUuidv4} from "@/utils/verify-uuid";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {logger} from "@/lib/logger";
|
||||
|
||||
|
||||
const log = logger.child({module: "api/agent/status/route"});
|
||||
import { db } from "@/db";
|
||||
import { EDbmsSchema } from "@/db/schema/types";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { withUpdatedAt } from "@/db/utils";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { isUUID } from "@/utils/text";
|
||||
|
||||
const log = logger.child({ module: "api/agent/status/route" });
|
||||
|
||||
export type databaseAgent = {
|
||||
name: string,
|
||||
dbms: EDbmsSchema,
|
||||
generatedId: string
|
||||
pingStatus: boolean
|
||||
}
|
||||
name: string;
|
||||
dbms: EDbmsSchema;
|
||||
generatedId: string;
|
||||
pingStatus: boolean;
|
||||
};
|
||||
|
||||
export type Body = {
|
||||
version: string,
|
||||
databases: databaseAgent[]
|
||||
}
|
||||
|
||||
version: string;
|
||||
databases: databaseAgent[];
|
||||
};
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{params}: { params: Promise<{ agentId: string }> }
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ agentId: string }> },
|
||||
) {
|
||||
try {
|
||||
const agentId = (await params).agentId
|
||||
log.debug(`Agent ID: ${agentId}`)
|
||||
const body: Body = await request.json();
|
||||
const lastContact = new Date();
|
||||
let message: string
|
||||
try {
|
||||
const agentId = (await params).agentId;
|
||||
log.debug(`Agent ID: ${agentId}`);
|
||||
const body: Body = await request.json();
|
||||
const lastContact = new Date();
|
||||
let message: string;
|
||||
|
||||
if (!isUuidv4(agentId)) {
|
||||
message = "agentId is not a valid uuid"
|
||||
log.error({error: message}, "An error occurred")
|
||||
return NextResponse.json(
|
||||
{error: "agentId is not a valid uuid"},
|
||||
{status: 500}
|
||||
);
|
||||
}
|
||||
|
||||
const agent = await db.query.agent.findFirst({
|
||||
where: and(eq(drizzleDb.schemas.agent.id, agentId), eq(drizzleDb.schemas.agent.isArchived, false)),
|
||||
})
|
||||
|
||||
if (!agent) {
|
||||
message = "Agent not found"
|
||||
return NextResponse.json({error: message}, {status: 404})
|
||||
}
|
||||
|
||||
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||
if (!settings) {
|
||||
return NextResponse.json({error: "An error occured"}, {status: 404})
|
||||
}
|
||||
|
||||
const databasesResponse = await handleDatabases(body, agent, lastContact, settings)
|
||||
|
||||
await db
|
||||
.update(drizzleDb.schemas.agent)
|
||||
.set(withUpdatedAt({
|
||||
version: body.version,
|
||||
lastContact: lastContact,
|
||||
healthErrorCount: null
|
||||
}))
|
||||
.where(eq(drizzleDb.schemas.agent.id, agentId));
|
||||
|
||||
await db
|
||||
.insert(drizzleDb.schemas.healthcheckLog)
|
||||
.values({
|
||||
kind: "agent",
|
||||
status: "success",
|
||||
objectId: agentId,
|
||||
date: lastContact
|
||||
})
|
||||
|
||||
const response = {
|
||||
agent: {
|
||||
id: agentId,
|
||||
lastContact: lastContact
|
||||
},
|
||||
databases: databasesResponse
|
||||
}
|
||||
|
||||
return Response.json(response)
|
||||
} catch (error) {
|
||||
log.error({error: error}, "Error in POST handler")
|
||||
return NextResponse.json(
|
||||
{error: 'Internal server error'},
|
||||
{status: 500}
|
||||
);
|
||||
if (!isUUID(agentId)) {
|
||||
message = "agentId is not a valid uuid";
|
||||
log.error({ error: message }, "An error occurred");
|
||||
return NextResponse.json(
|
||||
{ error: "agentId is not a valid uuid" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const agent = await db.query.agent.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.agent.id, agentId),
|
||||
eq(drizzleDb.schemas.agent.isArchived, false),
|
||||
),
|
||||
});
|
||||
|
||||
if (!agent) {
|
||||
message = "Agent not found";
|
||||
return NextResponse.json({ error: message }, { status: 404 });
|
||||
}
|
||||
|
||||
const [settings] = await db
|
||||
.select()
|
||||
.from(drizzleDb.schemas.setting)
|
||||
.where(eq(drizzleDb.schemas.setting.name, "system"))
|
||||
.limit(1);
|
||||
if (!settings) {
|
||||
return NextResponse.json({ error: "An error occured" }, { status: 404 });
|
||||
}
|
||||
|
||||
const databasesResponse = await handleDatabases(
|
||||
body,
|
||||
agent,
|
||||
lastContact,
|
||||
settings,
|
||||
);
|
||||
|
||||
await db
|
||||
.update(drizzleDb.schemas.agent)
|
||||
.set(
|
||||
withUpdatedAt({
|
||||
version: body.version,
|
||||
lastContact: lastContact,
|
||||
healthErrorCount: null,
|
||||
}),
|
||||
)
|
||||
.where(eq(drizzleDb.schemas.agent.id, agentId));
|
||||
|
||||
await db.insert(drizzleDb.schemas.healthcheckLog).values({
|
||||
kind: "agent",
|
||||
status: "success",
|
||||
objectId: agentId,
|
||||
date: lastContact,
|
||||
});
|
||||
|
||||
const response = {
|
||||
agent: {
|
||||
id: agentId,
|
||||
lastContact: lastContact,
|
||||
},
|
||||
databases: databasesResponse,
|
||||
};
|
||||
|
||||
return Response.json(response);
|
||||
} catch (error) {
|
||||
log.error({ error: error }, "Error in POST handler");
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,31 +5,33 @@ import { headers } from "next/headers";
|
||||
|
||||
const authHandler = toNextJsHandler(auth.handler);
|
||||
|
||||
async function blockApiKeyCreateForRestrictedUsers(req: NextRequest): Promise<NextResponse | null> {
|
||||
const url = req.nextUrl;
|
||||
if (req.method !== "POST" || !url.pathname.endsWith("/api-key/create")) {
|
||||
return null;
|
||||
}
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session?.user) {
|
||||
return null;
|
||||
}
|
||||
// @ts-ignore
|
||||
if (session.user.banned || (session.user.role as string) === "pending") {
|
||||
return NextResponse.json(
|
||||
{ error: "Account not eligible to create API keys" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
async function blockApiKeyCreateForRestrictedUsers(
|
||||
req: NextRequest,
|
||||
): Promise<NextResponse | null> {
|
||||
const url = req.nextUrl;
|
||||
if (req.method !== "POST" || !url.pathname.endsWith("/api-key/create")) {
|
||||
return null;
|
||||
}
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session?.user) {
|
||||
return null;
|
||||
}
|
||||
// @ts-ignore
|
||||
if (session.user.banned || (session.user.role as string) === "pending") {
|
||||
return NextResponse.json(
|
||||
{ error: "Account not eligible to create API keys" },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
return authHandler.GET(req);
|
||||
return authHandler.GET(req);
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const guard = await blockApiKeyCreateForRestrictedUsers(req);
|
||||
if (guard) return guard;
|
||||
return authHandler.POST(req);
|
||||
const guard = await blockApiKeyCreateForRestrictedUsers(req);
|
||||
if (guard) return guard;
|
||||
return authHandler.POST(req);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import path from "path";
|
||||
import type {StorageInput} from "@/features/storages/storages.types";
|
||||
import {dispatchStorage} from "@/features/storages/storages.dispatch";
|
||||
import type {StorageInput} from "@/features/storages/types";
|
||||
import {dispatchStorage} from "@/features/storages/utils/storages.dispatch";
|
||||
import {Readable} from "node:stream";
|
||||
import {logger} from "@/lib/logger";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import {auth} from "@/lib/auth/auth";
|
||||
import {headers} from "next/headers";
|
||||
import {StorageInput} from "@/features/storages/storages.types";
|
||||
import {dispatchStorage} from "@/features/storages/storages.dispatch";
|
||||
import {StorageInput} from "@/features/storages/types";
|
||||
import {dispatchStorage} from "@/features/storages/utils/storages.dispatch";
|
||||
import {Readable} from "node:stream";
|
||||
import {logger} from "@/lib/logger";
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {env} from "@/env.mjs";
|
||||
import {logger} from "@/lib/logger";
|
||||
|
||||
const log = logger.child({module: "api/tus/hooks"});
|
||||
|
||||
const FILE_PATH_RE = /^backups\/\d{4}-\d{2}-\d{2}\/[A-Za-z0-9._-]+$/;
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
@@ -24,17 +29,61 @@ export async function POST(request: Request) {
|
||||
event.Upload.Offset === event.Upload.Size
|
||||
) {
|
||||
const id = event.Upload.ID;
|
||||
const fileName = headers["X-File-Name"]?.[0];
|
||||
const filePath = headers["X-File-Path"]?.[0];
|
||||
|
||||
if (!filePath) {
|
||||
return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
|
||||
if (!filePath || !FILE_PATH_RE.test(filePath)) {
|
||||
log.warn({filePath}, "Rejected invalid X-File-Path in TUS hook");
|
||||
return NextResponse.json({error: "Invalid file path"}, {status: 400});
|
||||
}
|
||||
|
||||
const generatedId = headers["X-Generated-Id"]?.[0];
|
||||
const backupStorageId = headers["X-Backup-Storage-Id"]?.[0];
|
||||
|
||||
|
||||
console.log(`Backup Storage ID : ${backupStorageId}`);
|
||||
console.log(`generatedId : ${generatedId}`);
|
||||
|
||||
if (backupStorageId) {
|
||||
if (!generatedId) {
|
||||
log.warn("Missing X-Generated-Id in TUS hook");
|
||||
return NextResponse.json({error: "Forbidden"}, {status: 403});
|
||||
}
|
||||
|
||||
const database = await db.query.database.findFirst({
|
||||
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
|
||||
});
|
||||
|
||||
if (!database) {
|
||||
log.warn({generatedId}, "No database for X-Generated-Id in TUS hook");
|
||||
return NextResponse.json({error: "Forbidden"}, {status: 403});
|
||||
}
|
||||
|
||||
const storage = await db.query.backupStorage.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backupStorage.id, backupStorageId),
|
||||
),
|
||||
with: {backup: true},
|
||||
});
|
||||
|
||||
if (!storage || storage.backup?.databaseId !== database.id) {
|
||||
log.warn({backupStorageId, generatedId}, "No pending backup storage matches TUS hook");
|
||||
return NextResponse.json({error: "Forbidden"}, {status: 403});
|
||||
}
|
||||
} else {
|
||||
log.warn("TUS hook without X-Backup-Storage-Id (legacy agent)");
|
||||
}
|
||||
|
||||
const uploadDir = path.join(env.PRIVATE_PATH!, "/uploads/");
|
||||
|
||||
const oldFilePath = path.join(uploadDir, "tmp", id);
|
||||
const newFilePath = path.join(uploadDir, filePath);
|
||||
|
||||
const resolvedBase = path.resolve(uploadDir);
|
||||
const newFilePath = path.resolve(resolvedBase, filePath);
|
||||
const rel = path.relative(resolvedBase, newFilePath);
|
||||
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
||||
log.warn({filePath}, "Rejected path traversal in TUS hook");
|
||||
return NextResponse.json({error: "Invalid file path"}, {status: 400});
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(newFilePath), {recursive: true});
|
||||
|
||||
@@ -74,7 +123,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
return NextResponse.json({});
|
||||
} catch (error) {
|
||||
log.error({error: error},"TUS Hook error");
|
||||
log.error({error: error}, "TUS Hook error");
|
||||
return NextResponse.json({error: "Internal server error"}, {status: 500});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {withApiKey} from "@/lib/api-v1/middleware";
|
||||
import {logger} from "@/lib/logger";
|
||||
import {ApiKeyContext} from "@/lib/api-v1/types";
|
||||
import {getAgent, resolveAgentAccess} from "@/lib/api-v1/services/agents";
|
||||
import {deleteAgentService} from "@/features/agents/agent-delete.action";
|
||||
import {deleteAgentService} from "@/features/agents/actions/agent-delete.action";
|
||||
|
||||
const log = logger.child({module: "api/v1/agents/[id]"});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as drizzleDb from "@/db";
|
||||
import { inArray, eq, and, or, isNull } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { logger } from "@/lib/logger";
|
||||
import {createAgentService} from "@/features/agents/agents.action";
|
||||
import {createAgentService} from "@/features/agents/actions/agents.action";
|
||||
import { ActionError } from "@/lib/safe-actions/actions";
|
||||
import {getAccessibleAgentIds} from "@/lib/api-v1/services/agents";
|
||||
import {ApiKeyContext} from "@/lib/api-v1/types";
|
||||
|
||||
+5
-2
@@ -1,4 +1,5 @@
|
||||
import BackButton from "@/components/common/back-button";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default async function NotFound() {
|
||||
return (
|
||||
@@ -12,7 +13,9 @@ export default async function NotFound() {
|
||||
The content you are trying to view is not available.
|
||||
</p>
|
||||
</div>
|
||||
<BackButton>Go home</BackButton>
|
||||
<Button asChild>
|
||||
<Link href="/">Go home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+9
-4
@@ -5,8 +5,8 @@ import { type PropsWithChildren, Suspense } from "react";
|
||||
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { ErrorLayout } from "@/components/common/error-layout";
|
||||
import { ThemeMetaUpdaterRoot } from "@/features/theme/theme-meta-updater-root";
|
||||
import { ThemeProvider } from "@/features/theme/theme-provider";
|
||||
import { ThemeMetaUpdater } from "@/features/theme/components/theme-meta-updater";
|
||||
import { ThemeProvider } from "@/features/theme/components/theme-provider";
|
||||
|
||||
export type ProviderProps = PropsWithChildren<{}>;
|
||||
const queryClient = new QueryClient();
|
||||
@@ -14,8 +14,13 @@ const queryClient = new QueryClient();
|
||||
export const Providers = (props: ProviderProps) => {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<ThemeMetaUpdaterRoot />
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="light"
|
||||
enableSystem
|
||||
storageKey="portabase_theme"
|
||||
>
|
||||
<ThemeMetaUpdater />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ErrorLayout>
|
||||
<Toaster />
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
services:
|
||||
app:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: docker/dockerfile/Dockerfile
|
||||
# target: prod
|
||||
image: portabase/portabase:1
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/dockerfile/Dockerfile
|
||||
target: prod
|
||||
# image: portabase/portabase:1
|
||||
ports:
|
||||
- '8887:80'
|
||||
- "8887:80"
|
||||
environment:
|
||||
TZ: "Europe/Paris"
|
||||
env_file:
|
||||
@@ -35,7 +35,7 @@ services:
|
||||
- POSTGRES_USER=devuser
|
||||
- POSTGRES_PASSWORD=changeme
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U devuser -d devdb" ]
|
||||
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
@@ -55,9 +55,102 @@ services:
|
||||
|
||||
# DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;
|
||||
|
||||
gcs:
|
||||
image: fsouza/fake-gcs-server:latest
|
||||
container_name: fake-gcs
|
||||
ports:
|
||||
- "4443:4443"
|
||||
command:
|
||||
- "-scheme"
|
||||
- "http"
|
||||
- "-port"
|
||||
- "4443"
|
||||
- "-public-host"
|
||||
- "localhost:4443"
|
||||
- "-external-url"
|
||||
- "http://localhost:4443"
|
||||
- "-backend"
|
||||
- "filesystem"
|
||||
- "-filesystem-root"
|
||||
- "/storage"
|
||||
volumes:
|
||||
- gcs-data:/data
|
||||
- gcs-storage:/storage
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
gcs-init:
|
||||
image: curlimages/curl:latest
|
||||
depends_on:
|
||||
- gcs
|
||||
entrypoint: ["/bin/sh", "-c"]
|
||||
command: >
|
||||
'until curl -fsS http://gcs:4443/storage/v1/b >/dev/null; do sleep 1; done;
|
||||
curl -fsS http://gcs:4443/storage/v1/b/portabase >/dev/null
|
||||
|| curl -fsS -X POST "http://gcs:4443/storage/v1/b?project=local"
|
||||
-H "Content-Type: application/json"
|
||||
-d "{\"name\":\"portabase\"}"'
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
# curl http://localhost:4443/storage/v1/b
|
||||
# projectId: "test-project",
|
||||
# bucketName: "portabase",
|
||||
# clientEmail: "fake@test.iam.gserviceaccount.com",
|
||||
# privateKey: "unused-when-emulated",
|
||||
|
||||
rustfs:
|
||||
image: rustfs/rustfs:latest
|
||||
container_name: rustfs
|
||||
environment:
|
||||
- RUSTFS_ROOT_USER=admin
|
||||
- RUSTFS_ROOT_PASSWORD=root-password
|
||||
- RUSTFS_ACCESS_KEY=access-key
|
||||
- RUSTFS_SECRET_KEY=secret-key
|
||||
- RUSTFS_VOLUMES=/data
|
||||
- RUSTFS_ADDRESS=:9000
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_CONSOLE_ADDRESS=:9001
|
||||
volumes:
|
||||
- rustfs-data:/data
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-sf", "http://localhost:9000/health" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
ports:
|
||||
- "9001:9001"
|
||||
- "9000:9000"
|
||||
extra_hosts:
|
||||
- "localhost:host-gateway"
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
rustfs-init:
|
||||
image: rustfs/rc:latest
|
||||
container_name: rustfs-init
|
||||
depends_on:
|
||||
rustfs:
|
||||
condition: service_healthy
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
until (/usr/bin/rc alias set rustfs http://rustfs:9000 access-key secret-key) do echo '...waiting...' && sleep 1; done;
|
||||
/usr/bin/rc mb rustfs/portabase;
|
||||
echo 'RustFS initialization complete';
|
||||
tail -f /dev/null
|
||||
"
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
azurite-data:
|
||||
gcs-data:
|
||||
gcs-storage:
|
||||
rustfs-data:
|
||||
|
||||
networks:
|
||||
portabase:
|
||||
|
||||
@@ -1,11 +1,33 @@
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye AS base
|
||||
FROM --platform=$BUILDPLATFORM node:22-slim AS runtime-base
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
bash \
|
||||
tzdata \
|
||||
libc6 \
|
||||
nginx \
|
||||
gnupg \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /etc/apt/keyrings \
|
||||
&& curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
|
||||
| gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg \
|
||||
&& echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" \
|
||||
> /etc/apt/sources.list.d/pgdg.list
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
postgresql-18 \
|
||||
postgresql-client-18 \
|
||||
postgresql-contrib-18 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PATH="/usr/lib/postgresql/18/bin:${PATH}"
|
||||
|
||||
|
||||
FROM runtime-base AS build-base
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
g++ \
|
||||
make \
|
||||
@@ -13,25 +35,9 @@ RUN apt-get update && apt-get install -y \
|
||||
automake \
|
||||
libtool \
|
||||
git \
|
||||
nginx \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
RUN mkdir -p /etc/apt/keyrings \
|
||||
&& curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
|
||||
| gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg \
|
||||
&& echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main" \
|
||||
> /etc/apt/sources.list.d/pgdg.list
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
postgresql-18 \
|
||||
postgresql-client-18 \
|
||||
postgresql-contrib-18 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PATH="/usr/lib/postgresql/18/bin:${PATH}"
|
||||
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.23-bookworm AS tusd-base
|
||||
|
||||
ARG TUSD_VERSION=2.8.0
|
||||
@@ -51,7 +57,7 @@ FROM scratch AS tusd-dist
|
||||
COPY --from=tusd-base /tusddist/tusd /tusd
|
||||
|
||||
|
||||
FROM base AS build-env
|
||||
FROM build-base AS build-env
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
@@ -83,7 +89,7 @@ COPY . .
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN pnpm run build
|
||||
|
||||
FROM base AS prod
|
||||
FROM runtime-base AS prod
|
||||
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
@@ -105,7 +111,6 @@ COPY --from=builder /app/drizzle.config.ts ./
|
||||
COPY --from=builder --chown=1001:1001 /app/.next/standalone ./
|
||||
COPY --from=builder --chown=1001:1001 /app/.next/static ./.next/static
|
||||
COPY --chown=1001:1001 src/db ./src/db
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
|
||||
COPY --from=tusd-dist /tusd /usr/local/bin/tusd
|
||||
RUN chmod +x /usr/local/bin/tusd
|
||||
|
||||
@@ -30,6 +30,11 @@ http {
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
location = /api/tus/hooks {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
|
||||
|
||||
+80
-82
@@ -1,102 +1,100 @@
|
||||
import type {NextConfig} from "next";
|
||||
import {PORTABASE_DEFAULT_SETTINGS} from "./portabase.config";
|
||||
import type { NextConfig } from "next";
|
||||
import { PORTABASE_DEFAULT_SETTINGS } from "./portabase.config";
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
|
||||
function buildCSPHeader(): string {
|
||||
const {CSP} = PORTABASE_DEFAULT_SETTINGS.SECURITY;
|
||||
const { CSP } = PORTABASE_DEFAULT_SETTINGS.SECURITY;
|
||||
|
||||
const directives = [
|
||||
`default-src ${CSP.DEFAULT_SRC.join(" ")}`,
|
||||
`script-src ${CSP.SCRIPT_SRC.join(" ")}`,
|
||||
`style-src ${CSP.STYLE_SRC.join(" ")}`,
|
||||
`img-src ${CSP.IMG_SRC.join(" ")}`,
|
||||
`font-src ${CSP.FONT_SRC.join(" ")}`,
|
||||
`object-src ${CSP.OBJECT_SRC.join(" ")}`,
|
||||
`connect-src ${CSP.CONNECT_SRC.join(" ")}`,
|
||||
`base-uri ${CSP.BASE_URI.join(" ")}`,
|
||||
`form-action ${CSP.FORM_ACTION.join(" ")}`,
|
||||
`frame-ancestors ${CSP.FRAME_ANCESTORS.join(" ")}`,
|
||||
];
|
||||
const directives = [
|
||||
`default-src ${CSP.DEFAULT_SRC.join(" ")}`,
|
||||
`script-src ${CSP.SCRIPT_SRC.join(" ")}`,
|
||||
`style-src ${CSP.STYLE_SRC.join(" ")}`,
|
||||
`img-src ${CSP.IMG_SRC.join(" ")}`,
|
||||
`font-src ${CSP.FONT_SRC.join(" ")}`,
|
||||
`object-src ${CSP.OBJECT_SRC.join(" ")}`,
|
||||
`connect-src ${CSP.CONNECT_SRC.join(" ")}`,
|
||||
`base-uri ${CSP.BASE_URI.join(" ")}`,
|
||||
`form-action ${CSP.FORM_ACTION.join(" ")}`,
|
||||
`frame-ancestors ${CSP.FRAME_ANCESTORS.join(" ")}`,
|
||||
];
|
||||
|
||||
if (CSP.BLOCK_ALL_MIXED_CONTENT) {
|
||||
directives.push("block-all-mixed-content");
|
||||
}
|
||||
if (CSP.BLOCK_ALL_MIXED_CONTENT) {
|
||||
directives.push("block-all-mixed-content");
|
||||
}
|
||||
|
||||
if (CSP.UPGRADE_INSECURE_REQUESTS) {
|
||||
directives.push("upgrade-insecure-requests");
|
||||
}
|
||||
if (CSP.UPGRADE_INSECURE_REQUESTS) {
|
||||
directives.push("upgrade-insecure-requests");
|
||||
}
|
||||
|
||||
return directives.join("; ");
|
||||
return directives.join("; ");
|
||||
}
|
||||
|
||||
function buildPermissionsPolicy(): string {
|
||||
return Object.entries(PORTABASE_DEFAULT_SETTINGS.SECURITY.PERMISSIONS_POLICY)
|
||||
.map(([feature, values]) => `${feature.toLowerCase()}=${values.join(", ")}`)
|
||||
.join(", ");
|
||||
return Object.entries(PORTABASE_DEFAULT_SETTINGS.SECURITY.PERMISSIONS_POLICY)
|
||||
.map(([feature, values]) => `${feature.toLowerCase()}=${values.join(", ")}`)
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
output: "standalone",
|
||||
devIndicators: false,
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
logging: {
|
||||
browserToTerminal: false,
|
||||
},
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: "10gb",
|
||||
},
|
||||
logging: {
|
||||
browserToTerminal: false,
|
||||
},
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: "10gb",
|
||||
},
|
||||
proxyClientMaxBodySize: '10gb',
|
||||
},
|
||||
async rewrites() {
|
||||
if (!isDev) return [];
|
||||
proxyClientMaxBodySize: "10gb",
|
||||
},
|
||||
async rewrites() {
|
||||
if (!isDev) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
source: "/tus/:path*",
|
||||
destination: "http://localhost:1080/tus/:path*",
|
||||
},
|
||||
];
|
||||
},
|
||||
return [
|
||||
{
|
||||
source: "/tus/:path*",
|
||||
destination: "http://localhost:1080/tus/:path*",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: "/(.*)",
|
||||
headers: [
|
||||
{
|
||||
key: "Content-Security-Policy",
|
||||
value: buildCSPHeader(),
|
||||
},
|
||||
{
|
||||
key: "Permissions-Policy",
|
||||
value: buildPermissionsPolicy(),
|
||||
},
|
||||
{
|
||||
key: 'X-Content-Type-Options',
|
||||
value: 'nosniff',
|
||||
},
|
||||
{
|
||||
key: 'X-Frame-Options',
|
||||
value: 'DENY',
|
||||
},
|
||||
{
|
||||
key: 'Referrer-Policy',
|
||||
value: 'strict-origin-when-cross-origin',
|
||||
},
|
||||
{
|
||||
key: 'Strict-Transport-Security',
|
||||
value: 'max-age=63072000; includeSubDomains; preload',
|
||||
}
|
||||
// ...other security headers
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: "/(.*)",
|
||||
headers: [
|
||||
{
|
||||
key: "Content-Security-Policy",
|
||||
value: buildCSPHeader(),
|
||||
},
|
||||
{
|
||||
key: "Permissions-Policy",
|
||||
value: buildPermissionsPolicy(),
|
||||
},
|
||||
{
|
||||
key: "X-Content-Type-Options",
|
||||
value: "nosniff",
|
||||
},
|
||||
{
|
||||
key: "X-Frame-Options",
|
||||
value: "DENY",
|
||||
},
|
||||
{
|
||||
key: "Referrer-Policy",
|
||||
value: "strict-origin-when-cross-origin",
|
||||
},
|
||||
{
|
||||
key: "Strict-Transport-Security",
|
||||
value: "max-age=63072000; includeSubDomains; preload",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
+9
-38
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "portabase",
|
||||
"version": "1.20.1",
|
||||
"version": "1.21.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 8887",
|
||||
@@ -21,55 +21,44 @@
|
||||
"@better-auth/core": "1.6.11",
|
||||
"@better-auth/passkey": "1.6.11",
|
||||
"@better-auth/sso": "1.6.11",
|
||||
"@google-cloud/storage": "^7.19.0",
|
||||
"@hookform/resolvers": "^5.4.0",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@onboardjs/react": "1.0.0-rc.5",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-menubar": "^1.1.16",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-email/components": "^0.0.41",
|
||||
"@t3-oss/env-nextjs": "^0.13.11",
|
||||
"@tanstack/react-query": "^5.100.14",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@types/nodemailer": "^6.4.23",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@zenstackhq/runtime": "2.14.2",
|
||||
"argon2": "^0.43.1",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-auth": "1.6.11",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.3.0",
|
||||
"dockerode": "^4.0.12",
|
||||
"dotenv": "^16.6.1",
|
||||
"drizzle-orm": "0.45.2",
|
||||
"drizzle-zod": "0.8.3",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"googleapis": "^170.1.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.553.0",
|
||||
@@ -79,33 +68,22 @@
|
||||
"next-safe-action": "^7.10.8",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-cron": "^4.2.1",
|
||||
"node-forge": "^1.4.0",
|
||||
"nodemailer": "8.0.5",
|
||||
"npm-check-updates": "^18.3.1",
|
||||
"pg": "^8.21.0",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"prettier": "^3.8.3",
|
||||
"react": "^19.2.6",
|
||||
"react-day-picker": "9.7.0",
|
||||
"react-dom": "^19.2.6",
|
||||
"react-dropzone": "^14.4.1",
|
||||
"react-email": "^4.3.2",
|
||||
"react-hook-form": "^7.76.1",
|
||||
"react-qr-code": "^2.0.21",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-twc": "^1.5.1",
|
||||
"react-use-measure": "^2.1.7",
|
||||
"recharts": "^2.15.4",
|
||||
"server-only": "^0.0.1",
|
||||
"sharp": "^0.34.5",
|
||||
"socket.io": "^4.8.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"sonner": "^2.0.7",
|
||||
"swiper": "^12.1.4",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"uuid": "^11.1.1",
|
||||
"vaul": "^1.1.2",
|
||||
"ws": "^8.21.0",
|
||||
"zod": "4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -115,29 +93,22 @@
|
||||
"@release-it/bumper": "^7.0.5",
|
||||
"@release-it/conventional-changelog": "^10.0.6",
|
||||
"@tailwindcss/postcss": "^4.3.0",
|
||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/node": "^22.19.19",
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/react": "^19.2.15",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@zenstackhq/openapi": "^2.22.1",
|
||||
"@zenstackhq/tanstack-query": "^2.22.2",
|
||||
"baseline-browser-mapping": "^2.10.32",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"esbuild": "^0.27.7",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-next": "^16.2.6",
|
||||
"eslint-plugin-tailwindcss": "^3.18.3",
|
||||
"framer-motion": "^12.40.0",
|
||||
"node-pty": "^1.1.0",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"postcss": "8.5.10",
|
||||
"postcss-load-config": "^6.0.1",
|
||||
"release-it": "^19.2.4",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"tsx": "^4.22.3",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"zenstack": "2.14.2"
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"packageManager": "pnpm@11.8.0+sha512.c1f5e7c4cb241c8f174b743851d82f42b802324afc8b0f116b96adb15aa06664948dde36960a3ba1079ba5b4b29dd0140135b94b5b5f5263592249d68e555f26"
|
||||
}
|
||||
|
||||
Generated
+1471
-3594
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -46,7 +46,7 @@ export const PORTABASE_DEFAULT_SETTINGS = {
|
||||
"https://api.iconify.design",
|
||||
"https://code.iconify.design",
|
||||
"https://api.github.com",
|
||||
|
||||
"https://api.dicebear.com",
|
||||
],
|
||||
OBJECT_SRC: ["'none'"],
|
||||
BASE_URI: ["'self'"],
|
||||
|
||||
@@ -4,7 +4,7 @@ import { errorHandler } from "@/middleware/errorHandler";
|
||||
import { auth } from "@/lib/auth/auth";
|
||||
import { headers } from "next/headers";
|
||||
import { env } from "@/env.mjs";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
import { User } from "@/db/schema/02_user";
|
||||
|
||||
export async function proxy(request: NextRequest) {
|
||||
const url = request.nextUrl.clone();
|
||||
@@ -19,7 +19,7 @@ export async function proxy(request: NextRequest) {
|
||||
new URL(`/login?redirect=${redirectUrl}`, request.url),
|
||||
);
|
||||
}
|
||||
const user = session.user as User
|
||||
const user = session.user as User;
|
||||
|
||||
if (user.banned) {
|
||||
await auth.api.signOut({ headers: await headers() });
|
||||
@@ -104,6 +104,7 @@ function checkRouteExists(pathname: string) {
|
||||
/^\/api\/config\/?$/,
|
||||
/^\/api\/health\/?$/,
|
||||
/^\/api\/google\/drive\/callback\/?$/,
|
||||
/^\/api\/avatar\/?$/,
|
||||
// v1 external API
|
||||
/^\/api\/v1\/mcp\/?$/,
|
||||
/^\/api\/v1\/docs\/?$/,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
@@ -351,6 +351,14 @@
|
||||
"clientRole" : true,
|
||||
"containerId" : "18b5dfb2-44d4-448e-98df-9652daef1a22",
|
||||
"attributes" : { }
|
||||
} ],
|
||||
"exemple-client" : [ {
|
||||
"id" : "67766209-57dd-45c7-940d-ed3313473101",
|
||||
"name" : "uma_protection",
|
||||
"composite" : false,
|
||||
"clientRole" : true,
|
||||
"containerId" : "248d3eaa-466f-4b85-8903-9242959e67ba",
|
||||
"attributes" : { }
|
||||
} ]
|
||||
}
|
||||
},
|
||||
@@ -458,8 +466,8 @@
|
||||
"id" : "b1cedcac-5207-4479-bc29-065ba849e414",
|
||||
"type" : "password",
|
||||
"userLabel" : "My password",
|
||||
"createdDate" : 1772179391727,
|
||||
"secretData" : "{\"value\":\"IXr6L3pvO2BAkn/cH3rh3v2tg1R83u2f8SWEZK2fq5E=\",\"salt\":\"XWV8/yKm7Rw/YXiwXZZUig==\",\"additionalParameters\":{}}",
|
||||
"createdDate" : 1782393421785,
|
||||
"secretData" : "{\"value\":\"L585pLqSeHaNW4HzOu+GzZm+VJWWBO3Df+rPs04gUGk=\",\"salt\":\"w3pmQ0hsMDJnCX+qJ3K+zQ==\",\"additionalParameters\":{}}",
|
||||
"credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
|
||||
} ],
|
||||
"disableableCredentialTypes" : [ ],
|
||||
@@ -467,6 +475,23 @@
|
||||
"realmRoles" : [ "admin", "default-roles-master" ],
|
||||
"notBefore" : 0,
|
||||
"groups" : [ "/Admin" ]
|
||||
}, {
|
||||
"id" : "8c749a0c-f2ed-4bc7-9f54-4e0bcbc36626",
|
||||
"username" : "service-account-exemple-client",
|
||||
"emailVerified" : false,
|
||||
"enabled" : true,
|
||||
"createdTimestamp" : 1772190246795,
|
||||
"totp" : false,
|
||||
"serviceAccountClientId" : "exemple-client",
|
||||
"credentials" : [ ],
|
||||
"disableableCredentialTypes" : [ ],
|
||||
"requiredActions" : [ ],
|
||||
"realmRoles" : [ "default-roles-master" ],
|
||||
"clientRoles" : {
|
||||
"exemple-client" : [ "uma_protection" ]
|
||||
},
|
||||
"notBefore" : 0,
|
||||
"groups" : [ ]
|
||||
}, {
|
||||
"id" : "718dfe46-dbef-41a3-9120-78aeac240431",
|
||||
"username" : "service-account-portabase",
|
||||
@@ -625,6 +650,57 @@
|
||||
"nodeReRegistrationTimeout" : 0,
|
||||
"defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ],
|
||||
"optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ]
|
||||
}, {
|
||||
"id" : "248d3eaa-466f-4b85-8903-9242959e67ba",
|
||||
"clientId" : "exemple-client",
|
||||
"name" : "Exemple Client",
|
||||
"description" : "",
|
||||
"rootUrl" : "https://your-instance.portabase.io",
|
||||
"adminUrl" : "https://your-instance.portabase.io",
|
||||
"baseUrl" : "https://your-instance.portabase.io",
|
||||
"surrogateAuthRequired" : false,
|
||||
"enabled" : true,
|
||||
"alwaysDisplayInConsole" : false,
|
||||
"clientAuthenticatorType" : "client-secret",
|
||||
"secret" : "lFZY6fXzh9uxCXwvucw9JtXGhNRKgnnj",
|
||||
"redirectUris" : [ "https://your-instance.portabase.io/api/auth/sso/callback/exemple-client" ],
|
||||
"webOrigins" : [ "https://your-instance.portabase.io" ],
|
||||
"notBefore" : 0,
|
||||
"bearerOnly" : false,
|
||||
"consentRequired" : false,
|
||||
"standardFlowEnabled" : true,
|
||||
"implicitFlowEnabled" : false,
|
||||
"directAccessGrantsEnabled" : false,
|
||||
"serviceAccountsEnabled" : true,
|
||||
"authorizationServicesEnabled" : true,
|
||||
"publicClient" : false,
|
||||
"frontchannelLogout" : true,
|
||||
"protocol" : "openid-connect",
|
||||
"attributes" : {
|
||||
"realm_client" : "false",
|
||||
"oidc.ciba.grant.enabled" : "false",
|
||||
"client.secret.creation.time" : "1772190246",
|
||||
"backchannel.logout.session.required" : "true",
|
||||
"standard.token.exchange.enabled" : "false",
|
||||
"post.logout.redirect.uris" : "https://your-instance.portabase.io",
|
||||
"oauth2.device.authorization.grant.enabled" : "true",
|
||||
"pkce.code.challenge.method" : "S256",
|
||||
"backchannel.logout.revoke.offline.tokens" : "false",
|
||||
"dpop.bound.access.tokens" : "false"
|
||||
},
|
||||
"authenticationFlowBindingOverrides" : { },
|
||||
"fullScopeAllowed" : true,
|
||||
"nodeReRegistrationTimeout" : -1,
|
||||
"defaultClientScopes" : [ "web-origins", "service_account", "acr", "profile", "roles", "basic", "email" ],
|
||||
"optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ],
|
||||
"authorizationSettings" : {
|
||||
"allowRemoteResourceManagement" : true,
|
||||
"policyEnforcementMode" : "ENFORCING",
|
||||
"resources" : [ ],
|
||||
"policies" : [ ],
|
||||
"scopes" : [ ],
|
||||
"decisionStrategy" : "UNANIMOUS"
|
||||
}
|
||||
}, {
|
||||
"id" : "6022c206-5f1f-45b7-af07-74ce6804cbc7",
|
||||
"clientId" : "master-realm",
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type BackButtonProps = React.ComponentProps<typeof Button> & {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function BackButton({ children, ...props }: BackButtonProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Button onClick={() => router.back()} aria-label={children?.toString()} {...props}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type ThemeKey = "dark" | "light" | "system";
|
||||
|
||||
const themes: { value: ThemeKey }[] = [
|
||||
{ value: "light" },
|
||||
{ value: "dark" },
|
||||
{ value: "system" },
|
||||
];
|
||||
|
||||
const THEME_TEXT: Record<ThemeKey, string> = {
|
||||
dark: "Dark",
|
||||
light: "Light",
|
||||
system: "System",
|
||||
};
|
||||
|
||||
interface ThemeSelectorProps {
|
||||
value?: string;
|
||||
onSelect: (value: ThemeKey) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ThemeSelector({
|
||||
value,
|
||||
onSelect,
|
||||
className,
|
||||
}: ThemeSelectorProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn("grid grid-cols-1 sm:grid-cols-3 gap-4 w-full", className)}
|
||||
>
|
||||
{themes.map((item) => {
|
||||
const isDark = item.value === "dark";
|
||||
const isSystem = item.value === "system";
|
||||
const isActive = value === item.value;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"border-2 rounded-xl p-1 cursor-pointer transition-all hover:bg-accent/50 space-y-2",
|
||||
isActive ? "border-primary bg-primary/5" : "border-muted/40",
|
||||
)}
|
||||
onClick={() => onSelect(item.value)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"p-2 rounded-lg aspect-4/3 flex flex-col gap-2 relative overflow-hidden border",
|
||||
isDark
|
||||
? "bg-slate-950 border-slate-800"
|
||||
: "bg-white border-slate-200",
|
||||
isSystem && "bg-linear-to-br from-white to-slate-950",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-3 w-full rounded-sm shadow-sm opacity-80",
|
||||
isDark ? "bg-slate-800" : "bg-slate-100",
|
||||
)}
|
||||
/>
|
||||
<div className="flex gap-2 flex-1 relative">
|
||||
<div
|
||||
className={cn(
|
||||
"w-1/4 h-full rounded-sm shadow-sm opacity-80",
|
||||
isDark ? "bg-slate-800" : "bg-slate-100",
|
||||
)}
|
||||
/>
|
||||
<div className="flex-1 flex flex-col gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"h-3 w-full rounded-sm shadow-sm opacity-80",
|
||||
isDark ? "bg-slate-800" : "bg-slate-100",
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"flex-1 rounded-sm shadow-sm p-1 space-y-2 opacity-50",
|
||||
isDark ? "bg-slate-800" : "bg-slate-100",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-2 w-3/4 rounded-full",
|
||||
isDark ? "bg-slate-700" : "bg-slate-300",
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"h-2 w-full rounded-full",
|
||||
isDark ? "bg-slate-700" : "bg-slate-300",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-1 px-2">
|
||||
<span className="font-medium text-sm">
|
||||
{THEME_TEXT[item.value]}
|
||||
</span>
|
||||
<div
|
||||
className={cn(
|
||||
"w-4 h-4 rounded-full border flex items-center justify-center transition-all",
|
||||
isActive
|
||||
? "border-primary bg-primary"
|
||||
: "border-muted-foreground/30",
|
||||
)}
|
||||
>
|
||||
{isActive && (
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-primary-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// import * as React from "react";
|
||||
// import EmailLayout from "./email-layout";
|
||||
// import {Text, Section, Button} from "@react-email/components";
|
||||
// import type {EventPayload} from "@/features/notifications/notifications.types";
|
||||
// import type {EventPayload} from "@/features/notifications/types";
|
||||
//
|
||||
// export interface EmailNotificationProps {
|
||||
// payload: EventPayload
|
||||
@@ -48,7 +48,7 @@
|
||||
import * as React from "react";
|
||||
import EmailLayout from "./email-layout";
|
||||
import {Text, Section, Button} from "@react-email/components";
|
||||
import type {EventPayload} from "@/features/notifications/notifications.types";
|
||||
import type {EventPayload} from "@/features/notifications/types";
|
||||
|
||||
export interface EmailNotificationProps {
|
||||
payload: EventPayload;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "settings" ADD COLUMN "onboarding" boolean DEFAULT false NOT NULL;
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Custom SQL migration file, put your code below! --
|
||||
UPDATE settings SET onboarding = true
|
||||
WHERE onboarding = false
|
||||
AND (
|
||||
EXISTS (SELECT 1 FROM organization WHERE slug <> 'default')
|
||||
OR EXISTS (SELECT 1 FROM agents)
|
||||
OR EXISTS (SELECT 1 FROM projects)
|
||||
OR EXISTS (SELECT 1 FROM databases)
|
||||
OR EXISTS (SELECT 1 FROM notification_channel)
|
||||
OR EXISTS (SELECT 1 FROM storage_channel WHERE provider <> 'local')
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TYPE "public"."dbms_status" ADD VALUE 'postgresql-cluster' BEFORE 'mysql';
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TYPE "public"."provider_storage_kind" ADD VALUE 'google-cloud-storage' BEFORE 'blob';
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TYPE "public"."dbms_status" ADD VALUE 'docker-volume';
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -456,6 +456,41 @@
|
||||
"when": 1781960184847,
|
||||
"tag": "0064_orange_richard_fisk",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 65,
|
||||
"version": "7",
|
||||
"when": 1782474059918,
|
||||
"tag": "0065_mighty_killer_shrike",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 66,
|
||||
"version": "7",
|
||||
"when": 1782474105293,
|
||||
"tag": "0066_milky_puma",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 67,
|
||||
"version": "7",
|
||||
"when": 1782573099089,
|
||||
"tag": "0067_clammy_stephen_strange",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 68,
|
||||
"version": "7",
|
||||
"when": 1782575192480,
|
||||
"tag": "0068_busy_zeigeist",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 69,
|
||||
"version": "7",
|
||||
"when": 1782937885439,
|
||||
"tag": "0069_worried_alex_wilder",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -20,10 +20,11 @@ export const setting = pgTable("settings", {
|
||||
defaultStorageChannelId: uuid('default_storage_channel_id')
|
||||
.references(() => storageChannel.id, {onDelete: "set null"}),
|
||||
encryption: boolean("encryption").default(false),
|
||||
onboarding: boolean("onboarding").default(false).notNull(),
|
||||
...timestamps
|
||||
});
|
||||
|
||||
export const settingRelations = relations(setting, ({one, many}) => ({
|
||||
export const settingRelations = relations(setting, ({one}) => ({
|
||||
storageChannel: one(storageChannel, {fields: [setting.defaultStorageChannelId], references: [storageChannel.id]}),
|
||||
notificationChannel: one(notificationChannel, {fields: [setting.defaultNotificationChannelId], references: [notificationChannel.id]}),
|
||||
}));
|
||||
|
||||
@@ -5,7 +5,7 @@ import {relations} from "drizzle-orm";
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
import {z} from "zod";
|
||||
|
||||
export const providerStorageKindEnum = pgEnum('provider_storage_kind', ['local', 's3', 'google-drive', 'blob']);
|
||||
export const providerStorageKindEnum = pgEnum('provider_storage_kind', ['local', 's3', 'google-drive', 'google-cloud-storage', 'blob']);
|
||||
|
||||
export const storageChannel = pgTable('storage_channel', {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import {pgEnum} from "drizzle-orm/pg-core";
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
import {z} from "zod";
|
||||
|
||||
export const dbmsEnum = pgEnum("dbms_status", ["postgresql", "mysql", "mariadb", "mongodb", "sqlite", "redis", "valkey", "firebird", "mssql"]);
|
||||
export const dbmsEnum = pgEnum("dbms_status", ["postgresql", "postgresql-cluster", "mysql", "mariadb", "mongodb", "sqlite", "redis", "valkey", "firebird", "mssql", "docker-volume"]);
|
||||
export const statusEnum = pgEnum("status", ["waiting", "ongoing", "failed", "success"]);
|
||||
export const typeStorageEnum = pgEnum("type_storage", ["local", "s3"]);
|
||||
|
||||
|
||||
+33
-35
@@ -1,43 +1,41 @@
|
||||
import {and, desc, eq, sql} from "drizzle-orm";
|
||||
import {db} from "@/db";
|
||||
import {Agent, agent, organizationAgent} from "@/db/schema/08_agent";
|
||||
import {Database, database} from "@/db/schema/07_database";
|
||||
"use server";
|
||||
|
||||
import { and, desc, eq, sql } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import { Agent, agent, organizationAgent } from "@/db/schema/08_agent";
|
||||
import { Database, database } from "@/db/schema/07_database";
|
||||
|
||||
export async function getOrganizationAgents(organizationId: string) {
|
||||
|
||||
return await db
|
||||
.select({
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
organizationId: agent.organizationId,
|
||||
slug: agent.slug,
|
||||
healthErrorCount: agent.healthErrorCount,
|
||||
description: agent.description,
|
||||
isArchived: agent.isArchived,
|
||||
lastContact: agent.lastContact,
|
||||
version: agent.version,
|
||||
updatedAt: agent.updatedAt,
|
||||
createdAt: agent.createdAt,
|
||||
deletedAt: agent.deletedAt,
|
||||
databases: sql<Database[]>`
|
||||
return (await db
|
||||
.select({
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
organizationId: agent.organizationId,
|
||||
slug: agent.slug,
|
||||
healthErrorCount: agent.healthErrorCount,
|
||||
description: agent.description,
|
||||
isArchived: agent.isArchived,
|
||||
lastContact: agent.lastContact,
|
||||
version: agent.version,
|
||||
updatedAt: agent.updatedAt,
|
||||
createdAt: agent.createdAt,
|
||||
deletedAt: agent.deletedAt,
|
||||
databases: sql<Database[]>`
|
||||
COALESCE(
|
||||
json_agg(${database}.*) FILTER (WHERE ${database}.id IS NOT NULL),
|
||||
'[]'
|
||||
)
|
||||
`,
|
||||
})
|
||||
.from(organizationAgent)
|
||||
.innerJoin(
|
||||
agent,
|
||||
eq(organizationAgent.agentId, agent.id)
|
||||
)
|
||||
.leftJoin(database, eq(database.agentId, agent.id))
|
||||
.groupBy(agent.id)
|
||||
.orderBy(desc(agent.createdAt))
|
||||
.where(
|
||||
and(
|
||||
eq(organizationAgent.organizationId, organizationId),
|
||||
eq(agent.isArchived, false)
|
||||
)
|
||||
) as unknown as Agent[];
|
||||
})
|
||||
.from(organizationAgent)
|
||||
.innerJoin(agent, eq(organizationAgent.agentId, agent.id))
|
||||
.leftJoin(database, eq(database.agentId, agent.id))
|
||||
.groupBy(agent.id)
|
||||
.orderBy(desc(agent.createdAt))
|
||||
.where(
|
||||
and(
|
||||
eq(organizationAgent.organizationId, organizationId),
|
||||
eq(agent.isArchived, false),
|
||||
),
|
||||
)) as unknown as Agent[];
|
||||
}
|
||||
|
||||
+13
-13
@@ -1,19 +1,19 @@
|
||||
"use server"
|
||||
import {eq} from "drizzle-orm";
|
||||
"use server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {db} from "@/db";
|
||||
import { db } from "@/db";
|
||||
|
||||
export async function getDatabaseBackups(databaseId: string) {
|
||||
return await db.query.backup.findMany({
|
||||
where: eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||
return await db.query.backup.findMany({
|
||||
where: eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||
with: {
|
||||
restorations: true,
|
||||
storages: {
|
||||
with: {
|
||||
restorations: true,
|
||||
storages: {
|
||||
with: {
|
||||
storageChannel: true,
|
||||
},
|
||||
},
|
||||
storageChannel: true,
|
||||
},
|
||||
orderBy: (b, {desc}) => [desc(b.createdAt)],
|
||||
});
|
||||
},
|
||||
},
|
||||
orderBy: (b, { desc }) => [desc(b.createdAt)],
|
||||
});
|
||||
}
|
||||
|
||||
+118
-34
@@ -1,39 +1,123 @@
|
||||
"use server"
|
||||
import {db} from "@/db";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
import {AgentWith} from "@/db/schema/08_agent";
|
||||
"use server";
|
||||
import { inArray } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import { database, retentionPolicy } from "@/db/schema/07_database";
|
||||
import { alertPolicy } from "@/db/schema/10_alert-policy";
|
||||
import { storagePolicy } from "@/db/schema/13_storage-policy";
|
||||
import { DatabaseWith } from "@/db/schema/07_database";
|
||||
import { AgentWith } from "@/db/schema/08_agent";
|
||||
import type {
|
||||
OnboardingDbSettings,
|
||||
EventKind,
|
||||
} from "@/features/onboarding/types";
|
||||
|
||||
export async function getOrganizationAvailableDatabases(
|
||||
organizationId: string,
|
||||
projectId?: string
|
||||
organizationId: string,
|
||||
projectId?: string,
|
||||
) {
|
||||
const availableDatabases = (await db.query.database.findMany({
|
||||
where: (db, { eq, or, isNull }) =>
|
||||
projectId
|
||||
? or(isNull(db.projectId), eq(db.projectId, projectId))
|
||||
: isNull(db.projectId),
|
||||
with: {
|
||||
agent: {
|
||||
with: {
|
||||
organizations: true,
|
||||
},
|
||||
},
|
||||
project: true,
|
||||
backups: true,
|
||||
restorations: true,
|
||||
},
|
||||
orderBy: (db, { desc }) => [desc(db.createdAt)],
|
||||
})) as DatabaseWith[];
|
||||
|
||||
const availableDatabases = (
|
||||
await db.query.database.findMany({
|
||||
where: (db, { eq, or, isNull }) =>
|
||||
projectId
|
||||
? or(isNull(db.projectId), eq(db.projectId, projectId))
|
||||
: isNull(db.projectId),
|
||||
with: {
|
||||
agent: {
|
||||
with: {
|
||||
organizations: true
|
||||
}
|
||||
},
|
||||
project: true,
|
||||
backups: true,
|
||||
restorations: true,
|
||||
},
|
||||
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
||||
})
|
||||
) as DatabaseWith[];
|
||||
|
||||
return availableDatabases.filter(db => {
|
||||
const agent = db.agent as AgentWith;
|
||||
if (agent?.isArchived) return false;
|
||||
return (
|
||||
agent?.organizationId === organizationId ||
|
||||
agent?.organizations?.some(org => org.organizationId === organizationId)
|
||||
);
|
||||
})
|
||||
return availableDatabases.filter((db) => {
|
||||
const agent = db.agent as AgentWith;
|
||||
if (agent?.isArchived) return false;
|
||||
return (
|
||||
agent?.organizationId === organizationId ||
|
||||
agent?.organizations?.some((org) => org.organizationId === organizationId)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getDatabasesSettings(
|
||||
databaseIds: string[],
|
||||
): Promise<Record<string, OnboardingDbSettings>> {
|
||||
if (databaseIds.length === 0) return {};
|
||||
|
||||
const [retentionPolicies, dbs, alertPolicies, storagePolicies] =
|
||||
await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(retentionPolicy)
|
||||
.where(inArray(retentionPolicy.databaseId, databaseIds)),
|
||||
db
|
||||
.select({ id: database.id, backupPolicy: database.backupPolicy })
|
||||
.from(database)
|
||||
.where(inArray(database.id, databaseIds)),
|
||||
db
|
||||
.select()
|
||||
.from(alertPolicy)
|
||||
.where(inArray(alertPolicy.databaseId, databaseIds)),
|
||||
db
|
||||
.select()
|
||||
.from(storagePolicy)
|
||||
.where(inArray(storagePolicy.databaseId, databaseIds)),
|
||||
]);
|
||||
|
||||
const result: Record<string, OnboardingDbSettings> = {};
|
||||
|
||||
for (const dbId of databaseIds) {
|
||||
const rp = retentionPolicies.find((r) => r.databaseId === dbId);
|
||||
const dbRow = dbs.find((d) => d.id === dbId);
|
||||
const alerts = alertPolicies.filter((a) => a.databaseId === dbId);
|
||||
const storages = storagePolicies.filter((s) => s.databaseId === dbId);
|
||||
|
||||
const settings: OnboardingDbSettings = {};
|
||||
|
||||
if (rp) {
|
||||
settings.retention = {
|
||||
type: rp.type,
|
||||
count: rp.count ?? 7,
|
||||
days: rp.days ?? 30,
|
||||
gfs: {
|
||||
daily: rp.gfsDaily ?? 7,
|
||||
weekly: rp.gfsWeekly ?? 4,
|
||||
monthly: rp.gfsMonthly ?? 12,
|
||||
yearly: rp.gfsYearly ?? 3,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (dbRow) {
|
||||
if (dbRow.backupPolicy) {
|
||||
settings.backupMethod = "automatic";
|
||||
settings.backupCron = dbRow.backupPolicy;
|
||||
} else {
|
||||
settings.backupMethod = "manual";
|
||||
}
|
||||
}
|
||||
|
||||
if (alerts.length > 0) {
|
||||
settings.notificationPolicies = alerts.map((a) => ({
|
||||
channelId: a.notificationChannelId,
|
||||
eventKinds: a.eventKinds as EventKind[],
|
||||
enabled: a.enabled,
|
||||
}));
|
||||
}
|
||||
|
||||
if (storages.length > 0) {
|
||||
settings.storagePolicies = storages.map((s) => ({
|
||||
channelId: s.storageChannelId,
|
||||
enabled: s.enabled,
|
||||
}));
|
||||
}
|
||||
|
||||
result[dbId] = settings;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+172
-157
@@ -1,185 +1,200 @@
|
||||
import {db} from "@/db";
|
||||
"use server";
|
||||
|
||||
import { db } from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {and, eq, gte, isNotNull, lt} from "drizzle-orm";
|
||||
import {dispatchNotification} from "@/features/notifications/notifications.dispatch";
|
||||
import {EventPayload} from "@/features/notifications/notifications.types";
|
||||
import {logger} from "@/lib/logger";
|
||||
import { and, eq, gte, isNotNull, lt } from "drizzle-orm";
|
||||
import { dispatchNotification } from "@/features/notifications/utils/notifications.dispatch";
|
||||
import { EventPayload } from "@/features/notifications/types";
|
||||
import { logger } from "@/lib/logger";
|
||||
|
||||
const log = logger.child({module: "tasks/healthcheck"});
|
||||
const log = logger.child({ module: "tasks/healthcheck" });
|
||||
|
||||
export async function getHealthLast12hLogs({id}: { id: string }) {
|
||||
const now = new Date()
|
||||
const since = new Date(now.getTime() - 12 * 60 * 60 * 1000)
|
||||
export async function getHealthLast12hLogs({ id }: { id: string }) {
|
||||
const now = new Date();
|
||||
const since = new Date(now.getTime() - 12 * 60 * 60 * 1000);
|
||||
|
||||
return db
|
||||
.select()
|
||||
.from(drizzleDb.schemas.healthcheckLog)
|
||||
.where(
|
||||
and(
|
||||
eq(drizzleDb.schemas.healthcheckLog.objectId, id),
|
||||
gte(drizzleDb.schemas.healthcheckLog.date, since)
|
||||
)
|
||||
)
|
||||
return db
|
||||
.select()
|
||||
.from(drizzleDb.schemas.healthcheckLog)
|
||||
.where(
|
||||
and(
|
||||
eq(drizzleDb.schemas.healthcheckLog.objectId, id),
|
||||
gte(drizzleDb.schemas.healthcheckLog.date, since),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteHealthLogsOlderThan12h() {
|
||||
const now = new Date()
|
||||
const threshold = new Date(now.getTime() - 12 * 60 * 60 * 1000)
|
||||
const now = new Date();
|
||||
const threshold = new Date(now.getTime() - 12 * 60 * 60 * 1000);
|
||||
|
||||
const logsToDelete = await db
|
||||
.select()
|
||||
.from(drizzleDb.schemas.healthcheckLog)
|
||||
.where(
|
||||
lt(drizzleDb.schemas.healthcheckLog.date, threshold)
|
||||
)
|
||||
const logsToDelete = await db
|
||||
.select()
|
||||
.from(drizzleDb.schemas.healthcheckLog)
|
||||
.where(lt(drizzleDb.schemas.healthcheckLog.date, threshold));
|
||||
|
||||
log.info({name: "deleteHealthLogsOlderThan12h"},`Number of logs found to delete: ${logsToDelete.length}`)
|
||||
log.info(
|
||||
{ name: "deleteHealthLogsOlderThan12h" },
|
||||
`Number of logs found to delete: ${logsToDelete.length}`,
|
||||
);
|
||||
|
||||
await db
|
||||
.delete(drizzleDb.schemas.healthcheckLog)
|
||||
.where(
|
||||
lt(drizzleDb.schemas.healthcheckLog.date, threshold)
|
||||
)
|
||||
await db
|
||||
.delete(drizzleDb.schemas.healthcheckLog)
|
||||
.where(lt(drizzleDb.schemas.healthcheckLog.date, threshold));
|
||||
|
||||
return logsToDelete.length
|
||||
return logsToDelete.length;
|
||||
}
|
||||
|
||||
export async function checkAgentsHealthError() {
|
||||
const agents = await db.query.agent.findMany({
|
||||
where: isNotNull(drizzleDb.schemas.agent.lastContact),
|
||||
});
|
||||
const agents = await db.query.agent.findMany({
|
||||
where: isNotNull(drizzleDb.schemas.agent.lastContact),
|
||||
});
|
||||
|
||||
const settings = await db.query.setting.findFirst({
|
||||
where: (fields, {eq}) => eq(fields.name, "system"),
|
||||
});
|
||||
const settings = await db.query.setting.findFirst({
|
||||
where: (fields, { eq }) => eq(fields.name, "system"),
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
throw new Error("System settings not found");
|
||||
}
|
||||
|
||||
if (!settings.defaultNotificationChannelId) {
|
||||
log.error({name: "checkAgentsHealthError"},`No default notification channel id found.`)
|
||||
return
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
for (const agent of agents) {
|
||||
if (!agent.lastContact) continue;
|
||||
|
||||
const lastContactDate = new Date(agent.lastContact);
|
||||
const diffMinutes = (now.getTime() - lastContactDate.getTime()) / 1000 / 60;
|
||||
|
||||
if (diffMinutes > 10) {
|
||||
if ((agent.healthErrorCount ?? 0) < 3) {
|
||||
|
||||
const newHealthErrorCount = (agent.healthErrorCount ?? 0) + 1
|
||||
await db.update(drizzleDb.schemas.agent)
|
||||
.set({
|
||||
healthErrorCount: newHealthErrorCount,
|
||||
})
|
||||
.where(eq(drizzleDb.schemas.agent.id, agent.id));
|
||||
|
||||
const payload: EventPayload = {
|
||||
title: "Agent down",
|
||||
message: `Agent ${agent.name} is down, (notification number: ${newHealthErrorCount}/3)`,
|
||||
level: "critical",
|
||||
event: "error_health_agent",
|
||||
data: {
|
||||
agent: agent.name,
|
||||
id: agent.id,
|
||||
error: "Agent is down",
|
||||
},
|
||||
};
|
||||
log.info({name: "checkAgentsHealthError", payload: payload},`Agent Healthcheck Notification`)
|
||||
|
||||
await dispatchNotification(
|
||||
payload,
|
||||
undefined,
|
||||
settings.defaultNotificationChannelId,
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
if (!settings) {
|
||||
throw new Error("System settings not found");
|
||||
}
|
||||
|
||||
if (!settings.defaultNotificationChannelId) {
|
||||
log.error(
|
||||
{ name: "checkAgentsHealthError" },
|
||||
`No default notification channel id found.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
for (const agent of agents) {
|
||||
if (!agent.lastContact) continue;
|
||||
|
||||
const lastContactDate = new Date(agent.lastContact);
|
||||
const diffMinutes = (now.getTime() - lastContactDate.getTime()) / 1000 / 60;
|
||||
|
||||
if (diffMinutes > 10) {
|
||||
if ((agent.healthErrorCount ?? 0) < 3) {
|
||||
const newHealthErrorCount = (agent.healthErrorCount ?? 0) + 1;
|
||||
await db
|
||||
.update(drizzleDb.schemas.agent)
|
||||
.set({
|
||||
healthErrorCount: newHealthErrorCount,
|
||||
})
|
||||
.where(eq(drizzleDb.schemas.agent.id, agent.id));
|
||||
|
||||
const payload: EventPayload = {
|
||||
title: "Agent down",
|
||||
message: `Agent ${agent.name} is down, (notification number: ${newHealthErrorCount}/3)`,
|
||||
level: "critical",
|
||||
event: "error_health_agent",
|
||||
data: {
|
||||
agent: agent.name,
|
||||
id: agent.id,
|
||||
error: "Agent is down",
|
||||
},
|
||||
};
|
||||
log.info(
|
||||
{ name: "checkAgentsHealthError", payload: payload },
|
||||
`Agent Healthcheck Notification`,
|
||||
);
|
||||
|
||||
await dispatchNotification(
|
||||
payload,
|
||||
undefined,
|
||||
settings.defaultNotificationChannelId,
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function checkDatabasesHealthError() {
|
||||
const databases = await db.query.database.findMany({
|
||||
where: isNotNull(drizzleDb.schemas.database.lastContact),
|
||||
with: {
|
||||
agent: true,
|
||||
alertPolicies: true,
|
||||
},
|
||||
});
|
||||
|
||||
const databases = await db.query.database.findMany({
|
||||
where: isNotNull(drizzleDb.schemas.database.lastContact),
|
||||
with: {
|
||||
agent: true,
|
||||
alertPolicies: true
|
||||
const now = new Date();
|
||||
|
||||
for (const database of databases) {
|
||||
if (!database.lastContact) continue;
|
||||
|
||||
const lastContactDate = new Date(database.lastContact);
|
||||
const diffMinutes = (now.getTime() - lastContactDate.getTime()) / 1000 / 60;
|
||||
|
||||
if (diffMinutes > 10) {
|
||||
if ((database.healthErrorCount ?? 0) < 3) {
|
||||
const newHealthErrorCount = (database.healthErrorCount ?? 0) + 1;
|
||||
await db
|
||||
.update(drizzleDb.schemas.database)
|
||||
.set({
|
||||
healthErrorCount: newHealthErrorCount,
|
||||
})
|
||||
.where(eq(drizzleDb.schemas.database.id, database.id));
|
||||
|
||||
const settings = await db.query.setting.findFirst({
|
||||
where: eq(drizzleDb.schemas.setting.name, "system"),
|
||||
with: { notificationChannel: true },
|
||||
});
|
||||
|
||||
const defaultPolicy = settings?.notificationChannel
|
||||
? [
|
||||
{
|
||||
id: null,
|
||||
notificationChannelId: settings.notificationChannel.id,
|
||||
enabled: settings.notificationChannel.enabled,
|
||||
eventKinds: ["error_health_database"],
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const policiesToUse =
|
||||
database.alertPolicies && database.alertPolicies.length > 0
|
||||
? database.alertPolicies.filter(
|
||||
(policy) =>
|
||||
policy.enabled &&
|
||||
policy.eventKinds.includes("error_health_database"),
|
||||
)
|
||||
: defaultPolicy;
|
||||
|
||||
if (!policiesToUse || policiesToUse.length === 0) {
|
||||
continue;
|
||||
}
|
||||
})
|
||||
|
||||
const now = new Date();
|
||||
const promises = policiesToUse.map((alertPolicy) => {
|
||||
const payload: EventPayload = {
|
||||
title: "Database down",
|
||||
message: `Database ${database.name} is down, (notification number: ${newHealthErrorCount}/3)`,
|
||||
level: "critical",
|
||||
event: "error_health_database",
|
||||
data: {
|
||||
agent: database.name,
|
||||
id: database.id,
|
||||
error: "Database is down",
|
||||
},
|
||||
};
|
||||
|
||||
for (const database of databases) {
|
||||
if (!database.lastContact) continue;
|
||||
log.info(
|
||||
{ name: "checkDatabasesHealthError", payload: payload },
|
||||
`Database Healthcheck Notification`,
|
||||
);
|
||||
|
||||
const lastContactDate = new Date(database.lastContact);
|
||||
const diffMinutes = (now.getTime() - lastContactDate.getTime()) / 1000 / 60;
|
||||
return dispatchNotification(
|
||||
payload,
|
||||
alertPolicy.id == null ? undefined : alertPolicy.id,
|
||||
alertPolicy.id ? undefined : alertPolicy.notificationChannelId,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
if (diffMinutes > 10) {
|
||||
if ((database.healthErrorCount ?? 0) < 3) {
|
||||
|
||||
const newHealthErrorCount = (database.healthErrorCount ?? 0) + 1
|
||||
await db.update(drizzleDb.schemas.database)
|
||||
.set({
|
||||
healthErrorCount: newHealthErrorCount,
|
||||
})
|
||||
.where(eq(drizzleDb.schemas.database.id, database.id));
|
||||
|
||||
const settings = await db.query.setting.findFirst({
|
||||
where: eq(drizzleDb.schemas.setting.name, "system"),
|
||||
with: { notificationChannel: true },
|
||||
});
|
||||
|
||||
const defaultPolicy = settings?.notificationChannel
|
||||
? [{
|
||||
id: null,
|
||||
notificationChannelId: settings.notificationChannel.id,
|
||||
enabled: settings.notificationChannel.enabled,
|
||||
eventKinds: ["error_health_database"]
|
||||
}]
|
||||
: [];
|
||||
|
||||
const policiesToUse = (database.alertPolicies && database.alertPolicies.length > 0)
|
||||
? database.alertPolicies.filter(policy => policy.enabled && policy.eventKinds.includes("error_health_database"))
|
||||
: defaultPolicy;
|
||||
|
||||
if (!policiesToUse || policiesToUse.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const promises = policiesToUse.map(alertPolicy => {
|
||||
|
||||
const payload: EventPayload = {
|
||||
title: "Database down",
|
||||
message: `Database ${database.name} is down, (notification number: ${newHealthErrorCount}/3)`,
|
||||
level: "critical",
|
||||
event: "error_health_database",
|
||||
data: {
|
||||
agent: database.name,
|
||||
id: database.id,
|
||||
error: "Database is down",
|
||||
},
|
||||
};
|
||||
|
||||
log.info({name: "checkDatabasesHealthError", payload: payload},`Database Healthcheck Notification`)
|
||||
|
||||
return dispatchNotification(payload, alertPolicy.id == null ? undefined : alertPolicy.id, alertPolicy.id ? undefined : alertPolicy.notificationChannelId, undefined);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,30 +1,62 @@
|
||||
import {desc, eq} from "drizzle-orm";
|
||||
import {db} from "@/db";
|
||||
import {
|
||||
NotificationChannel,
|
||||
notificationChannel,
|
||||
organizationNotificationChannel
|
||||
} from "@/db/schema/09_notification-channel";
|
||||
import {storageChannel} from "@/db/schema/12_storage-channel";
|
||||
"use server";
|
||||
|
||||
export async function getOrganizationChannels(organizationId: string) {
|
||||
return await db
|
||||
.select({
|
||||
id: notificationChannel.id,
|
||||
name: notificationChannel.name,
|
||||
provider: notificationChannel.provider,
|
||||
config: notificationChannel.config,
|
||||
enabled: notificationChannel.enabled,
|
||||
updatedAt: notificationChannel.updatedAt,
|
||||
createdAt: notificationChannel.createdAt,
|
||||
deletedAt: notificationChannel.deletedAt,
|
||||
organizationId: notificationChannel.organizationId
|
||||
})
|
||||
.from(organizationNotificationChannel)
|
||||
.innerJoin(
|
||||
notificationChannel,
|
||||
eq(organizationNotificationChannel.notificationChannelId, notificationChannel.id)
|
||||
)
|
||||
.orderBy(desc(notificationChannel.createdAt))
|
||||
.where(eq(organizationNotificationChannel.organizationId, organizationId)) as unknown as NotificationChannel[];
|
||||
import { desc, eq, isNull } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import {
|
||||
NotificationChannel,
|
||||
notificationChannel,
|
||||
organizationNotificationChannel,
|
||||
} from "@/db/schema/09_notification-channel";
|
||||
|
||||
export async function getOrganizationChannels(
|
||||
organizationId: string,
|
||||
): Promise<NotificationChannel[]> {
|
||||
const [orgChannels, systemChannels] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: notificationChannel.id,
|
||||
name: notificationChannel.name,
|
||||
provider: notificationChannel.provider,
|
||||
config: notificationChannel.config,
|
||||
enabled: notificationChannel.enabled,
|
||||
updatedAt: notificationChannel.updatedAt,
|
||||
createdAt: notificationChannel.createdAt,
|
||||
deletedAt: notificationChannel.deletedAt,
|
||||
organizationId: notificationChannel.organizationId,
|
||||
})
|
||||
.from(organizationNotificationChannel)
|
||||
.innerJoin(
|
||||
notificationChannel,
|
||||
eq(
|
||||
organizationNotificationChannel.notificationChannelId,
|
||||
notificationChannel.id,
|
||||
),
|
||||
)
|
||||
.orderBy(desc(notificationChannel.createdAt))
|
||||
.where(
|
||||
eq(organizationNotificationChannel.organizationId, organizationId),
|
||||
),
|
||||
db
|
||||
.select({
|
||||
id: notificationChannel.id,
|
||||
name: notificationChannel.name,
|
||||
provider: notificationChannel.provider,
|
||||
config: notificationChannel.config,
|
||||
enabled: notificationChannel.enabled,
|
||||
updatedAt: notificationChannel.updatedAt,
|
||||
createdAt: notificationChannel.createdAt,
|
||||
deletedAt: notificationChannel.deletedAt,
|
||||
organizationId: notificationChannel.organizationId,
|
||||
})
|
||||
.from(notificationChannel)
|
||||
.orderBy(desc(notificationChannel.createdAt))
|
||||
.where(isNull(notificationChannel.organizationId)),
|
||||
]);
|
||||
|
||||
const seen = new Set<string>();
|
||||
return [...orgChannels, ...systemChannels].filter((c) => {
|
||||
if (seen.has(c.id)) return false;
|
||||
seen.add(c.id);
|
||||
return true;
|
||||
}) as NotificationChannel[];
|
||||
}
|
||||
|
||||
@@ -1,81 +1,91 @@
|
||||
import {and, desc, eq, gte, lte} from 'drizzle-orm';
|
||||
import {NotificationLevel, notificationLog} from "@/db/schema/11_notification-log";
|
||||
import {notificationChannel} from "@/db/schema/09_notification-channel";
|
||||
import {db} from "@/db";
|
||||
import {Json} from "drizzle-zod";
|
||||
"use server";
|
||||
|
||||
import { and, desc, eq, gte, lte } from "drizzle-orm";
|
||||
import {
|
||||
NotificationLevel,
|
||||
notificationLog,
|
||||
} from "@/db/schema/11_notification-log";
|
||||
import { notificationChannel } from "@/db/schema/09_notification-channel";
|
||||
import { db } from "@/db";
|
||||
import { Json } from "drizzle-zod";
|
||||
|
||||
export type NotificationLogWithRelations = {
|
||||
id: string;
|
||||
id: string;
|
||||
title: string;
|
||||
level: NotificationLevel;
|
||||
success: boolean;
|
||||
error: string | null;
|
||||
sentAt: Date;
|
||||
payload: Json | null;
|
||||
content: {
|
||||
title: string;
|
||||
level: NotificationLevel;
|
||||
success: boolean;
|
||||
error: string | null;
|
||||
sentAt: Date;
|
||||
payload: Json | null;
|
||||
content: {
|
||||
title: string;
|
||||
message: string;
|
||||
},
|
||||
channel: {
|
||||
name: string;
|
||||
provider: string;
|
||||
} | null;
|
||||
policy: {
|
||||
event: string | null;
|
||||
} | null;
|
||||
message: string;
|
||||
};
|
||||
channel: {
|
||||
name: string;
|
||||
provider: string;
|
||||
} | null;
|
||||
policy: {
|
||||
event: string | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export async function getNotificationHistory(
|
||||
filters?: {
|
||||
channelId?: string;
|
||||
policyId?: string;
|
||||
organizationId?: string;
|
||||
level?: NotificationLevel;
|
||||
success?: boolean;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
limit?: number;
|
||||
}
|
||||
): Promise<NotificationLogWithRelations[]> {
|
||||
const where = [];
|
||||
if (filters?.channelId) where.push(eq(notificationLog.channelId, filters.channelId));
|
||||
if (filters?.policyId) where.push(eq(notificationLog.policyId, filters.policyId));
|
||||
if (filters?.organizationId) where.push(eq(notificationLog.organizationId, filters.organizationId));
|
||||
if (filters?.level) where.push(eq(notificationLog.level, filters.level));
|
||||
if (typeof filters?.success === 'boolean') where.push(eq(notificationLog.success, filters.success));
|
||||
if (filters?.from) where.push(gte(notificationLog.sentAt, filters.from));
|
||||
if (filters?.to) where.push(lte(notificationLog.sentAt, filters.to));
|
||||
export async function getNotificationHistory(filters?: {
|
||||
channelId?: string;
|
||||
policyId?: string;
|
||||
organizationId?: string;
|
||||
level?: NotificationLevel;
|
||||
success?: boolean;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
limit?: number;
|
||||
}): Promise<NotificationLogWithRelations[]> {
|
||||
const where = [];
|
||||
if (filters?.channelId)
|
||||
where.push(eq(notificationLog.channelId, filters.channelId));
|
||||
if (filters?.policyId)
|
||||
where.push(eq(notificationLog.policyId, filters.policyId));
|
||||
if (filters?.organizationId)
|
||||
where.push(eq(notificationLog.organizationId, filters.organizationId));
|
||||
if (filters?.level) where.push(eq(notificationLog.level, filters.level));
|
||||
if (typeof filters?.success === "boolean")
|
||||
where.push(eq(notificationLog.success, filters.success));
|
||||
if (filters?.from) where.push(gte(notificationLog.sentAt, filters.from));
|
||||
if (filters?.to) where.push(lte(notificationLog.sentAt, filters.to));
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
id: notificationLog.id,
|
||||
title: notificationLog.title,
|
||||
level: notificationLog.level,
|
||||
success: notificationLog.success,
|
||||
error: notificationLog.error,
|
||||
sentAt: notificationLog.sentAt,
|
||||
payload: notificationLog.payload,
|
||||
content: {
|
||||
title: notificationLog.title,
|
||||
message: notificationLog.message,
|
||||
},
|
||||
channel: {
|
||||
name: notificationLog.providerName,
|
||||
provider: notificationLog.provider,
|
||||
},
|
||||
policy: {
|
||||
event: notificationLog.event,
|
||||
},
|
||||
})
|
||||
.from(notificationLog)
|
||||
.leftJoin(notificationChannel, eq(notificationLog.channelId, notificationChannel.id))
|
||||
// .leftJoin(alertPolicy, eq(notificationLog.policyId, alertPolicy.id))
|
||||
.where(and(...where))
|
||||
.orderBy(desc(notificationLog.sentAt))
|
||||
.limit(filters?.limit || 100);
|
||||
const rows = await db
|
||||
.select({
|
||||
id: notificationLog.id,
|
||||
title: notificationLog.title,
|
||||
level: notificationLog.level,
|
||||
success: notificationLog.success,
|
||||
error: notificationLog.error,
|
||||
sentAt: notificationLog.sentAt,
|
||||
payload: notificationLog.payload,
|
||||
content: {
|
||||
title: notificationLog.title,
|
||||
message: notificationLog.message,
|
||||
},
|
||||
channel: {
|
||||
name: notificationLog.providerName,
|
||||
provider: notificationLog.provider,
|
||||
},
|
||||
policy: {
|
||||
event: notificationLog.event,
|
||||
},
|
||||
})
|
||||
.from(notificationLog)
|
||||
.leftJoin(
|
||||
notificationChannel,
|
||||
eq(notificationLog.channelId, notificationChannel.id),
|
||||
)
|
||||
// .leftJoin(alertPolicy, eq(notificationLog.policyId, alertPolicy.id))
|
||||
.where(and(...where))
|
||||
.orderBy(desc(notificationLog.sentAt))
|
||||
.limit(filters?.limit || 100);
|
||||
|
||||
return rows.map(row => ({
|
||||
...row,
|
||||
payload: row.payload as Json,
|
||||
}));
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
payload: row.payload as Json,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"use server";
|
||||
|
||||
import { db } from "@/db";
|
||||
import { and, eq, inArray, ne } from "drizzle-orm";
|
||||
import { member } from "@/db/schema/04_member";
|
||||
import { organization } from "@/db/schema/03_organization";
|
||||
|
||||
export async function getUserOrganization(userId: string) {
|
||||
const memberRow = await db.query.member.findFirst({
|
||||
columns: { organizationId: true },
|
||||
where: eq(member.userId, userId),
|
||||
});
|
||||
if (!memberRow) return null;
|
||||
return db.query.organization.findFirst({
|
||||
where: eq(organization.id, memberRow.organizationId),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserOwnOrganization(userId: string) {
|
||||
const memberRows = await db.query.member.findMany({
|
||||
columns: { organizationId: true },
|
||||
where: eq(member.userId, userId),
|
||||
});
|
||||
if (!memberRows.length) return null;
|
||||
const orgIds = memberRows.map((r) => r.organizationId);
|
||||
return db.query.organization.findFirst({
|
||||
where: and(inArray(organization.id, orgIds), ne(organization.slug, "default")),
|
||||
});
|
||||
}
|
||||
+70
-54
@@ -1,61 +1,77 @@
|
||||
import {getOrganization} from "@/lib/auth/auth";
|
||||
import {db} from "@/db";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
"use server";
|
||||
|
||||
import { getOrganization } from "@/lib/auth/auth";
|
||||
import { db } from "@/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import { project } from "@/db/schema/06_project";
|
||||
|
||||
export async function getOrganizationProject(organizationId: string) {
|
||||
return db.query.project.findFirst({
|
||||
where: eq(project.organizationId, organizationId),
|
||||
with: {
|
||||
databases: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const getOrganizationProjectDatabases = async ({organizationSlug, projectId}: {
|
||||
organizationSlug: string, projectId: string
|
||||
export const getOrganizationProjectDatabases = async ({
|
||||
organizationSlug,
|
||||
projectId,
|
||||
}: {
|
||||
organizationSlug: string;
|
||||
projectId: string;
|
||||
}) => {
|
||||
try {
|
||||
try {
|
||||
const organization = await getOrganization({});
|
||||
|
||||
const organization = await getOrganization({});
|
||||
|
||||
if (!organization) {
|
||||
return {
|
||||
name: "ErrorGettingOrganizationProjectDatabases",
|
||||
message: "No organization found.",
|
||||
status: 400,
|
||||
cause: "Unknown error occurred.",
|
||||
};
|
||||
}
|
||||
const databasesProject = await db.query.project.findFirst({
|
||||
where: and(eq(drizzleDb.schemas.project.organizationId, organization.id), eq(drizzleDb.schemas.project.id, projectId)),
|
||||
with: {
|
||||
databases: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!databasesProject) {
|
||||
return {
|
||||
name: "ErrorGettingOrganizationProjectDatabases",
|
||||
message: "No organization found.",
|
||||
status: 400,
|
||||
cause: "Unknown error occurred.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: databasesProject.databases,
|
||||
ids: databasesProject.databases.map((project) => project.id)
|
||||
}
|
||||
|
||||
|
||||
} catch (e: any) {
|
||||
const errorMessage = e?.response?.data?.message || e?.message || "Unknown auth error";
|
||||
const status = e?.response?.status || 500;
|
||||
|
||||
console.error("API GettingOrganizationProjectDatabases error:", {
|
||||
message: errorMessage,
|
||||
status,
|
||||
raw: e,
|
||||
});
|
||||
|
||||
throw {
|
||||
name: "ErrorGettingOrganizationProjectDatabases",
|
||||
message: errorMessage,
|
||||
status,
|
||||
cause: e,
|
||||
};
|
||||
if (!organization) {
|
||||
return {
|
||||
name: "ErrorGettingOrganizationProjectDatabases",
|
||||
message: "No organization found.",
|
||||
status: 400,
|
||||
cause: "Unknown error occurred.",
|
||||
};
|
||||
}
|
||||
const databasesProject = await db.query.project.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.project.organizationId, organization.id),
|
||||
eq(drizzleDb.schemas.project.id, projectId),
|
||||
),
|
||||
with: {
|
||||
databases: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!databasesProject) {
|
||||
return {
|
||||
name: "ErrorGettingOrganizationProjectDatabases",
|
||||
message: "No organization found.",
|
||||
status: 400,
|
||||
cause: "Unknown error occurred.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: databasesProject.databases,
|
||||
ids: databasesProject.databases.map((project) => project.id),
|
||||
};
|
||||
} catch (e: any) {
|
||||
const errorMessage =
|
||||
e?.response?.data?.message || e?.message || "Unknown auth error";
|
||||
const status = e?.response?.status || 500;
|
||||
|
||||
console.error("API GettingOrganizationProjectDatabases error:", {
|
||||
message: errorMessage,
|
||||
status,
|
||||
raw: e,
|
||||
});
|
||||
|
||||
throw {
|
||||
name: "ErrorGettingOrganizationProjectDatabases",
|
||||
message: errorMessage,
|
||||
status,
|
||||
cause: e,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
"use server";
|
||||
|
||||
import { db } from "@/db";
|
||||
|
||||
export async function getSettings() {
|
||||
return db.query.setting.findFirst();
|
||||
}
|
||||
|
||||
export async function isOnboardingDone(): Promise<boolean> {
|
||||
const settings = await db.query.setting.findFirst();
|
||||
return settings?.onboarding ?? false;
|
||||
}
|
||||
@@ -1,25 +1,57 @@
|
||||
import {desc, eq} from "drizzle-orm";
|
||||
import {db} from "@/db";
|
||||
import {organizationStorageChannel, StorageChannel, storageChannel} from "@/db/schema/12_storage-channel";
|
||||
"use server";
|
||||
|
||||
export async function getOrganizationStorageChannels(organizationId: string) {
|
||||
return await db
|
||||
.select({
|
||||
id: storageChannel.id,
|
||||
name: storageChannel.name,
|
||||
provider: storageChannel.provider,
|
||||
organizationId: storageChannel.organizationId,
|
||||
config: storageChannel.config,
|
||||
enabled: storageChannel.enabled,
|
||||
updatedAt: storageChannel.updatedAt,
|
||||
createdAt: storageChannel.createdAt,
|
||||
deletedAt: storageChannel.deletedAt,
|
||||
})
|
||||
.from(organizationStorageChannel)
|
||||
.innerJoin(
|
||||
storageChannel,
|
||||
eq(organizationStorageChannel.storageChannelId, storageChannel.id)
|
||||
)
|
||||
.orderBy(desc(storageChannel.createdAt))
|
||||
.where(eq(organizationStorageChannel.organizationId, organizationId)) as unknown as StorageChannel[];
|
||||
import { desc, eq, isNull } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import {
|
||||
organizationStorageChannel,
|
||||
StorageChannel,
|
||||
storageChannel,
|
||||
} from "@/db/schema/12_storage-channel";
|
||||
|
||||
export async function getOrganizationStorageChannels(
|
||||
organizationId: string,
|
||||
): Promise<StorageChannel[]> {
|
||||
const [orgChannels, systemChannels] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: storageChannel.id,
|
||||
name: storageChannel.name,
|
||||
provider: storageChannel.provider,
|
||||
organizationId: storageChannel.organizationId,
|
||||
config: storageChannel.config,
|
||||
enabled: storageChannel.enabled,
|
||||
updatedAt: storageChannel.updatedAt,
|
||||
createdAt: storageChannel.createdAt,
|
||||
deletedAt: storageChannel.deletedAt,
|
||||
})
|
||||
.from(organizationStorageChannel)
|
||||
.innerJoin(
|
||||
storageChannel,
|
||||
eq(organizationStorageChannel.storageChannelId, storageChannel.id),
|
||||
)
|
||||
.orderBy(desc(storageChannel.createdAt))
|
||||
.where(eq(organizationStorageChannel.organizationId, organizationId)),
|
||||
db
|
||||
.select({
|
||||
id: storageChannel.id,
|
||||
name: storageChannel.name,
|
||||
provider: storageChannel.provider,
|
||||
organizationId: storageChannel.organizationId,
|
||||
config: storageChannel.config,
|
||||
enabled: storageChannel.enabled,
|
||||
updatedAt: storageChannel.updatedAt,
|
||||
createdAt: storageChannel.createdAt,
|
||||
deletedAt: storageChannel.deletedAt,
|
||||
})
|
||||
.from(storageChannel)
|
||||
.orderBy(desc(storageChannel.createdAt))
|
||||
.where(isNull(storageChannel.organizationId)),
|
||||
]);
|
||||
|
||||
const seen = new Set<string>();
|
||||
return [...orgChannels, ...systemChannels].filter((c) => {
|
||||
if (seen.has(c.id)) return false;
|
||||
seen.add(c.id);
|
||||
return true;
|
||||
}) as StorageChannel[];
|
||||
}
|
||||
|
||||
+41
-32
@@ -1,40 +1,49 @@
|
||||
import {SignUpUser} from "@/types/auth";
|
||||
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";
|
||||
"use server";
|
||||
|
||||
import { SignUpUser } from "@/types/auth";
|
||||
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 hasUsers(): Promise<boolean> {
|
||||
const result = await db.select().from(drizzleDb.schemas.user).limit(1);
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
export async function createUserDb(data: SignUpUser): Promise<User> {
|
||||
assertValidPassword(data.password);
|
||||
assertValidPassword(data.password);
|
||||
|
||||
const now = new Date();
|
||||
const userId = crypto.randomUUID();
|
||||
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,
|
||||
emailVerified: true,
|
||||
role: data.role,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
theme: data.theme as UserThemeEnum,
|
||||
}).returning();
|
||||
const [newUser] = await db
|
||||
.insert(drizzleDb.schemas.user)
|
||||
.values({
|
||||
...data,
|
||||
id: userId,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
emailVerified: true,
|
||||
role: data.role,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
theme: data.theme as UserThemeEnum,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (data.password) {
|
||||
const hashedPassword = await hashPassword(data.password);
|
||||
await db.insert(drizzleDb.schemas.account).values({
|
||||
providerId: "credential",
|
||||
accountId: userId,
|
||||
userId: userId,
|
||||
password: hashedPassword,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
if (data.password) {
|
||||
const hashedPassword = await hashPassword(data.password);
|
||||
await db.insert(drizzleDb.schemas.account).values({
|
||||
providerId: "credential",
|
||||
accountId: userId,
|
||||
userId: userId,
|
||||
password: hashedPassword,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
return newUser
|
||||
return newUser;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ export const env = createEnv({
|
||||
process.env.NODE_ENV === "production" ? "0 * * * *" : "* * * * *",
|
||||
),
|
||||
|
||||
STALE_BACKUP_THRESHOLD_HOURS: z.coerce.number().default(6),
|
||||
|
||||
AUTH_OIDC_ID: z.string().optional().default("oidc"),
|
||||
AUTH_OIDC_TITLE: z.string().optional(),
|
||||
@@ -88,6 +89,8 @@ export const env = createEnv({
|
||||
|
||||
ALLOWED_GROUP: z.string().optional(),
|
||||
|
||||
SKIP_ONBOARDING: 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"),
|
||||
@@ -145,6 +148,7 @@ export const env = createEnv({
|
||||
|
||||
RETENTION_CRON: process.env.RETENTION_CRON,
|
||||
CLEANING_HEALTHCHECK_LOGS_CRON: process.env.CLEANING_HEALTHCHECK_LOGS_CRON,
|
||||
STALE_BACKUP_THRESHOLD_HOURS: process.env.STALE_BACKUP_THRESHOLD_HOURS,
|
||||
|
||||
AUTH_OIDC_ID: process.env.AUTH_OIDC_ID,
|
||||
AUTH_OIDC_TITLE: process.env.AUTH_OIDC_TITLE,
|
||||
@@ -174,6 +178,8 @@ export const env = createEnv({
|
||||
|
||||
ALLOWED_GROUP: process.env.ALLOWED_GROUP,
|
||||
|
||||
SKIP_ONBOARDING: process.env.SKIP_ONBOARDING,
|
||||
|
||||
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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
import {action, ActionError, userAction} from "@/lib/safe-actions/actions";
|
||||
import {AgentSchema} from "@/features/agents/agents.schema";
|
||||
import {AgentSchema} from "@/features/agents/schemas/agents.schema";
|
||||
import {z} from "zod";
|
||||
import {eq, and, ne, count, desc} from "drizzle-orm";
|
||||
import {db} from "@/db";
|
||||
@@ -11,7 +11,7 @@ import {Badge} from "@/components/ui/badge";
|
||||
import {truncateWords} from "@/utils/text";
|
||||
import {useIsMobile} from "@/hooks/use-mobile";
|
||||
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
import {useAgentUpdateCheck} from "@/features/agents/use-agent-update-check";
|
||||
import {useAgentUpdateCheck} from "@/features/agents/hooks/use-agent-update-check";
|
||||
|
||||
export type agentCardProps = {
|
||||
data: AgentWith;
|
||||
+4
-4
@@ -3,10 +3,10 @@
|
||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {Server} from "lucide-react";
|
||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||
import {AgentCardKey} from "@/features/agents/agent-card-key";
|
||||
import {AgentCardKey} from "@/features/agents/components/agent-card-key";
|
||||
import {AgentWithDatabases} from "@/db/schema/08_agent";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {getAgentAction} from "@/features/agents/agents.action";
|
||||
import {getAgentAction} from "@/features/agents/actions/agents.action";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
||||
import {AgentDatabaseCard} from "@/features/agents/agent-database-card";
|
||||
import {HealthCheckGraph} from "@/features/database/health-grid";
|
||||
import {AgentDatabaseCard} from "@/features/agents/components/agent-database-card";
|
||||
import {HealthCheckGraph} from "@/features/database/components/health-grid";
|
||||
import {HealthcheckLog} from "@/db/schema/15_healthcheck-log";
|
||||
|
||||
type AgentContentPageProps = {
|
||||
+1
-1
@@ -5,7 +5,7 @@ import {ButtonWithConfirm} from "@/components/common/button-with-confirm";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {toast} from "sonner";
|
||||
import {deleteAgentAction} from "@/features/agents/agent-delete.action";
|
||||
import {deleteAgentAction} from "@/features/agents/actions/agent-delete.action";
|
||||
import {useIsMobile} from "@/hooks/use-mobile";
|
||||
|
||||
export type ButtonDeleteAgentProps = {
|
||||
+2
-2
@@ -7,7 +7,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {AgentForm} from "@/features/agents/agent-form";
|
||||
import {AgentForm} from "@/features/agents/components/agent-form";
|
||||
import {Button, buttonVariants} from "@/components/ui/button";
|
||||
import {Plus} from "lucide-react";
|
||||
import {GearIcon} from "@radix-ui/react-icons";
|
||||
@@ -16,7 +16,7 @@ import {useState} from "react";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {AgentOrganisationForm} from "@/features/agents/agent-organizations.form";
|
||||
import {AgentOrganisationForm} from "@/features/agents/components/agent-organizations.form";
|
||||
import {AgentWith} from "@/db/schema/08_agent";
|
||||
|
||||
type AgentDialogProps = {
|
||||
@@ -15,9 +15,9 @@ import {Button} from "@/components/ui/button";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import {AgentSchema, AgentType} from "@/features/agents/agents.schema";
|
||||
import {AgentSchema, AgentType} from "@/features/agents/schemas/agents.schema";
|
||||
import {toast} from "sonner";
|
||||
import {createAgentAction, updateAgentAction} from "@/features/agents/agents.action";
|
||||
import {createAgentAction, updateAgentAction} from "@/features/agents/actions/agents.action";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
|
||||
export type agentFormProps = {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user