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 |
@@ -81,3 +81,6 @@ TRUSTED_DOMAINS="http://localhost:8887, http://localhost:3055, http://localhost:
|
|||||||
|
|
||||||
# Default to false
|
# Default to false
|
||||||
#TUSD_BEHIND_PROXY=true
|
#TUSD_BEHIND_PROXY=true
|
||||||
|
|
||||||
|
#Skip the onboarding on fresh instance
|
||||||
|
#SKIP_ONBOARDING=true
|
||||||
|
|||||||
+1
-5
@@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": [
|
"extends": ["next/core-web-vitals", "plugin:tailwindcss/recommended"],
|
||||||
"next/core-web-vitals",
|
|
||||||
"plugin:tailwindcss/recommended"
|
|
||||||
],
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"react/no-unescaped-entities": "off",
|
"react/no-unescaped-entities": "off",
|
||||||
"react-hooks/rules-of-hooks": "off"
|
"react-hooks/rules-of-hooks": "off"
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
tags: ${{ steps.prep.outputs.image }}
|
tags: ${{ steps.prep.outputs.image }}
|
||||||
target: ${{ inputs.target }}
|
target: ${{ inputs.target }}
|
||||||
cache-from: type=gha,scope=build-${{ matrix.platform }}
|
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
|
- name: Save image name for manifest
|
||||||
run: echo "${{ steps.prep.outputs.image }}" > image.txt
|
run: echo "${{ steps.prep.outputs.image }}" > image.txt
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
gcs-data/
|
||||||
|
gcs-storage/
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
@@ -28,6 +30,8 @@ yarn-error.log*
|
|||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env*.local
|
.env*.local
|
||||||
|
.env
|
||||||
|
**/.env
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
@@ -60,3 +64,4 @@ certificates
|
|||||||
.codex
|
.codex
|
||||||
/docs
|
/docs
|
||||||
.worktrees
|
.worktrees
|
||||||
|
.superpowers
|
||||||
+1
-1
@@ -33,5 +33,5 @@ keywords:
|
|||||||
- web-ui
|
- web-ui
|
||||||
- agent
|
- agent
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
version: 1.20.2
|
version: 1.21.3
|
||||||
date-released: '2026-03-02'
|
date-released: '2026-03-02'
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ You have 5 ways to install Portabase:
|
|||||||
| **Valkey** | ✅ Stable | 7.2+ | No |
|
| **Valkey** | ✅ Stable | 7.2+ | No |
|
||||||
| **Firebird** | ✅ Stable | 3.0, 4.0, 5.0 | Yes |
|
| **Firebird** | ✅ Stable | 3.0, 4.0, 5.0 | Yes |
|
||||||
| **MSSQL Server** | ✅ Stable | 2017, 2019, 2022, Azure SQL | 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.
|
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 { CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
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 { env } from "@/env.mjs";
|
||||||
import { CardAuth } from "@/features/layout/card-auth";
|
import { CardAuth } from "@/features/layout/components/card-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default async function RoutePage(props: { searchParams: Promise<{ callbackUrl: string | undefined }> }) {
|
export default async function RoutePage(props: { searchParams: Promise<{ callbackUrl: string | undefined }> }) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {CardContent, CardHeader} from "@/components/ui/card";
|
import {CardContent, CardHeader} from "@/components/ui/card";
|
||||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
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 {cookies} from "next/headers";
|
||||||
import {redirect} from "next/navigation";
|
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() {
|
export default async function GuardPage() {
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { currentUser } from "@/lib/auth/current-user";
|
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";
|
import { Heart } from "lucide-react";
|
||||||
|
|
||||||
export default async function Layout({
|
export default async function Layout({
|
||||||
@@ -9,6 +11,10 @@ export default async function Layout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
if (env.SKIP_ONBOARDING !== "true" && !(await isOnboardingDone())) {
|
||||||
|
redirect("/welcome");
|
||||||
|
}
|
||||||
|
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
|
|
||||||
if (user && !user.banned && user.role !== "pending") {
|
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 { Metadata } from "next";
|
||||||
import { SUPPORTED_PROVIDERS } from "@/lib/auth/config";
|
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 { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { CardContent, CardHeader } from "@/components/ui/card";
|
import { CardContent, CardHeader } from "@/components/ui/card";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Separator } from "@/components/ui/separator";
|
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";
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PageParams } from "@/types/next";
|
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 { Metadata } from "next";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { env } from "@/env.mjs";
|
import { env } from "@/env.mjs";
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { CardContent, CardHeader } from "@/components/ui/card";
|
import { CardContent, CardHeader } from "@/components/ui/card";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
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 { redirect } from "next/navigation";
|
||||||
import { auth } from "@/lib/auth/auth";
|
import { auth } from "@/lib/auth/auth";
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
|
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";
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) {
|
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 {db} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page} from "@/features/layout/page";
|
import {Page} from "@/features/layout/components/page";
|
||||||
import {OrganizationManagement} from "@/features/organizations/admin-organization-management";
|
import {OrganizationManagement} from "@/features/organizations/components/admin-organization-management";
|
||||||
import {buildOrganizationWithMembers} from "@/utils/common";
|
import {buildOrganizationWithMembers} from "@/utils/common";
|
||||||
import {isUUID} from "@/utils/text";
|
import {isUUID} from "@/utils/text";
|
||||||
import {user} from "@/db/schema/02_user";
|
import {user} from "@/db/schema/02_user";
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {PageParams} from "@/types/next";
|
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 {db} from "@/db";
|
||||||
import {
|
import {
|
||||||
AdminOrganizationAddModal
|
AdminOrganizationAddModal
|
||||||
} from "@/features/organizations/admin-organization-add-modal";
|
} from "@/features/organizations/components/admin-organization-add-modal";
|
||||||
import {AdminOrganizationList} from "@/features/organizations/admin-organization-list";
|
import {AdminOrganizationList} from "@/features/organizations/components/admin-organization-list";
|
||||||
import {isNull} from "drizzle-orm";
|
import {isNull} from "drizzle-orm";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {PageParams} from "@/types/next";
|
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 {db} from "@/db";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
import {notFound} from "next/navigation";
|
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 {desc, isNull} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
import {PageParams} from "@/types/next";
|
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 {db} from "@/db";
|
||||||
import {desc, isNull} from "drizzle-orm";
|
import {desc, isNull} from "drizzle-orm";
|
||||||
import {AdminUserList} from "@/features/users/admin-user-list";
|
import {AdminUserList} from "@/features/users/components/admin-user-list";
|
||||||
import {AdminUserAddModal} from "@/features/users/admin-user-add-modal";
|
import {AdminUserAddModal} from "@/features/users/components/admin-user-add-modal";
|
||||||
import {SUPPORTED_PROVIDERS} from "@/lib/auth/config";
|
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<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
const users = await db.query.user.findMany({
|
const [settings, users] = await Promise.all([
|
||||||
|
getSettings(),
|
||||||
|
db.query.user.findMany({
|
||||||
where: (fields) => isNull(fields.deletedAt),
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
with: {
|
with: { accounts: true },
|
||||||
accounts: true
|
|
||||||
},
|
|
||||||
orderBy: (fields) => desc(fields.createdAt),
|
orderBy: (fields) => desc(fields.createdAt),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const avatarUrls = Object.fromEntries(
|
||||||
|
users.map((u) => [u.id, resolveAvatarUrl(u, settings)])
|
||||||
|
);
|
||||||
|
|
||||||
});
|
|
||||||
const organizations = await db.query.organization.findMany({
|
const organizations = await db.query.organization.findMany({
|
||||||
with: {
|
with: {
|
||||||
members: true,
|
members: true,
|
||||||
@@ -38,7 +45,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="flex flex-col gap-5">
|
<PageContent className="flex flex-col gap-5">
|
||||||
<AdminUserList users={users} isPasswordAuthEnabled={isPasswordAuthEnabled}/>
|
<AdminUserList users={users} isPasswordAuthEnabled={isPasswordAuthEnabled} avatarUrls={avatarUrls}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import {
|
|||||||
PageContent,
|
PageContent,
|
||||||
PageDescription,
|
PageDescription,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
} from "@/features/layout/page";
|
} from "@/features/layout/components/page";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {eq, isNull} from "drizzle-orm";
|
import {eq, isNull} from "drizzle-orm";
|
||||||
import { notFound } from "next/navigation";
|
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 { capitalizeFirstLetter } from "@/utils/text";
|
||||||
import { generateEdgeKey } from "@/utils/edge_key";
|
import { generateEdgeKey } from "@/utils/edge_key";
|
||||||
import { getServerUrl } from "@/utils/get-server-url";
|
import { getServerUrl } from "@/utils/get-server-url";
|
||||||
import { AgentContentPage } from "@/features/agents/agent-content";
|
import { AgentContentPage } from "@/features/agents/components/agent-content";
|
||||||
import { AgentDialog } from "@/features/agents/agent-dialog";
|
import { AgentDialog } from "@/features/agents/components/agent-dialog";
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(
|
export default async function RoutePage(
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {PageParams} from "@/types/next";
|
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 {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 {notFound} from "next/navigation";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {and, desc, eq, isNull, not} from "drizzle-orm";
|
import {and, desc, eq, isNull, not} from "drizzle-orm";
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
import {AgentDialog} from "@/features/agents/agent-dialog";
|
import {AgentDialog} from "@/features/agents/components/agent-dialog";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Agents",
|
title: "Agents",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {PageParams} from "@/types/next";
|
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 {Metadata} from "next";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {notificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
import {notificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
import {desc, isNull} from "drizzle-orm";
|
import {desc, isNull} from "drizzle-orm";
|
||||||
import {ChannelsSection} from "@/features/channel/channels-section";
|
import {ChannelsSection} from "@/features/channel/components/channels-section";
|
||||||
import {ChannelAddEditModal} from "@/features/channel/channel-add-edit-modal";
|
import {ChannelAddEditModal} from "@/features/channel/components/channel-add-edit-modal";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {PageParams} from "@/types/next";
|
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 {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 {notFound} from "next/navigation";
|
||||||
import {getNotificationHistory} from "@/db/services/notification-log";
|
import {getNotificationHistory} from "@/db/services/notification-log";
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import {PageParams} from "@/types/next";
|
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 {Metadata} from "next";
|
||||||
import {ChannelsSection} from "@/features/channel/channels-section";
|
import {ChannelsSection} from "@/features/channel/components/channels-section";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {desc, eq, isNull} from "drizzle-orm";
|
import {desc, eq, isNull} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
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 = {
|
export const metadata: Metadata = {
|
||||||
title: "Storage Channels",
|
title: "Storage Channels",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {PageParams} from "@/types/next";
|
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 {notFound} from "next/navigation";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getOrganization} from "@/lib/auth/auth";
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {MigrationTool} from "@/features/migration/migration-tool";
|
import {MigrationTool} from "@/features/migration/components/migration-tool";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Projects",
|
title: "Projects",
|
||||||
|
|||||||
+56
-41
@@ -1,24 +1,26 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {notFound, redirect} from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import {Page} from "@/features/layout/page";
|
import { Page } from "@/features/layout/components/page";
|
||||||
import {db} from "@/db";
|
import { db } from "@/db";
|
||||||
import {eq, and, inArray} from "drizzle-orm";
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {getOrganizationProjectDatabases} from "@/db/services/project";
|
import { getOrganizationProjectDatabases } from "@/db/services/project";
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import { getActiveMember, getOrganization } from "@/lib/auth/auth";
|
||||||
import {BackupModalProvider} from "@/features/database/backup-modal-context";
|
import { BackupModalProvider } from "@/features/database/components/backup-modal-context";
|
||||||
import {DatabaseContent} from "@/features/database/database-content";
|
import { DatabaseContent } from "@/features/database/components/database-content";
|
||||||
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
|
import { getHealthLast12hLogs } from "@/db/services/healthcheck";
|
||||||
import {LogsModalProvider} from "@/features/logs/logs-modal-context";
|
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;
|
projectId: string;
|
||||||
databaseId: string
|
databaseId: string;
|
||||||
}>) {
|
}>,
|
||||||
const {projectId, databaseId} = await props.params;
|
) {
|
||||||
|
const { projectId, databaseId } = await props.params;
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
const activeMember = await getActiveMember()
|
const activeMember = await getActiveMember();
|
||||||
|
|
||||||
if (!organization || !activeMember) {
|
if (!organization || !activeMember) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -26,17 +28,21 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
|
|
||||||
const databasesProject = await getOrganizationProjectDatabases({
|
const databasesProject = await getOrganizationProjectDatabases({
|
||||||
organizationSlug: organization.slug,
|
organizationSlug: organization.slug,
|
||||||
projectId: projectId
|
projectId: projectId,
|
||||||
})
|
});
|
||||||
|
|
||||||
const dbItem = await db.query.database.findFirst({
|
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)),
|
where: and(
|
||||||
|
inArray(drizzleDb.schemas.backup.id, databasesProject.ids ?? []),
|
||||||
|
eq(drizzleDb.schemas.database.id, databaseId),
|
||||||
|
eq(drizzleDb.schemas.database.projectId, projectId),
|
||||||
|
),
|
||||||
with: {
|
with: {
|
||||||
project: true,
|
project: true,
|
||||||
retentionPolicy: true,
|
retentionPolicy: true,
|
||||||
alertPolicies: true,
|
alertPolicies: true,
|
||||||
storagePolicies: true,
|
storagePolicies: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!dbItem) {
|
if (!dbItem) {
|
||||||
@@ -49,52 +55,61 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
restorations: true,
|
restorations: true,
|
||||||
storages: {
|
storages: {
|
||||||
with: {
|
with: {
|
||||||
storageChannel: true
|
storageChannel: true,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
logs: true
|
|
||||||
},
|
},
|
||||||
orderBy: (b, {desc}) => [desc(b.createdAt)],
|
logs: true,
|
||||||
|
},
|
||||||
|
orderBy: (b, { desc }) => [desc(b.createdAt)],
|
||||||
});
|
});
|
||||||
|
|
||||||
const restorations = await db.query.restoration.findMany({
|
const restorations = await db.query.restoration.findMany({
|
||||||
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
|
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
|
||||||
with: {
|
with: {
|
||||||
logs: true
|
logs: true,
|
||||||
},
|
},
|
||||||
orderBy: (r, {desc}) => [desc(r.createdAt)],
|
orderBy: (r, { desc }) => [desc(r.createdAt)],
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAlreadyBackup = backups.some((b) => b.status === "waiting" || b.status === "ongoing");
|
//const isAlreadyBackup = backups.some((b) => b.status === "waiting" || b.status === "ongoing");
|
||||||
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
||||||
|
|
||||||
const totalBackups = await db.select({count: drizzleDb.schemas.backup.id})
|
const totalBackups = await db
|
||||||
|
.select({ count: drizzleDb.schemas.backup.id })
|
||||||
.from(drizzleDb.schemas.backup)
|
.from(drizzleDb.schemas.backup)
|
||||||
.where(eq(drizzleDb.schemas.backup.databaseId, dbItem.id))
|
.where(eq(drizzleDb.schemas.backup.databaseId, dbItem.id))
|
||||||
.then(rows => rows.length);
|
.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})
|
const successfulBackups = await db
|
||||||
|
.select({ count: drizzleDb.schemas.backup.id })
|
||||||
.from(drizzleDb.schemas.backup)
|
.from(drizzleDb.schemas.backup)
|
||||||
.where(and(
|
.where(
|
||||||
|
and(
|
||||||
eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
|
eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
|
||||||
eq(drizzleDb.schemas.backup.status, "success")
|
eq(drizzleDb.schemas.backup.status, "success"),
|
||||||
))
|
),
|
||||||
.then(rows => rows.length);
|
)
|
||||||
|
.then((rows) => rows.length);
|
||||||
|
|
||||||
|
const [settings] = await db
|
||||||
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
.select()
|
||||||
|
.from(drizzleDb.schemas.setting)
|
||||||
|
.where(eq(drizzleDb.schemas.setting.name, "system"))
|
||||||
|
.limit(1);
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
notFound();
|
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 successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
//const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
const isMember = activeMember?.role === "member";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
|
|||||||
@@ -1,35 +1,31 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {Page, PageContent, PageTitle} from "@/features/layout/page";
|
import { Page, PageContent, PageTitle } from "@/features/layout/components/page";
|
||||||
import {
|
import { ButtonDeleteProject } from "@/features/projects/components/project-delete-button";
|
||||||
ButtonDeleteProject
|
import { CardsWithPagination } from "@/components/common/cards-with-pagination";
|
||||||
} from "@/features/projects/project-delete-button";
|
import { ProjectDatabaseCard } from "@/features/projects/components/project-database-card";
|
||||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import {ProjectDatabaseCard} from "@/features/projects/project-database-card";
|
import { db } from "@/db";
|
||||||
import {notFound, redirect} from "next/navigation";
|
import { eq } from "drizzle-orm";
|
||||||
import {db} from "@/db";
|
import { getActiveMember, getOrganization } from "@/lib/auth/auth";
|
||||||
import {eq} from "drizzle-orm";
|
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {capitalizeFirstLetter} from "@/utils/text";
|
import { capitalizeFirstLetter, isUUID } from "@/utils/text";
|
||||||
import {ProjectDialog} from "@/features/projects/project-dialog";
|
import { ProjectDialog } from "@/features/projects/components/project-dialog";
|
||||||
import {ProjectWith} from "@/db/schema/06_project";
|
import { ProjectWith } from "@/db/schema/06_project";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import { getOrganizationAvailableDatabases } from "@/db/services/database";
|
||||||
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<{
|
if (!isUUID(projectId)) {
|
||||||
projectId: string
|
notFound();
|
||||||
}>) {
|
|
||||||
const {
|
|
||||||
projectId
|
|
||||||
} = await props.params;
|
|
||||||
|
|
||||||
if (!isUuidv4(projectId)) {
|
|
||||||
notFound()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
const activeMember = await getActiveMember()
|
const activeMember = await getActiveMember();
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -41,11 +37,12 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
if (!org) notFound();
|
if (!org) notFound();
|
||||||
|
|
||||||
const proj = await db.query.project.findFirst({
|
const proj = await db.query.project.findFirst({
|
||||||
where: (proj, {
|
where: (proj, { and, eq, not }) =>
|
||||||
and,
|
and(
|
||||||
eq,
|
eq(proj.id, projectId),
|
||||||
not
|
eq(proj.organizationId, org.id),
|
||||||
}) => and(eq(proj.id, projectId), eq(proj.organizationId, org.id), not(eq(proj.isArchived, true))),
|
not(eq(proj.isArchived, true)),
|
||||||
|
),
|
||||||
with: {
|
with: {
|
||||||
databases: true,
|
databases: true,
|
||||||
},
|
},
|
||||||
@@ -55,7 +52,10 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
redirect("/dashboard/projects");
|
redirect("/dashboard/projects");
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableDatabases = await getOrganizationAvailableDatabases(organization.id, proj.id)
|
const availableDatabases = await getOrganizationAvailableDatabases(
|
||||||
|
organization.id,
|
||||||
|
proj.id,
|
||||||
|
);
|
||||||
const isMember = activeMember?.role === "member";
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -76,7 +76,10 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
<ButtonDeleteProject
|
||||||
|
projectId={projectId}
|
||||||
|
text={"Delete Project"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -85,8 +88,10 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
<PageContent className="flex flex-col w-full h-full">
|
<PageContent className="flex flex-col w-full h-full">
|
||||||
{proj.databases.length > 0 ? (
|
{proj.databases.length > 0 ? (
|
||||||
<CardsWithPagination
|
<CardsWithPagination
|
||||||
data={[...proj.databases].sort((a, b) =>
|
data={[...proj.databases].sort(
|
||||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
(a, b) =>
|
||||||
|
new Date(b.createdAt).getTime() -
|
||||||
|
new Date(a.createdAt).getTime(),
|
||||||
)}
|
)}
|
||||||
organizationSlug={organization.slug}
|
organizationSlug={organization.slug}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -99,7 +104,9 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground py-20">
|
<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-lg font-medium">No databases found</p>
|
||||||
<p className="text-sm mt-2">You haven’t added any databases to this project yet.</p>
|
<p className="text-sm mt-2">
|
||||||
|
You haven’t added any databases to this project yet.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
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 {ProjectCard} from "@/features/projects/project-card";
|
import {ProjectCard} from "@/features/projects/components/project-card";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {EmptyStatePlaceholder} from "@/components/common/empty-state-placeholder";
|
import {EmptyStatePlaceholder} from "@/components/common/empty-state-placeholder";
|
||||||
import {Metadata} from "next";
|
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 {DatabaseWith} from "@/db/schema/07_database";
|
||||||
import {getOrganizationAvailableDatabases} from "@/db/services/database";
|
import {getOrganizationAvailableDatabases} from "@/db/services/database";
|
||||||
|
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import {
|
|||||||
PageContent,
|
PageContent,
|
||||||
PageDescription,
|
PageDescription,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
} from "@/features/layout/page";
|
} from "@/features/layout/components/page";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {notFound} from "next/navigation";
|
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 {capitalizeFirstLetter} from "@/utils/text";
|
||||||
import {generateEdgeKey} from "@/utils/edge_key";
|
import {generateEdgeKey} from "@/utils/edge_key";
|
||||||
import {getServerUrl} from "@/utils/get-server-url";
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
import {AgentContentPage} from "@/features/agents/agent-content";
|
import {AgentContentPage} from "@/features/agents/components/agent-content";
|
||||||
import {AgentDialog} from "@/features/agents/agent-dialog";
|
import {AgentDialog} from "@/features/agents/components/agent-dialog";
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import {
|
|||||||
PageContent,
|
PageContent,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
} from "@/features/layout/page";
|
} from "@/features/layout/components/page";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {Metadata} from "next";
|
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 {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||||
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
||||||
import {DeleteOrganizationButton} from "@/features/organizations/organization-delete-button";
|
import {DeleteOrganizationButton} from "@/features/organizations/components/organization-delete-button";
|
||||||
import {EditOrganizationDialog} from "@/features/organizations/organization-edit-dialog";
|
import {EditOrganizationDialog} from "@/features/organizations/components/organization-edit-dialog";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {isNull} from "drizzle-orm";
|
import {isNull} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {PageParams} from "@/types/next";
|
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 {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
import {EvolutionLineChart} from "@/features/statistics/evolution-line-chart";
|
import {EvolutionLineChart} from "@/features/statistics/components/evolution-line-chart";
|
||||||
import {PercentageLineChart} from "@/features/statistics/percentage-line-chart";
|
import {PercentageLineChart} from "@/features/statistics/components/percentage-line-chart";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {and, asc, count, eq, inArray} from "drizzle-orm";
|
import {and, asc, count, eq, inArray} from "drizzle-orm";
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import {PageParams} from "@/types/next";
|
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 {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
import {Building2, Database, DatabaseBackup, Folder, RefreshCcw, Server, Workflow} from "lucide-react";
|
import {Building2, Database, DatabaseBackup, Folder, RefreshCcw, Server, Workflow} from "lucide-react";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {db} from "@/db";
|
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 * as drizzleDb from "@/db";
|
||||||
import {listOrganizations} from "@/lib/auth/auth";
|
import {listOrganizations} from "@/lib/auth/auth";
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
@@ -23,7 +24,9 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
|
|
||||||
const organizationIds = organizations.map(project => project.id);
|
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({
|
const projects = await db.query.project.findMany({
|
||||||
where: and(inArray(drizzleDb.schemas.project.organizationId, organizationIds), eq(drizzleDb.schemas.project.isArchived, false)),
|
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 { redirect } from "next/navigation";
|
||||||
|
|
||||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||||
import { AppSidebar } from "@/features/layout/app-sidebar";
|
import { AppSidebar } from "@/features/layout/components/app-sidebar";
|
||||||
import { Header } from "@/features/layout/header";
|
import { Header } from "@/features/layout/components/header";
|
||||||
import { currentUser } from "@/lib/auth/current-user";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
import { ThemeMetaUpdater } from "@/features/theme/theme-meta-updater";
|
import { isOnboardingDone } from "@/db/services/setting";
|
||||||
import { ModeToggle } from "@/features/theme/mode-toggle";
|
import { env } from "@/env.mjs";
|
||||||
import { UpdateNotification } from "@/features/updates/update-notification";
|
import { ModeToggle } from "@/features/theme/components/mode-toggle";
|
||||||
|
import { UpdateNotification } from "@/features/updates/components/update-notification";
|
||||||
|
|
||||||
export default async function Layout({ children }: { children: ReactNode }) {
|
export default async function Layout({ children }: { children: ReactNode }) {
|
||||||
|
if (env.SKIP_ONBOARDING !== "true" && !(await isOnboardingDone())) {
|
||||||
|
redirect("/welcome");
|
||||||
|
}
|
||||||
|
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
if (!user) redirect("/login");
|
if (!user) redirect("/login");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<div className="flex flex-col lg:flex-row w-full">
|
<div className="flex flex-col lg:flex-row w-full">
|
||||||
<ThemeMetaUpdater />
|
|
||||||
<AppSidebar updateNotification={<UpdateNotification />} />
|
<AppSidebar updateNotification={<UpdateNotification />} />
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<Header actions={<ModeToggle />} />
|
<Header actions={<ModeToggle />} />
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { redirect } from "next/navigation";
|
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 { currentUser } from "@/lib/auth/current-user";
|
||||||
|
import { isOnboardingDone } from "@/db/services/setting";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
export default async function Index() {
|
export default async function Index() {
|
||||||
|
if (env.SKIP_ONBOARDING !== "true" && !(await isOnboardingDone())) {
|
||||||
|
redirect("/welcome");
|
||||||
|
}
|
||||||
|
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
if (user) {
|
if (user) {
|
||||||
const currentOrganizationSlug = await getCurrentOrganizationSlug();
|
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 {Backup} from "@/db/schema/07_database";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
import {eventEmitter} from "@/lib/event";
|
import {eventEmitter} from "@/lib/event";
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers";
|
import {sendNotificationsBackupRestore} from "@/features/notifications/utils/notifications.helpers";
|
||||||
import {EventKind} from "@/features/notifications/notifications.types";
|
import {EventKind} from "@/features/notifications/types";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
import {JobLogEntry} from "@/features/logs/types";
|
import {JobLogEntry} from "@/features/logs/types";
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,43 @@
|
|||||||
import {NextResponse} from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import {and, eq} from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import { db } from "@/db";
|
||||||
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
import { getDatabaseOrThrow, withAgentCheck } from "../../helpers";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import { eventEmitter } from "@/lib/event";
|
||||||
import {eventEmitter} from "@/lib/event";
|
import { logger } from "@/lib/logger";
|
||||||
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 = {
|
export type Body = {
|
||||||
generatedId: string
|
generatedId: string;
|
||||||
storageChannelId: string
|
storageChannelId: string;
|
||||||
backupId: string
|
backupId: string;
|
||||||
}
|
};
|
||||||
export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
export const POST = withAgentCheck(
|
||||||
params: Promise<{ agentId: string }>,
|
async (
|
||||||
agent: any
|
request: Request,
|
||||||
}) => {
|
{
|
||||||
|
params,
|
||||||
|
agent,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ agentId: string }>;
|
||||||
|
agent: any;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
try {
|
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 generatedId = body.generatedId;
|
||||||
const storageChannelId = body.storageChannelId;
|
const storageChannelId = body.storageChannelId;
|
||||||
const backupId = body.backupId;
|
const backupId = body.backupId;
|
||||||
|
|
||||||
if (!generatedId || !isUuidv4(generatedId)) {
|
if (!generatedId || !isUUID(generatedId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "generatedId is not a valid UUID"},
|
{ error: "generatedId is not a valid UUID" },
|
||||||
{status: 400}
|
{ status: 400 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,8 +52,8 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
|
|
||||||
if (!backup) {
|
if (!backup) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Unable to find the corresponding backup"},
|
{ error: "Unable to find the corresponding backup" },
|
||||||
{status: 404}
|
{ status: 404 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,21 +66,21 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
eventEmitter.emit("modification", { update: true });
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
message: "Backup storage successfully created",
|
message: "Backup storage successfully created",
|
||||||
backupStorage: backupStorage
|
backupStorage: backupStorage,
|
||||||
},
|
},
|
||||||
{status: 200}
|
{ status: 200 },
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error({error: error}, "Error in POST for INIT backup");
|
log.error({ error: error }, "Error in POST for INIT backup");
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Internal server error"},
|
{ error: "Internal server error" },
|
||||||
{status: 500}
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,73 +1,86 @@
|
|||||||
import {NextResponse} from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db as dbClient, db} from "@/db";
|
import { db as dbClient, db } from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers";
|
import { sendNotificationsBackupRestore } from "@/features/notifications/utils/notifications.helpers";
|
||||||
import {logger} from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import { withUpdatedAt } from "@/db/utils";
|
||||||
import {JobLogEntry} from "@/features/logs/types";
|
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 = {
|
export type BodyResultRestore = {
|
||||||
generatedId: string
|
generatedId: string;
|
||||||
status: string
|
status: string;
|
||||||
logs: JobLogEntry[]
|
logs: JobLogEntry[];
|
||||||
durationMs: number
|
durationMs: number;
|
||||||
}
|
};
|
||||||
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
|
type RestorationStatus = "waiting" | "ongoing" | "failed" | "success";
|
||||||
|
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: Request,
|
request: Request,
|
||||||
{params}: { params: Promise<{ agentId: string }> }
|
{ params }: { params: Promise<{ agentId: string }> },
|
||||||
) {
|
) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const agentId = (await params).agentId;
|
||||||
const agentId = (await params).agentId
|
|
||||||
const body: BodyResultRestore = await request.json();
|
const body: BodyResultRestore = await request.json();
|
||||||
|
|
||||||
|
if (!isUUID(body.generatedId)) {
|
||||||
if (!isUuidv4(body.generatedId)) {
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "generatedId is not a valid uuid"},
|
{ error: "generatedId is not a valid uuid" },
|
||||||
{status: 500}
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({
|
const agent = await db.query.agent.findFirst({
|
||||||
where: and(eq(drizzleDb.schemas.agent.id, agentId), eq(drizzleDb.schemas.agent.isArchived, false)),
|
where: and(
|
||||||
})
|
eq(drizzleDb.schemas.agent.id, agentId),
|
||||||
|
eq(drizzleDb.schemas.agent.isArchived, false),
|
||||||
|
),
|
||||||
|
});
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
return NextResponse.json({error: "Agent not found"}, {status: 404})
|
return NextResponse.json({ error: "Agent not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const database = await db.query.database.findFirst({
|
const database = await db.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.agentDatabaseId, body.generatedId),
|
where: eq(drizzleDb.schemas.database.agentDatabaseId, body.generatedId),
|
||||||
with: {
|
with: {
|
||||||
alertPolicies: true
|
alertPolicies: true,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!database) {
|
if (!database) {
|
||||||
return NextResponse.json({error: "Database associated with generatedId provided not found"}, {status: 404})
|
return NextResponse.json(
|
||||||
|
{ error: "Database associated with generatedId provided not found" },
|
||||||
|
{ status: 404 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const restoration = await db.query.restoration.findFirst({
|
const restoration = await db.query.restoration.findFirst({
|
||||||
where: and(eq(drizzleDb.schemas.restoration.status, "ongoing"), eq(drizzleDb.schemas.restoration.databaseId, database.id),)
|
where: and(
|
||||||
})
|
eq(drizzleDb.schemas.restoration.status, "ongoing"),
|
||||||
|
eq(drizzleDb.schemas.restoration.databaseId, database.id),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
if (!restoration) {
|
if (!restoration) {
|
||||||
return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404})
|
return NextResponse.json(
|
||||||
|
{ error: "Unable to fin the corresponding restoration" },
|
||||||
|
{ status: 404 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [restorationUpdated] = await db
|
const [restorationUpdated] = await db
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set(withUpdatedAt({status: body.status as RestorationStatus, durationMs: body.durationMs}))
|
.set(
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id)).returning();
|
withUpdatedAt({
|
||||||
|
status: body.status as RestorationStatus,
|
||||||
|
durationMs: body.durationMs,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
const logsToInsert = body.logs.map((entry) => ({
|
const logsToInsert = body.logs.map((entry) => ({
|
||||||
backupId: null,
|
backupId: null,
|
||||||
@@ -87,24 +100,25 @@ export async function POST(
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
if (logsToInsert.length > 0) {
|
if (logsToInsert.length > 0) {
|
||||||
await dbClient
|
await dbClient.insert(drizzleDb.schemas.jobLog).values(logsToInsert);
|
||||||
.insert(drizzleDb.schemas.jobLog)
|
|
||||||
.values(logsToInsert);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore");
|
await sendNotificationsBackupRestore(
|
||||||
|
database,
|
||||||
|
body.status == "failed" ? "error_restore" : "success_restore",
|
||||||
|
);
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
status: true,
|
status: true,
|
||||||
message: "Restoration successfully updated"
|
message: "Restoration successfully updated",
|
||||||
}
|
};
|
||||||
|
|
||||||
return Response.json(response, {status: 200})
|
return Response.json(response, { status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error({error: error}, "Error in POST handler")
|
log.error({ error: error }, "Error in POST handler");
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: 'Internal server error'},
|
{ error: "Internal server error" },
|
||||||
{status: 500}
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {Body} from "./route";
|
import {Body} from "./route";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
|
||||||
import {Agent} from "@/db/schema/08_agent";
|
import {Agent} from "@/db/schema/08_agent";
|
||||||
import {DatabaseWith} from "@/db/schema/07_database";
|
import {DatabaseWith} from "@/db/schema/07_database";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db, db as dbClient} 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 {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
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 {Setting} from "@/db/schema/01_setting";
|
||||||
import {logger} from "@/lib/logger";
|
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"});
|
const log = logger.child({module: "api/agent/status/helpers"});
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
let backupSize: number | null = null
|
let backupSize: number | null = null
|
||||||
|
|
||||||
if (!existingDatabase) {
|
if (!existingDatabase) {
|
||||||
if (!isUuidv4(db.generatedId)) {
|
if (!isUUID(db.generatedId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "generatedId is not a valid uuid"},
|
{error: "generatedId is not a valid uuid"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
@@ -123,7 +123,11 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
where: and(
|
where: and(
|
||||||
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
|
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
|
||||||
inArray(drizzleDb.schemas.backup.status, ["waiting", "ongoing"])
|
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({
|
const restoration = await dbClient.query.restoration.findFirst({
|
||||||
|
|||||||
@@ -1,99 +1,107 @@
|
|||||||
import {NextResponse} from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import {handleDatabases} from "./helpers";
|
import { handleDatabases } from "./helpers";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import { db } from "@/db";
|
||||||
import {EDbmsSchema} from "@/db/schema/types";
|
import { EDbmsSchema } from "@/db/schema/types";
|
||||||
import {and, eq} from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import { withUpdatedAt } from "@/db/utils";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import { logger } from "@/lib/logger";
|
||||||
import {logger} from "@/lib/logger";
|
import { isUUID } from "@/utils/text";
|
||||||
|
|
||||||
|
|
||||||
const log = logger.child({module: "api/agent/status/route"});
|
|
||||||
|
|
||||||
|
const log = logger.child({ module: "api/agent/status/route" });
|
||||||
|
|
||||||
export type databaseAgent = {
|
export type databaseAgent = {
|
||||||
name: string,
|
name: string;
|
||||||
dbms: EDbmsSchema,
|
dbms: EDbmsSchema;
|
||||||
generatedId: string
|
generatedId: string;
|
||||||
pingStatus: boolean
|
pingStatus: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Body = {
|
export type Body = {
|
||||||
version: string,
|
version: string;
|
||||||
databases: databaseAgent[]
|
databases: databaseAgent[];
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: Request,
|
request: Request,
|
||||||
{params}: { params: Promise<{ agentId: string }> }
|
{ params }: { params: Promise<{ agentId: string }> },
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const agentId = (await params).agentId
|
const agentId = (await params).agentId;
|
||||||
log.debug(`Agent ID: ${agentId}`)
|
log.debug(`Agent ID: ${agentId}`);
|
||||||
const body: Body = await request.json();
|
const body: Body = await request.json();
|
||||||
const lastContact = new Date();
|
const lastContact = new Date();
|
||||||
let message: string
|
let message: string;
|
||||||
|
|
||||||
if (!isUuidv4(agentId)) {
|
if (!isUUID(agentId)) {
|
||||||
message = "agentId is not a valid uuid"
|
message = "agentId is not a valid uuid";
|
||||||
log.error({error: message}, "An error occurred")
|
log.error({ error: message }, "An error occurred");
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "agentId is not a valid uuid"},
|
{ error: "agentId is not a valid uuid" },
|
||||||
{status: 500}
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({
|
const agent = await db.query.agent.findFirst({
|
||||||
where: and(eq(drizzleDb.schemas.agent.id, agentId), eq(drizzleDb.schemas.agent.isArchived, false)),
|
where: and(
|
||||||
})
|
eq(drizzleDb.schemas.agent.id, agentId),
|
||||||
|
eq(drizzleDb.schemas.agent.isArchived, false),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
message = "Agent not found"
|
message = "Agent not found";
|
||||||
return NextResponse.json({error: message}, {status: 404})
|
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);
|
const [settings] = await db
|
||||||
|
.select()
|
||||||
|
.from(drizzleDb.schemas.setting)
|
||||||
|
.where(eq(drizzleDb.schemas.setting.name, "system"))
|
||||||
|
.limit(1);
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
return NextResponse.json({error: "An error occured"}, {status: 404})
|
return NextResponse.json({ error: "An error occured" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const databasesResponse = await handleDatabases(body, agent, lastContact, settings)
|
const databasesResponse = await handleDatabases(
|
||||||
|
body,
|
||||||
|
agent,
|
||||||
|
lastContact,
|
||||||
|
settings,
|
||||||
|
);
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(drizzleDb.schemas.agent)
|
.update(drizzleDb.schemas.agent)
|
||||||
.set(withUpdatedAt({
|
.set(
|
||||||
|
withUpdatedAt({
|
||||||
version: body.version,
|
version: body.version,
|
||||||
lastContact: lastContact,
|
lastContact: lastContact,
|
||||||
healthErrorCount: null
|
healthErrorCount: null,
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.where(eq(drizzleDb.schemas.agent.id, agentId));
|
.where(eq(drizzleDb.schemas.agent.id, agentId));
|
||||||
|
|
||||||
await db
|
await db.insert(drizzleDb.schemas.healthcheckLog).values({
|
||||||
.insert(drizzleDb.schemas.healthcheckLog)
|
|
||||||
.values({
|
|
||||||
kind: "agent",
|
kind: "agent",
|
||||||
status: "success",
|
status: "success",
|
||||||
objectId: agentId,
|
objectId: agentId,
|
||||||
date: lastContact
|
date: lastContact,
|
||||||
})
|
});
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
agent: {
|
agent: {
|
||||||
id: agentId,
|
id: agentId,
|
||||||
lastContact: lastContact
|
lastContact: lastContact,
|
||||||
},
|
},
|
||||||
databases: databasesResponse
|
databases: databasesResponse,
|
||||||
}
|
};
|
||||||
|
|
||||||
return Response.json(response)
|
return Response.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error({error: error}, "Error in POST handler")
|
log.error({ error: error }, "Error in POST handler");
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: 'Internal server error'},
|
{ error: "Internal server error" },
|
||||||
{status: 500}
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { headers } from "next/headers";
|
|||||||
|
|
||||||
const authHandler = toNextJsHandler(auth.handler);
|
const authHandler = toNextJsHandler(auth.handler);
|
||||||
|
|
||||||
async function blockApiKeyCreateForRestrictedUsers(req: NextRequest): Promise<NextResponse | null> {
|
async function blockApiKeyCreateForRestrictedUsers(
|
||||||
|
req: NextRequest,
|
||||||
|
): Promise<NextResponse | null> {
|
||||||
const url = req.nextUrl;
|
const url = req.nextUrl;
|
||||||
if (req.method !== "POST" || !url.pathname.endsWith("/api-key/create")) {
|
if (req.method !== "POST" || !url.pathname.endsWith("/api-key/create")) {
|
||||||
return null;
|
return null;
|
||||||
@@ -18,7 +20,7 @@ async function blockApiKeyCreateForRestrictedUsers(req: NextRequest): Promise<Ne
|
|||||||
if (session.user.banned || (session.user.role as string) === "pending") {
|
if (session.user.banned || (session.user.role as string) === "pending") {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Account not eligible to create API keys" },
|
{ error: "Account not eligible to create API keys" },
|
||||||
{ status: 403 }
|
{ status: 403 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import type {StorageInput} from "@/features/storages/storages.types";
|
import type {StorageInput} from "@/features/storages/types";
|
||||||
import {dispatchStorage} from "@/features/storages/storages.dispatch";
|
import {dispatchStorage} from "@/features/storages/utils/storages.dispatch";
|
||||||
import {Readable} from "node:stream";
|
import {Readable} from "node:stream";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {auth} from "@/lib/auth/auth";
|
import {auth} from "@/lib/auth/auth";
|
||||||
import {headers} from "next/headers";
|
import {headers} from "next/headers";
|
||||||
import {StorageInput} from "@/features/storages/storages.types";
|
import {StorageInput} from "@/features/storages/types";
|
||||||
import {dispatchStorage} from "@/features/storages/storages.dispatch";
|
import {dispatchStorage} from "@/features/storages/utils/storages.dispatch";
|
||||||
import {Readable} from "node:stream";
|
import {Readable} from "node:stream";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
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 {env} from "@/env.mjs";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
const log = logger.child({module: "api/tus/hooks"});
|
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) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
@@ -24,17 +29,61 @@ export async function POST(request: Request) {
|
|||||||
event.Upload.Offset === event.Upload.Size
|
event.Upload.Offset === event.Upload.Size
|
||||||
) {
|
) {
|
||||||
const id = event.Upload.ID;
|
const id = event.Upload.ID;
|
||||||
const fileName = headers["X-File-Name"]?.[0];
|
|
||||||
const filePath = headers["X-File-Path"]?.[0];
|
const filePath = headers["X-File-Path"]?.[0];
|
||||||
|
|
||||||
if (!filePath) {
|
if (!filePath || !FILE_PATH_RE.test(filePath)) {
|
||||||
return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
|
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 uploadDir = path.join(env.PRIVATE_PATH!, "/uploads/");
|
||||||
|
|
||||||
const oldFilePath = path.join(uploadDir, "tmp", id);
|
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});
|
fs.mkdirSync(path.dirname(newFilePath), {recursive: true});
|
||||||
|
|
||||||
@@ -74,7 +123,7 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
return NextResponse.json({});
|
return NextResponse.json({});
|
||||||
} catch (error) {
|
} 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});
|
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 {logger} from "@/lib/logger";
|
||||||
import {ApiKeyContext} from "@/lib/api-v1/types";
|
import {ApiKeyContext} from "@/lib/api-v1/types";
|
||||||
import {getAgent, resolveAgentAccess} from "@/lib/api-v1/services/agents";
|
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]"});
|
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 { inArray, eq, and, or, isNull } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { logger } from "@/lib/logger";
|
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 { ActionError } from "@/lib/safe-actions/actions";
|
||||||
import {getAccessibleAgentIds} from "@/lib/api-v1/services/agents";
|
import {getAccessibleAgentIds} from "@/lib/api-v1/services/agents";
|
||||||
import {ApiKeyContext} from "@/lib/api-v1/types";
|
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() {
|
export default async function NotFound() {
|
||||||
return (
|
return (
|
||||||
@@ -12,7 +13,9 @@ export default async function NotFound() {
|
|||||||
The content you are trying to view is not available.
|
The content you are trying to view is not available.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<BackButton>Go home</BackButton>
|
<Button asChild>
|
||||||
|
<Link href="/">Go home</Link>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+9
-4
@@ -5,8 +5,8 @@ import { type PropsWithChildren, Suspense } from "react";
|
|||||||
|
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { ErrorLayout } from "@/components/common/error-layout";
|
import { ErrorLayout } from "@/components/common/error-layout";
|
||||||
import { ThemeMetaUpdaterRoot } from "@/features/theme/theme-meta-updater-root";
|
import { ThemeMetaUpdater } from "@/features/theme/components/theme-meta-updater";
|
||||||
import { ThemeProvider } from "@/features/theme/theme-provider";
|
import { ThemeProvider } from "@/features/theme/components/theme-provider";
|
||||||
|
|
||||||
export type ProviderProps = PropsWithChildren<{}>;
|
export type ProviderProps = PropsWithChildren<{}>;
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@@ -14,8 +14,13 @@ const queryClient = new QueryClient();
|
|||||||
export const Providers = (props: ProviderProps) => {
|
export const Providers = (props: ProviderProps) => {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
<ThemeProvider
|
||||||
<ThemeMetaUpdaterRoot />
|
attribute="class"
|
||||||
|
defaultTheme="light"
|
||||||
|
enableSystem
|
||||||
|
storageKey="portabase_theme"
|
||||||
|
>
|
||||||
|
<ThemeMetaUpdater />
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ErrorLayout>
|
<ErrorLayout>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
# build:
|
build:
|
||||||
# context: .
|
context: .
|
||||||
# dockerfile: docker/dockerfile/Dockerfile
|
dockerfile: docker/dockerfile/Dockerfile
|
||||||
# target: prod
|
target: prod
|
||||||
image: portabase/portabase:1
|
# image: portabase/portabase:1
|
||||||
ports:
|
ports:
|
||||||
- '8887:80'
|
- "8887:80"
|
||||||
environment:
|
environment:
|
||||||
TZ: "Europe/Paris"
|
TZ: "Europe/Paris"
|
||||||
env_file:
|
env_file:
|
||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
- POSTGRES_USER=devuser
|
- POSTGRES_USER=devuser
|
||||||
- POSTGRES_PASSWORD=changeme
|
- POSTGRES_PASSWORD=changeme
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "pg_isready -U devuser -d devdb" ]
|
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
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;
|
# 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:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
azurite-data:
|
azurite-data:
|
||||||
|
gcs-data:
|
||||||
|
gcs-storage:
|
||||||
|
rustfs-data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
portabase:
|
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 \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
bash \
|
bash \
|
||||||
tzdata \
|
tzdata \
|
||||||
libc6 \
|
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 \
|
build-essential \
|
||||||
g++ \
|
g++ \
|
||||||
make \
|
make \
|
||||||
@@ -13,25 +35,9 @@ RUN apt-get update && apt-get install -y \
|
|||||||
automake \
|
automake \
|
||||||
libtool \
|
libtool \
|
||||||
git \
|
git \
|
||||||
nginx \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& 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
|
FROM --platform=$BUILDPLATFORM golang:1.23-bookworm AS tusd-base
|
||||||
|
|
||||||
ARG TUSD_VERSION=2.8.0
|
ARG TUSD_VERSION=2.8.0
|
||||||
@@ -51,7 +57,7 @@ FROM scratch AS tusd-dist
|
|||||||
COPY --from=tusd-base /tusddist/tusd /tusd
|
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
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
|
||||||
@@ -83,7 +89,7 @@ COPY . .
|
|||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
|
||||||
FROM base AS prod
|
FROM runtime-base AS prod
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NODE_ENV=production
|
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/standalone ./
|
||||||
COPY --from=builder --chown=1001:1001 /app/.next/static ./.next/static
|
COPY --from=builder --chown=1001:1001 /app/.next/static ./.next/static
|
||||||
COPY --chown=1001:1001 src/db ./src/db
|
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
|
COPY --from=tusd-dist /tusd /usr/local/bin/tusd
|
||||||
RUN chmod +x /usr/local/bin/tusd
|
RUN chmod +x /usr/local/bin/tusd
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ http {
|
|||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location = /api/tus/hooks {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:3000;
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
|
||||||
|
|||||||
+14
-16
@@ -1,11 +1,10 @@
|
|||||||
import type {NextConfig} from "next";
|
import type { NextConfig } from "next";
|
||||||
import {PORTABASE_DEFAULT_SETTINGS} from "./portabase.config";
|
import { PORTABASE_DEFAULT_SETTINGS } from "./portabase.config";
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === "development";
|
const isDev = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
|
|
||||||
function buildCSPHeader(): string {
|
function buildCSPHeader(): string {
|
||||||
const {CSP} = PORTABASE_DEFAULT_SETTINGS.SECURITY;
|
const { CSP } = PORTABASE_DEFAULT_SETTINGS.SECURITY;
|
||||||
|
|
||||||
const directives = [
|
const directives = [
|
||||||
`default-src ${CSP.DEFAULT_SRC.join(" ")}`,
|
`default-src ${CSP.DEFAULT_SRC.join(" ")}`,
|
||||||
@@ -37,9 +36,9 @@ function buildPermissionsPolicy(): string {
|
|||||||
.join(", ");
|
.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
devIndicators: false,
|
||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
@@ -50,7 +49,7 @@ const nextConfig: NextConfig = {
|
|||||||
serverActions: {
|
serverActions: {
|
||||||
bodySizeLimit: "10gb",
|
bodySizeLimit: "10gb",
|
||||||
},
|
},
|
||||||
proxyClientMaxBodySize: '10gb',
|
proxyClientMaxBodySize: "10gb",
|
||||||
},
|
},
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
if (!isDev) return [];
|
if (!isDev) return [];
|
||||||
@@ -77,22 +76,21 @@ const nextConfig: NextConfig = {
|
|||||||
value: buildPermissionsPolicy(),
|
value: buildPermissionsPolicy(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'X-Content-Type-Options',
|
key: "X-Content-Type-Options",
|
||||||
value: 'nosniff',
|
value: "nosniff",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'X-Frame-Options',
|
key: "X-Frame-Options",
|
||||||
value: 'DENY',
|
value: "DENY",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Referrer-Policy',
|
key: "Referrer-Policy",
|
||||||
value: 'strict-origin-when-cross-origin',
|
value: "strict-origin-when-cross-origin",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Strict-Transport-Security',
|
key: "Strict-Transport-Security",
|
||||||
value: 'max-age=63072000; includeSubDomains; preload',
|
value: "max-age=63072000; includeSubDomains; preload",
|
||||||
}
|
},
|
||||||
// ...other security headers
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
+9
-38
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.20.2",
|
"version": "1.21.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 8887",
|
"dev": "next dev --turbopack -p 8887",
|
||||||
@@ -21,55 +21,44 @@
|
|||||||
"@better-auth/core": "1.6.11",
|
"@better-auth/core": "1.6.11",
|
||||||
"@better-auth/passkey": "1.6.11",
|
"@better-auth/passkey": "1.6.11",
|
||||||
"@better-auth/sso": "1.6.11",
|
"@better-auth/sso": "1.6.11",
|
||||||
|
"@google-cloud/storage": "^7.19.0",
|
||||||
"@hookform/resolvers": "^5.4.0",
|
"@hookform/resolvers": "^5.4.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
|
"@onboardjs/react": "1.0.0-rc.5",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@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-avatar": "^1.1.11",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@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-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@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-icons": "^1.3.2",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@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-popover": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
"@radix-ui/react-radio-group": "^1.3.8",
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slider": "^1.3.6",
|
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@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": "^1.1.10",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@react-email/components": "^0.0.41",
|
"@react-email/components": "^0.0.41",
|
||||||
"@t3-oss/env-nextjs": "^0.13.11",
|
"@t3-oss/env-nextjs": "^0.13.11",
|
||||||
"@tanstack/react-query": "^5.100.14",
|
"@tanstack/react-query": "^5.100.14",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@types/nodemailer": "^6.4.23",
|
"@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",
|
"better-auth": "1.6.11",
|
||||||
|
"canvas-confetti": "^1.9.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.3.0",
|
"date-fns": "^4.3.0",
|
||||||
"dockerode": "^4.0.12",
|
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
"drizzle-orm": "0.45.2",
|
"drizzle-orm": "0.45.2",
|
||||||
"drizzle-zod": "0.8.3",
|
"drizzle-zod": "0.8.3",
|
||||||
"embla-carousel-react": "^8.6.0",
|
|
||||||
"googleapis": "^170.1.0",
|
"googleapis": "^170.1.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.553.0",
|
||||||
@@ -79,33 +68,22 @@
|
|||||||
"next-safe-action": "^7.10.8",
|
"next-safe-action": "^7.10.8",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"node-forge": "^1.4.0",
|
|
||||||
"nodemailer": "8.0.5",
|
"nodemailer": "8.0.5",
|
||||||
"npm-check-updates": "^18.3.1",
|
|
||||||
"pg": "^8.21.0",
|
"pg": "^8.21.0",
|
||||||
"pino": "^10.3.1",
|
"pino": "^10.3.1",
|
||||||
"pino-pretty": "^13.1.3",
|
|
||||||
"prettier": "^3.8.3",
|
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-day-picker": "9.7.0",
|
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"react-dropzone": "^14.4.1",
|
"react-dropzone": "^14.4.1",
|
||||||
"react-email": "^4.3.2",
|
"react-email": "^4.3.2",
|
||||||
"react-hook-form": "^7.76.1",
|
"react-hook-form": "^7.76.1",
|
||||||
"react-qr-code": "^2.0.21",
|
"react-qr-code": "^2.0.21",
|
||||||
"react-resizable-panels": "^3.0.6",
|
|
||||||
"react-twc": "^1.5.1",
|
"react-twc": "^1.5.1",
|
||||||
"react-use-measure": "^2.1.7",
|
|
||||||
"recharts": "^2.15.4",
|
"recharts": "^2.15.4",
|
||||||
|
"server-only": "^0.0.1",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"socket.io": "^4.8.3",
|
|
||||||
"socket.io-client": "^4.8.3",
|
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"swiper": "^12.1.4",
|
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"uuid": "^11.1.1",
|
"uuid": "^11.1.1",
|
||||||
"vaul": "^1.1.2",
|
|
||||||
"ws": "^8.21.0",
|
|
||||||
"zod": "4.3.6"
|
"zod": "4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -115,29 +93,22 @@
|
|||||||
"@release-it/bumper": "^7.0.5",
|
"@release-it/bumper": "^7.0.5",
|
||||||
"@release-it/conventional-changelog": "^10.0.6",
|
"@release-it/conventional-changelog": "^10.0.6",
|
||||||
"@tailwindcss/postcss": "^4.3.0",
|
"@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": "^22.19.19",
|
||||||
"@types/node-forge": "^1.3.14",
|
|
||||||
"@types/pg": "^8.20.0",
|
"@types/pg": "^8.20.0",
|
||||||
"@types/react": "^19.2.15",
|
"@types/react": "^19.2.15",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@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",
|
"drizzle-kit": "^0.31.10",
|
||||||
"esbuild": "^0.27.7",
|
|
||||||
"eslint": "^9.39.4",
|
"eslint": "^9.39.4",
|
||||||
"eslint-config-next": "^16.2.6",
|
"eslint-config-next": "^16.2.6",
|
||||||
"eslint-plugin-tailwindcss": "^3.18.3",
|
|
||||||
"framer-motion": "^12.40.0",
|
"framer-motion": "^12.40.0",
|
||||||
"node-pty": "^1.1.0",
|
"pino-pretty": "^13.1.3",
|
||||||
"postcss": "8.5.10",
|
"postcss": "8.5.10",
|
||||||
|
"postcss-load-config": "^6.0.1",
|
||||||
"release-it": "^19.2.4",
|
"release-it": "^19.2.4",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
"tsx": "^4.22.3",
|
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3"
|
||||||
"zenstack": "2.14.2"
|
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.8.0+sha512.c1f5e7c4cb241c8f174b743851d82f42b802324afc8b0f116b96adb15aa06664948dde36960a3ba1079ba5b4b29dd0140135b94b5b5f5263592249d68e555f26"
|
"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://api.iconify.design",
|
||||||
"https://code.iconify.design",
|
"https://code.iconify.design",
|
||||||
"https://api.github.com",
|
"https://api.github.com",
|
||||||
|
"https://api.dicebear.com",
|
||||||
],
|
],
|
||||||
OBJECT_SRC: ["'none'"],
|
OBJECT_SRC: ["'none'"],
|
||||||
BASE_URI: ["'self'"],
|
BASE_URI: ["'self'"],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { errorHandler } from "@/middleware/errorHandler";
|
|||||||
import { auth } from "@/lib/auth/auth";
|
import { auth } from "@/lib/auth/auth";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { env } from "@/env.mjs";
|
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) {
|
export async function proxy(request: NextRequest) {
|
||||||
const url = request.nextUrl.clone();
|
const url = request.nextUrl.clone();
|
||||||
@@ -19,7 +19,7 @@ export async function proxy(request: NextRequest) {
|
|||||||
new URL(`/login?redirect=${redirectUrl}`, request.url),
|
new URL(`/login?redirect=${redirectUrl}`, request.url),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const user = session.user as User
|
const user = session.user as User;
|
||||||
|
|
||||||
if (user.banned) {
|
if (user.banned) {
|
||||||
await auth.api.signOut({ headers: await headers() });
|
await auth.api.signOut({ headers: await headers() });
|
||||||
@@ -104,6 +104,7 @@ function checkRouteExists(pathname: string) {
|
|||||||
/^\/api\/config\/?$/,
|
/^\/api\/config\/?$/,
|
||||||
/^\/api\/health\/?$/,
|
/^\/api\/health\/?$/,
|
||||||
/^\/api\/google\/drive\/callback\/?$/,
|
/^\/api\/google\/drive\/callback\/?$/,
|
||||||
|
/^\/api\/avatar\/?$/,
|
||||||
// v1 external API
|
// v1 external API
|
||||||
/^\/api\/v1\/mcp\/?$/,
|
/^\/api\/v1\/mcp\/?$/,
|
||||||
/^\/api\/v1\/docs\/?$/,
|
/^\/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,
|
"clientRole" : true,
|
||||||
"containerId" : "18b5dfb2-44d4-448e-98df-9652daef1a22",
|
"containerId" : "18b5dfb2-44d4-448e-98df-9652daef1a22",
|
||||||
"attributes" : { }
|
"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",
|
"id" : "b1cedcac-5207-4479-bc29-065ba849e414",
|
||||||
"type" : "password",
|
"type" : "password",
|
||||||
"userLabel" : "My password",
|
"userLabel" : "My password",
|
||||||
"createdDate" : 1772179391727,
|
"createdDate" : 1782393421785,
|
||||||
"secretData" : "{\"value\":\"IXr6L3pvO2BAkn/cH3rh3v2tg1R83u2f8SWEZK2fq5E=\",\"salt\":\"XWV8/yKm7Rw/YXiwXZZUig==\",\"additionalParameters\":{}}",
|
"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\"]}}"
|
"credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
|
||||||
} ],
|
} ],
|
||||||
"disableableCredentialTypes" : [ ],
|
"disableableCredentialTypes" : [ ],
|
||||||
@@ -467,6 +475,23 @@
|
|||||||
"realmRoles" : [ "admin", "default-roles-master" ],
|
"realmRoles" : [ "admin", "default-roles-master" ],
|
||||||
"notBefore" : 0,
|
"notBefore" : 0,
|
||||||
"groups" : [ "/Admin" ]
|
"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",
|
"id" : "718dfe46-dbef-41a3-9120-78aeac240431",
|
||||||
"username" : "service-account-portabase",
|
"username" : "service-account-portabase",
|
||||||
@@ -625,6 +650,57 @@
|
|||||||
"nodeReRegistrationTimeout" : 0,
|
"nodeReRegistrationTimeout" : 0,
|
||||||
"defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ],
|
"defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ],
|
||||||
"optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ]
|
"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",
|
"id" : "6022c206-5f1f-45b7-af07-74ce6804cbc7",
|
||||||
"clientId" : "master-realm",
|
"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 * as React from "react";
|
||||||
// import EmailLayout from "./email-layout";
|
// import EmailLayout from "./email-layout";
|
||||||
// import {Text, Section, Button} from "@react-email/components";
|
// 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 {
|
// export interface EmailNotificationProps {
|
||||||
// payload: EventPayload
|
// payload: EventPayload
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import EmailLayout from "./email-layout";
|
import EmailLayout from "./email-layout";
|
||||||
import {Text, Section, Button} from "@react-email/components";
|
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 {
|
export interface EmailNotificationProps {
|
||||||
payload: EventPayload;
|
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,
|
"when": 1781960184847,
|
||||||
"tag": "0064_orange_richard_fisk",
|
"tag": "0064_orange_richard_fisk",
|
||||||
"breakpoints": true
|
"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')
|
defaultStorageChannelId: uuid('default_storage_channel_id')
|
||||||
.references(() => storageChannel.id, {onDelete: "set null"}),
|
.references(() => storageChannel.id, {onDelete: "set null"}),
|
||||||
encryption: boolean("encryption").default(false),
|
encryption: boolean("encryption").default(false),
|
||||||
|
onboarding: boolean("onboarding").default(false).notNull(),
|
||||||
...timestamps
|
...timestamps
|
||||||
});
|
});
|
||||||
|
|
||||||
export const settingRelations = relations(setting, ({one, many}) => ({
|
export const settingRelations = relations(setting, ({one}) => ({
|
||||||
storageChannel: one(storageChannel, {fields: [setting.defaultStorageChannelId], references: [storageChannel.id]}),
|
storageChannel: one(storageChannel, {fields: [setting.defaultStorageChannelId], references: [storageChannel.id]}),
|
||||||
notificationChannel: one(notificationChannel, {fields: [setting.defaultNotificationChannelId], references: [notificationChannel.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 {createSelectSchema} from "drizzle-zod";
|
||||||
import {z} from "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', {
|
export const storageChannel = pgTable('storage_channel', {
|
||||||
id: uuid("id").defaultRandom().primaryKey(),
|
id: uuid("id").defaultRandom().primaryKey(),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {pgEnum} from "drizzle-orm/pg-core";
|
|||||||
import {createSelectSchema} from "drizzle-zod";
|
import {createSelectSchema} from "drizzle-zod";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
|
|
||||||
export const dbmsEnum = pgEnum("dbms_status", ["postgresql", "mysql", "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 statusEnum = pgEnum("status", ["waiting", "ongoing", "failed", "success"]);
|
||||||
export const typeStorageEnum = pgEnum("type_storage", ["local", "s3"]);
|
export const typeStorageEnum = pgEnum("type_storage", ["local", "s3"]);
|
||||||
|
|
||||||
|
|||||||
+11
-13
@@ -1,11 +1,12 @@
|
|||||||
import {and, desc, eq, sql} from "drizzle-orm";
|
"use server";
|
||||||
import {db} from "@/db";
|
|
||||||
import {Agent, agent, organizationAgent} from "@/db/schema/08_agent";
|
import { and, desc, eq, sql } from "drizzle-orm";
|
||||||
import {Database, database} from "@/db/schema/07_database";
|
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) {
|
export async function getOrganizationAgents(organizationId: string) {
|
||||||
|
return (await db
|
||||||
return await db
|
|
||||||
.select({
|
.select({
|
||||||
id: agent.id,
|
id: agent.id,
|
||||||
name: agent.name,
|
name: agent.name,
|
||||||
@@ -27,17 +28,14 @@ export async function getOrganizationAgents(organizationId: string) {
|
|||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
.from(organizationAgent)
|
.from(organizationAgent)
|
||||||
.innerJoin(
|
.innerJoin(agent, eq(organizationAgent.agentId, agent.id))
|
||||||
agent,
|
|
||||||
eq(organizationAgent.agentId, agent.id)
|
|
||||||
)
|
|
||||||
.leftJoin(database, eq(database.agentId, agent.id))
|
.leftJoin(database, eq(database.agentId, agent.id))
|
||||||
.groupBy(agent.id)
|
.groupBy(agent.id)
|
||||||
.orderBy(desc(agent.createdAt))
|
.orderBy(desc(agent.createdAt))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(organizationAgent.organizationId, organizationId),
|
eq(organizationAgent.organizationId, organizationId),
|
||||||
eq(agent.isArchived, false)
|
eq(agent.isArchived, false),
|
||||||
)
|
),
|
||||||
) as unknown as Agent[];
|
)) as unknown as Agent[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use server"
|
"use server";
|
||||||
import {eq} from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import { db } from "@/db";
|
||||||
|
|
||||||
export async function getDatabaseBackups(databaseId: string) {
|
export async function getDatabaseBackups(databaseId: string) {
|
||||||
return await db.query.backup.findMany({
|
return await db.query.backup.findMany({
|
||||||
@@ -14,6 +14,6 @@ export async function getDatabaseBackups(databaseId: string) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: (b, {desc}) => [desc(b.createdAt)],
|
orderBy: (b, { desc }) => [desc(b.createdAt)],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-16
@@ -1,15 +1,21 @@
|
|||||||
"use server"
|
"use server";
|
||||||
import {db} from "@/db";
|
import { inArray } from "drizzle-orm";
|
||||||
import {DatabaseWith} from "@/db/schema/07_database";
|
import { db } from "@/db";
|
||||||
import {AgentWith} from "@/db/schema/08_agent";
|
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(
|
export async function getOrganizationAvailableDatabases(
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
projectId?: string
|
projectId?: string,
|
||||||
) {
|
) {
|
||||||
|
const availableDatabases = (await db.query.database.findMany({
|
||||||
const availableDatabases = (
|
|
||||||
await db.query.database.findMany({
|
|
||||||
where: (db, { eq, or, isNull }) =>
|
where: (db, { eq, or, isNull }) =>
|
||||||
projectId
|
projectId
|
||||||
? or(isNull(db.projectId), eq(db.projectId, projectId))
|
? or(isNull(db.projectId), eq(db.projectId, projectId))
|
||||||
@@ -17,23 +23,101 @@ export async function getOrganizationAvailableDatabases(
|
|||||||
with: {
|
with: {
|
||||||
agent: {
|
agent: {
|
||||||
with: {
|
with: {
|
||||||
organizations: true
|
organizations: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
project: true,
|
project: true,
|
||||||
backups: true,
|
backups: true,
|
||||||
restorations: true,
|
restorations: true,
|
||||||
},
|
},
|
||||||
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
orderBy: (db, { desc }) => [desc(db.createdAt)],
|
||||||
})
|
})) as DatabaseWith[];
|
||||||
) as DatabaseWith[];
|
|
||||||
|
|
||||||
return availableDatabases.filter(db => {
|
return availableDatabases.filter((db) => {
|
||||||
const agent = db.agent as AgentWith;
|
const agent = db.agent as AgentWith;
|
||||||
if (agent?.isArchived) return false;
|
if (agent?.isArchived) return false;
|
||||||
return (
|
return (
|
||||||
agent?.organizationId === organizationId ||
|
agent?.organizationId === organizationId ||
|
||||||
agent?.organizations?.some(org => org.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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import {db} from "@/db";
|
"use server";
|
||||||
|
|
||||||
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {and, eq, gte, isNotNull, lt} from "drizzle-orm";
|
import { and, eq, gte, isNotNull, lt } from "drizzle-orm";
|
||||||
import {dispatchNotification} from "@/features/notifications/notifications.dispatch";
|
import { dispatchNotification } from "@/features/notifications/utils/notifications.dispatch";
|
||||||
import {EventPayload} from "@/features/notifications/notifications.types";
|
import { EventPayload } from "@/features/notifications/types";
|
||||||
import {logger} from "@/lib/logger";
|
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 }) {
|
export async function getHealthLast12hLogs({ id }: { id: string }) {
|
||||||
const now = new Date()
|
const now = new Date();
|
||||||
const since = new Date(now.getTime() - 12 * 60 * 60 * 1000)
|
const since = new Date(now.getTime() - 12 * 60 * 60 * 1000);
|
||||||
|
|
||||||
return db
|
return db
|
||||||
.select()
|
.select()
|
||||||
@@ -17,31 +19,30 @@ export async function getHealthLast12hLogs({id}: { id: string }) {
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(drizzleDb.schemas.healthcheckLog.objectId, id),
|
eq(drizzleDb.schemas.healthcheckLog.objectId, id),
|
||||||
gte(drizzleDb.schemas.healthcheckLog.date, since)
|
gte(drizzleDb.schemas.healthcheckLog.date, since),
|
||||||
)
|
),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteHealthLogsOlderThan12h() {
|
export async function deleteHealthLogsOlderThan12h() {
|
||||||
const now = new Date()
|
const now = new Date();
|
||||||
const threshold = new Date(now.getTime() - 12 * 60 * 60 * 1000)
|
const threshold = new Date(now.getTime() - 12 * 60 * 60 * 1000);
|
||||||
|
|
||||||
const logsToDelete = await db
|
const logsToDelete = await db
|
||||||
.select()
|
.select()
|
||||||
.from(drizzleDb.schemas.healthcheckLog)
|
.from(drizzleDb.schemas.healthcheckLog)
|
||||||
.where(
|
.where(lt(drizzleDb.schemas.healthcheckLog.date, threshold));
|
||||||
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
|
await db
|
||||||
.delete(drizzleDb.schemas.healthcheckLog)
|
.delete(drizzleDb.schemas.healthcheckLog)
|
||||||
.where(
|
.where(lt(drizzleDb.schemas.healthcheckLog.date, threshold));
|
||||||
lt(drizzleDb.schemas.healthcheckLog.date, threshold)
|
|
||||||
)
|
|
||||||
|
|
||||||
return logsToDelete.length
|
return logsToDelete.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkAgentsHealthError() {
|
export async function checkAgentsHealthError() {
|
||||||
@@ -50,7 +51,7 @@ export async function checkAgentsHealthError() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const settings = await db.query.setting.findFirst({
|
const settings = await db.query.setting.findFirst({
|
||||||
where: (fields, {eq}) => eq(fields.name, "system"),
|
where: (fields, { eq }) => eq(fields.name, "system"),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
@@ -58,8 +59,11 @@ export async function checkAgentsHealthError() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!settings.defaultNotificationChannelId) {
|
if (!settings.defaultNotificationChannelId) {
|
||||||
log.error({name: "checkAgentsHealthError"},`No default notification channel id found.`)
|
log.error(
|
||||||
return
|
{ name: "checkAgentsHealthError" },
|
||||||
|
`No default notification channel id found.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -72,9 +76,9 @@ export async function checkAgentsHealthError() {
|
|||||||
|
|
||||||
if (diffMinutes > 10) {
|
if (diffMinutes > 10) {
|
||||||
if ((agent.healthErrorCount ?? 0) < 3) {
|
if ((agent.healthErrorCount ?? 0) < 3) {
|
||||||
|
const newHealthErrorCount = (agent.healthErrorCount ?? 0) + 1;
|
||||||
const newHealthErrorCount = (agent.healthErrorCount ?? 0) + 1
|
await db
|
||||||
await db.update(drizzleDb.schemas.agent)
|
.update(drizzleDb.schemas.agent)
|
||||||
.set({
|
.set({
|
||||||
healthErrorCount: newHealthErrorCount,
|
healthErrorCount: newHealthErrorCount,
|
||||||
})
|
})
|
||||||
@@ -91,31 +95,30 @@ export async function checkAgentsHealthError() {
|
|||||||
error: "Agent is down",
|
error: "Agent is down",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
log.info({name: "checkAgentsHealthError", payload: payload},`Agent Healthcheck Notification`)
|
log.info(
|
||||||
|
{ name: "checkAgentsHealthError", payload: payload },
|
||||||
|
`Agent Healthcheck Notification`,
|
||||||
|
);
|
||||||
|
|
||||||
await dispatchNotification(
|
await dispatchNotification(
|
||||||
payload,
|
payload,
|
||||||
undefined,
|
undefined,
|
||||||
settings.defaultNotificationChannelId,
|
settings.defaultNotificationChannelId,
|
||||||
undefined
|
undefined,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function checkDatabasesHealthError() {
|
export async function checkDatabasesHealthError() {
|
||||||
|
|
||||||
const databases = await db.query.database.findMany({
|
const databases = await db.query.database.findMany({
|
||||||
where: isNotNull(drizzleDb.schemas.database.lastContact),
|
where: isNotNull(drizzleDb.schemas.database.lastContact),
|
||||||
with: {
|
with: {
|
||||||
agent: true,
|
agent: true,
|
||||||
alertPolicies: true
|
alertPolicies: true,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
@@ -127,9 +130,9 @@ export async function checkDatabasesHealthError() {
|
|||||||
|
|
||||||
if (diffMinutes > 10) {
|
if (diffMinutes > 10) {
|
||||||
if ((database.healthErrorCount ?? 0) < 3) {
|
if ((database.healthErrorCount ?? 0) < 3) {
|
||||||
|
const newHealthErrorCount = (database.healthErrorCount ?? 0) + 1;
|
||||||
const newHealthErrorCount = (database.healthErrorCount ?? 0) + 1
|
await db
|
||||||
await db.update(drizzleDb.schemas.database)
|
.update(drizzleDb.schemas.database)
|
||||||
.set({
|
.set({
|
||||||
healthErrorCount: newHealthErrorCount,
|
healthErrorCount: newHealthErrorCount,
|
||||||
})
|
})
|
||||||
@@ -141,24 +144,30 @@ export async function checkDatabasesHealthError() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const defaultPolicy = settings?.notificationChannel
|
const defaultPolicy = settings?.notificationChannel
|
||||||
? [{
|
? [
|
||||||
|
{
|
||||||
id: null,
|
id: null,
|
||||||
notificationChannelId: settings.notificationChannel.id,
|
notificationChannelId: settings.notificationChannel.id,
|
||||||
enabled: settings.notificationChannel.enabled,
|
enabled: settings.notificationChannel.enabled,
|
||||||
eventKinds: ["error_health_database"]
|
eventKinds: ["error_health_database"],
|
||||||
}]
|
},
|
||||||
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const policiesToUse = (database.alertPolicies && database.alertPolicies.length > 0)
|
const policiesToUse =
|
||||||
? database.alertPolicies.filter(policy => policy.enabled && policy.eventKinds.includes("error_health_database"))
|
database.alertPolicies && database.alertPolicies.length > 0
|
||||||
|
? database.alertPolicies.filter(
|
||||||
|
(policy) =>
|
||||||
|
policy.enabled &&
|
||||||
|
policy.eventKinds.includes("error_health_database"),
|
||||||
|
)
|
||||||
: defaultPolicy;
|
: defaultPolicy;
|
||||||
|
|
||||||
if (!policiesToUse || policiesToUse.length === 0) {
|
if (!policiesToUse || policiesToUse.length === 0) {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises = policiesToUse.map(alertPolicy => {
|
const promises = policiesToUse.map((alertPolicy) => {
|
||||||
|
|
||||||
const payload: EventPayload = {
|
const payload: EventPayload = {
|
||||||
title: "Database down",
|
title: "Database down",
|
||||||
message: `Database ${database.name} is down, (notification number: ${newHealthErrorCount}/3)`,
|
message: `Database ${database.name} is down, (notification number: ${newHealthErrorCount}/3)`,
|
||||||
@@ -171,9 +180,17 @@ export async function checkDatabasesHealthError() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
log.info({name: "checkDatabasesHealthError", payload: payload},`Database Healthcheck Notification`)
|
log.info(
|
||||||
|
{ name: "checkDatabasesHealthError", payload: payload },
|
||||||
|
`Database Healthcheck Notification`,
|
||||||
|
);
|
||||||
|
|
||||||
return dispatchNotification(payload, alertPolicy.id == null ? undefined : alertPolicy.id, alertPolicy.id ? undefined : alertPolicy.notificationChannelId, undefined);
|
return dispatchNotification(
|
||||||
|
payload,
|
||||||
|
alertPolicy.id == null ? undefined : alertPolicy.id,
|
||||||
|
alertPolicy.id ? undefined : alertPolicy.notificationChannelId,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
@@ -181,5 +198,3 @@ export async function checkDatabasesHealthError() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import {desc, eq} from "drizzle-orm";
|
"use server";
|
||||||
import {db} from "@/db";
|
|
||||||
|
import { desc, eq, isNull } from "drizzle-orm";
|
||||||
|
import { db } from "@/db";
|
||||||
import {
|
import {
|
||||||
NotificationChannel,
|
NotificationChannel,
|
||||||
notificationChannel,
|
notificationChannel,
|
||||||
organizationNotificationChannel
|
organizationNotificationChannel,
|
||||||
} from "@/db/schema/09_notification-channel";
|
} from "@/db/schema/09_notification-channel";
|
||||||
import {storageChannel} from "@/db/schema/12_storage-channel";
|
|
||||||
|
|
||||||
export async function getOrganizationChannels(organizationId: string) {
|
export async function getOrganizationChannels(
|
||||||
return await db
|
organizationId: string,
|
||||||
|
): Promise<NotificationChannel[]> {
|
||||||
|
const [orgChannels, systemChannels] = await Promise.all([
|
||||||
|
db
|
||||||
.select({
|
.select({
|
||||||
id: notificationChannel.id,
|
id: notificationChannel.id,
|
||||||
name: notificationChannel.name,
|
name: notificationChannel.name,
|
||||||
@@ -18,13 +22,41 @@ export async function getOrganizationChannels(organizationId: string) {
|
|||||||
updatedAt: notificationChannel.updatedAt,
|
updatedAt: notificationChannel.updatedAt,
|
||||||
createdAt: notificationChannel.createdAt,
|
createdAt: notificationChannel.createdAt,
|
||||||
deletedAt: notificationChannel.deletedAt,
|
deletedAt: notificationChannel.deletedAt,
|
||||||
organizationId: notificationChannel.organizationId
|
organizationId: notificationChannel.organizationId,
|
||||||
})
|
})
|
||||||
.from(organizationNotificationChannel)
|
.from(organizationNotificationChannel)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
notificationChannel,
|
notificationChannel,
|
||||||
eq(organizationNotificationChannel.notificationChannelId, notificationChannel.id)
|
eq(
|
||||||
|
organizationNotificationChannel.notificationChannelId,
|
||||||
|
notificationChannel.id,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.orderBy(desc(notificationChannel.createdAt))
|
.orderBy(desc(notificationChannel.createdAt))
|
||||||
.where(eq(organizationNotificationChannel.organizationId, organizationId)) as unknown as NotificationChannel[];
|
.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,8 +1,13 @@
|
|||||||
import {and, desc, eq, gte, lte} from 'drizzle-orm';
|
"use server";
|
||||||
import {NotificationLevel, notificationLog} from "@/db/schema/11_notification-log";
|
|
||||||
import {notificationChannel} from "@/db/schema/09_notification-channel";
|
import { and, desc, eq, gte, lte } from "drizzle-orm";
|
||||||
import {db} from "@/db";
|
import {
|
||||||
import {Json} from "drizzle-zod";
|
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 = {
|
export type NotificationLogWithRelations = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -15,7 +20,7 @@ export type NotificationLogWithRelations = {
|
|||||||
content: {
|
content: {
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
},
|
};
|
||||||
channel: {
|
channel: {
|
||||||
name: string;
|
name: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
@@ -25,8 +30,7 @@ export type NotificationLogWithRelations = {
|
|||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getNotificationHistory(
|
export async function getNotificationHistory(filters?: {
|
||||||
filters?: {
|
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
policyId?: string;
|
policyId?: string;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
@@ -35,14 +39,17 @@ export async function getNotificationHistory(
|
|||||||
from?: Date;
|
from?: Date;
|
||||||
to?: Date;
|
to?: Date;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}): Promise<NotificationLogWithRelations[]> {
|
||||||
): Promise<NotificationLogWithRelations[]> {
|
|
||||||
const where = [];
|
const where = [];
|
||||||
if (filters?.channelId) where.push(eq(notificationLog.channelId, filters.channelId));
|
if (filters?.channelId)
|
||||||
if (filters?.policyId) where.push(eq(notificationLog.policyId, filters.policyId));
|
where.push(eq(notificationLog.channelId, filters.channelId));
|
||||||
if (filters?.organizationId) where.push(eq(notificationLog.organizationId, filters.organizationId));
|
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 (filters?.level) where.push(eq(notificationLog.level, filters.level));
|
||||||
if (typeof filters?.success === 'boolean') where.push(eq(notificationLog.success, filters.success));
|
if (typeof filters?.success === "boolean")
|
||||||
|
where.push(eq(notificationLog.success, filters.success));
|
||||||
if (filters?.from) where.push(gte(notificationLog.sentAt, filters.from));
|
if (filters?.from) where.push(gte(notificationLog.sentAt, filters.from));
|
||||||
if (filters?.to) where.push(lte(notificationLog.sentAt, filters.to));
|
if (filters?.to) where.push(lte(notificationLog.sentAt, filters.to));
|
||||||
|
|
||||||
@@ -68,13 +75,16 @@ export async function getNotificationHistory(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.from(notificationLog)
|
.from(notificationLog)
|
||||||
.leftJoin(notificationChannel, eq(notificationLog.channelId, notificationChannel.id))
|
.leftJoin(
|
||||||
|
notificationChannel,
|
||||||
|
eq(notificationLog.channelId, notificationChannel.id),
|
||||||
|
)
|
||||||
// .leftJoin(alertPolicy, eq(notificationLog.policyId, alertPolicy.id))
|
// .leftJoin(alertPolicy, eq(notificationLog.policyId, alertPolicy.id))
|
||||||
.where(and(...where))
|
.where(and(...where))
|
||||||
.orderBy(desc(notificationLog.sentAt))
|
.orderBy(desc(notificationLog.sentAt))
|
||||||
.limit(filters?.limit || 100);
|
.limit(filters?.limit || 100);
|
||||||
|
|
||||||
return rows.map(row => ({
|
return rows.map((row) => ({
|
||||||
...row,
|
...row,
|
||||||
payload: row.payload as Json,
|
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")),
|
||||||
|
});
|
||||||
|
}
|
||||||
+30
-14
@@ -1,14 +1,28 @@
|
|||||||
import {getOrganization} from "@/lib/auth/auth";
|
"use server";
|
||||||
import {db} from "@/db";
|
|
||||||
import {and, eq} from "drizzle-orm";
|
import { getOrganization } from "@/lib/auth/auth";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
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}: {
|
export const getOrganizationProjectDatabases = async ({
|
||||||
organizationSlug: string, projectId: string
|
organizationSlug,
|
||||||
|
projectId,
|
||||||
|
}: {
|
||||||
|
organizationSlug: string;
|
||||||
|
projectId: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
@@ -20,10 +34,13 @@ export const getOrganizationProjectDatabases = async ({organizationSlug, project
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const databasesProject = await db.query.project.findFirst({
|
const databasesProject = await db.query.project.findFirst({
|
||||||
where: and(eq(drizzleDb.schemas.project.organizationId, organization.id), eq(drizzleDb.schemas.project.id, projectId)),
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.project.organizationId, organization.id),
|
||||||
|
eq(drizzleDb.schemas.project.id, projectId),
|
||||||
|
),
|
||||||
with: {
|
with: {
|
||||||
databases: true
|
databases: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!databasesProject) {
|
if (!databasesProject) {
|
||||||
@@ -37,12 +54,11 @@ export const getOrganizationProjectDatabases = async ({organizationSlug, project
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
data: databasesProject.databases,
|
data: databasesProject.databases,
|
||||||
ids: databasesProject.databases.map((project) => project.id)
|
ids: databasesProject.databases.map((project) => project.id),
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const errorMessage = e?.response?.data?.message || e?.message || "Unknown auth error";
|
const errorMessage =
|
||||||
|
e?.response?.data?.message || e?.message || "Unknown auth error";
|
||||||
const status = e?.response?.status || 500;
|
const status = e?.response?.status || 500;
|
||||||
|
|
||||||
console.error("API GettingOrganizationProjectDatabases error:", {
|
console.error("API GettingOrganizationProjectDatabases error:", {
|
||||||
|
|||||||
@@ -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,9 +1,18 @@
|
|||||||
import {desc, eq} from "drizzle-orm";
|
"use server";
|
||||||
import {db} from "@/db";
|
|
||||||
import {organizationStorageChannel, StorageChannel, storageChannel} from "@/db/schema/12_storage-channel";
|
|
||||||
|
|
||||||
export async function getOrganizationStorageChannels(organizationId: string) {
|
import { desc, eq, isNull } from "drizzle-orm";
|
||||||
return await db
|
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({
|
.select({
|
||||||
id: storageChannel.id,
|
id: storageChannel.id,
|
||||||
name: storageChannel.name,
|
name: storageChannel.name,
|
||||||
@@ -18,8 +27,31 @@ export async function getOrganizationStorageChannels(organizationId: string) {
|
|||||||
.from(organizationStorageChannel)
|
.from(organizationStorageChannel)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
storageChannel,
|
storageChannel,
|
||||||
eq(organizationStorageChannel.storageChannelId, storageChannel.id)
|
eq(organizationStorageChannel.storageChannelId, storageChannel.id),
|
||||||
)
|
)
|
||||||
.orderBy(desc(storageChannel.createdAt))
|
.orderBy(desc(storageChannel.createdAt))
|
||||||
.where(eq(organizationStorageChannel.organizationId, organizationId)) as unknown as StorageChannel[];
|
.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[];
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-9
@@ -1,10 +1,16 @@
|
|||||||
import {SignUpUser} from "@/types/auth";
|
"use server";
|
||||||
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";
|
|
||||||
|
|
||||||
|
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> {
|
export async function createUserDb(data: SignUpUser): Promise<User> {
|
||||||
assertValidPassword(data.password);
|
assertValidPassword(data.password);
|
||||||
@@ -12,7 +18,9 @@ export async function createUserDb(data: SignUpUser): Promise<User> {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
const userId = crypto.randomUUID();
|
const userId = crypto.randomUUID();
|
||||||
|
|
||||||
const [newUser] = await db.insert(drizzleDb.schemas.user).values({
|
const [newUser] = await db
|
||||||
|
.insert(drizzleDb.schemas.user)
|
||||||
|
.values({
|
||||||
...data,
|
...data,
|
||||||
id: userId,
|
id: userId,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -22,7 +30,8 @@ export async function createUserDb(data: SignUpUser): Promise<User> {
|
|||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
theme: data.theme as UserThemeEnum,
|
theme: data.theme as UserThemeEnum,
|
||||||
}).returning();
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
if (data.password) {
|
if (data.password) {
|
||||||
const hashedPassword = await hashPassword(data.password);
|
const hashedPassword = await hashPassword(data.password);
|
||||||
@@ -36,5 +45,5 @@ export async function createUserDb(data: SignUpUser): Promise<User> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return newUser
|
return newUser;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export const env = createEnv({
|
|||||||
process.env.NODE_ENV === "production" ? "0 * * * *" : "* * * * *",
|
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_ID: z.string().optional().default("oidc"),
|
||||||
AUTH_OIDC_TITLE: z.string().optional(),
|
AUTH_OIDC_TITLE: z.string().optional(),
|
||||||
@@ -88,6 +89,8 @@ export const env = createEnv({
|
|||||||
|
|
||||||
ALLOWED_GROUP: z.string().optional(),
|
ALLOWED_GROUP: z.string().optional(),
|
||||||
|
|
||||||
|
SKIP_ONBOARDING: z.string().optional().default("false"),
|
||||||
|
|
||||||
AUTH_EMAIL_PASSWORD_ENABLED: z.string().optional().default("true"),
|
AUTH_EMAIL_PASSWORD_ENABLED: z.string().optional().default("true"),
|
||||||
AUTH_SIGNUP_ENABLED: z.string().optional().default("true"),
|
AUTH_SIGNUP_ENABLED: z.string().optional().default("true"),
|
||||||
AUTH_PASSKEY_ENABLED: z.string().optional().default("false"),
|
AUTH_PASSKEY_ENABLED: z.string().optional().default("false"),
|
||||||
@@ -145,6 +148,7 @@ export const env = createEnv({
|
|||||||
|
|
||||||
RETENTION_CRON: process.env.RETENTION_CRON,
|
RETENTION_CRON: process.env.RETENTION_CRON,
|
||||||
CLEANING_HEALTHCHECK_LOGS_CRON: process.env.CLEANING_HEALTHCHECK_LOGS_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_ID: process.env.AUTH_OIDC_ID,
|
||||||
AUTH_OIDC_TITLE: process.env.AUTH_OIDC_TITLE,
|
AUTH_OIDC_TITLE: process.env.AUTH_OIDC_TITLE,
|
||||||
@@ -174,6 +178,8 @@ export const env = createEnv({
|
|||||||
|
|
||||||
ALLOWED_GROUP: process.env.ALLOWED_GROUP,
|
ALLOWED_GROUP: process.env.ALLOWED_GROUP,
|
||||||
|
|
||||||
|
SKIP_ONBOARDING: process.env.SKIP_ONBOARDING,
|
||||||
|
|
||||||
AUTH_EMAIL_PASSWORD_ENABLED: process.env.AUTH_EMAIL_PASSWORD_ENABLED,
|
AUTH_EMAIL_PASSWORD_ENABLED: process.env.AUTH_EMAIL_PASSWORD_ENABLED,
|
||||||
AUTH_SIGNUP_ENABLED: process.env.AUTH_SIGNUP_ENABLED,
|
AUTH_SIGNUP_ENABLED: process.env.AUTH_SIGNUP_ENABLED,
|
||||||
AUTH_PASSKEY_ENABLED: process.env.AUTH_PASSKEY_ENABLED,
|
AUTH_PASSKEY_ENABLED: process.env.AUTH_PASSKEY_ENABLED,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import {action, ActionError, userAction} from "@/lib/safe-actions/actions";
|
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 {z} from "zod";
|
||||||
import {eq, and, ne, count, desc} from "drizzle-orm";
|
import {eq, and, ne, count, desc} from "drizzle-orm";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
@@ -11,7 +11,7 @@ import {Badge} from "@/components/ui/badge";
|
|||||||
import {truncateWords} from "@/utils/text";
|
import {truncateWords} from "@/utils/text";
|
||||||
import {useIsMobile} from "@/hooks/use-mobile";
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
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 = {
|
export type agentCardProps = {
|
||||||
data: AgentWith;
|
data: AgentWith;
|
||||||
+4
-4
@@ -3,10 +3,10 @@
|
|||||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
import {Server} from "lucide-react";
|
import {Server} from "lucide-react";
|
||||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
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 {AgentWithDatabases} from "@/db/schema/08_agent";
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import {useQuery} from "@tanstack/react-query";
|
||||||
import {getAgentAction} from "@/features/agents/agents.action";
|
import {getAgentAction} from "@/features/agents/actions/agents.action";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
import {Separator} from "@/components/ui/separator";
|
import {Separator} from "@/components/ui/separator";
|
||||||
import {Badge} from "@/components/ui/badge";
|
import {Badge} from "@/components/ui/badge";
|
||||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
||||||
import {AgentDatabaseCard} from "@/features/agents/agent-database-card";
|
import {AgentDatabaseCard} from "@/features/agents/components/agent-database-card";
|
||||||
import {HealthCheckGraph} from "@/features/database/health-grid";
|
import {HealthCheckGraph} from "@/features/database/components/health-grid";
|
||||||
import {HealthcheckLog} from "@/db/schema/15_healthcheck-log";
|
import {HealthcheckLog} from "@/db/schema/15_healthcheck-log";
|
||||||
|
|
||||||
type AgentContentPageProps = {
|
type AgentContentPageProps = {
|
||||||
+1
-1
@@ -5,7 +5,7 @@ import {ButtonWithConfirm} from "@/components/common/button-with-confirm";
|
|||||||
import {useMutation} from "@tanstack/react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {toast} from "sonner";
|
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";
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
|
|
||||||
export type ButtonDeleteAgentProps = {
|
export type ButtonDeleteAgentProps = {
|
||||||
+2
-2
@@ -7,7 +7,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} 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 {Button, buttonVariants} from "@/components/ui/button";
|
||||||
import {Plus} from "lucide-react";
|
import {Plus} from "lucide-react";
|
||||||
import {GearIcon} from "@radix-ui/react-icons";
|
import {GearIcon} from "@radix-ui/react-icons";
|
||||||
@@ -16,7 +16,7 @@ import {useState} from "react";
|
|||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
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";
|
import {AgentWith} from "@/db/schema/08_agent";
|
||||||
|
|
||||||
type AgentDialogProps = {
|
type AgentDialogProps = {
|
||||||
@@ -15,9 +15,9 @@ import {Button} from "@/components/ui/button";
|
|||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
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 {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";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
|
|
||||||
export type agentFormProps = {
|
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