mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44f0112620 | ||
|
|
2d7a5ce264 | ||
|
|
7afcf0f76b | ||
|
|
a7b28ce84d | ||
|
|
4ef82ac3a8 | ||
|
|
f86c826d1b | ||
|
|
3de7ef1a26 | ||
|
|
1713780528 | ||
|
|
6db39b829e | ||
|
|
174ff0886c | ||
|
|
d49d8f52c4 | ||
|
|
c9552e448a | ||
|
|
47d3059d78 | ||
|
|
c3bafef6d8 | ||
|
|
1fbbc935c1 | ||
|
|
ad0e6be10d | ||
|
|
5735d47889 | ||
|
|
294bdfa296 | ||
|
|
83eb012cb5 | ||
|
|
51d165fe84 | ||
|
|
3b7eb6c606 | ||
|
|
333146c590 | ||
|
|
6adb310f0c | ||
|
|
a64fe1c08c | ||
|
|
40cdb3d0ab | ||
|
|
7a97721abf | ||
|
|
c1b71e7772 | ||
|
|
56517226b6 | ||
|
|
337d270ba6 | ||
|
|
373ab315a3 | ||
|
|
685e1fc300 | ||
|
|
274aded8fd | ||
|
|
39b6b5e116 | ||
|
|
3f52d8c76e | ||
|
|
11f814777b | ||
|
|
dd66ecdb78 | ||
|
|
ec90a34262 | ||
|
|
5a2ce4968d | ||
|
|
03659b4a15 | ||
|
|
b032c96a7c | ||
|
|
d887d44bd4 | ||
|
|
7115ce8484 | ||
|
|
96cc9822d0 | ||
|
|
68fb74f441 | ||
|
|
06ed4f3517 | ||
|
|
3a4dd10b4c | ||
|
|
0316fdc0a3 | ||
|
|
72ecd7f054 | ||
|
|
539fec4dbc | ||
|
|
3fa0f9b76d | ||
|
|
4fadfcfaf2 | ||
|
|
af3df7f72a | ||
|
|
2cd5bda416 | ||
|
|
965b215224 |
+3
-1
@@ -54,7 +54,6 @@ AUTH_OIDC_ROLE_MAP=""
|
|||||||
AUTH_ALLOW_LINKING=true
|
AUTH_ALLOW_LINKING=true
|
||||||
AUTH_ALLOW_UNLINKING=true
|
AUTH_ALLOW_UNLINKING=true
|
||||||
|
|
||||||
|
|
||||||
# Social OAuth2 Authentification
|
# Social OAuth2 Authentification
|
||||||
AUTH_SOCIAL_ID=""
|
AUTH_SOCIAL_ID=""
|
||||||
AUTH_SOCIAL_TITLE=""
|
AUTH_SOCIAL_TITLE=""
|
||||||
@@ -76,3 +75,6 @@ RETENTION_CRON="* * * * *"
|
|||||||
|
|
||||||
TRUSTED_DOMAINS="http://localhost:8887, http://localhost:3055, http://localhost:3056"
|
TRUSTED_DOMAINS="http://localhost:8887, http://localhost:3055, http://localhost:3056"
|
||||||
|
|
||||||
|
#OPENAPI_ENABLED=true
|
||||||
|
#API_ENABLED=true
|
||||||
|
#MCP_ENABLED=true
|
||||||
|
|||||||
@@ -24,50 +24,86 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
notify-discord:
|
notify-discord:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Send Discord Notification
|
- name: Send Discord Notification
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||||
|
DISCORD_TITLE: ${{ inputs.discord_title }}
|
||||||
|
DISCORD_COLOR: ${{ inputs.discord_color }}
|
||||||
|
DISCORD_FOOTER: ${{ inputs.discord_footer }}
|
||||||
run: |
|
run: |
|
||||||
RELEASE_INFO=$(gh release view "${{ inputs.release_tag }}" -R ${{ github.repository }} --json name,url,body,author)
|
set -euo pipefail
|
||||||
|
|
||||||
RELEASE_TITLE=$(echo "$RELEASE_INFO" | jq -r .name)
|
RELEASE_INFO=$(gh release view "$RELEASE_TAG" -R "${{ github.repository }}" --json name,url,body,author)
|
||||||
if [ -z "$RELEASE_TITLE" ] || [ "$RELEASE_TITLE" = "null" ]; then RELEASE_TITLE="${{ inputs.release_tag }}"; fi
|
|
||||||
|
|
||||||
RELEASE_URL=$(echo "$RELEASE_INFO" | jq -r .url)
|
RELEASE_TITLE=$(echo "$RELEASE_INFO" | jq -r '.name // empty')
|
||||||
RELEASE_BODY=$(echo "$RELEASE_INFO" | jq -r .body)
|
if [ -z "$RELEASE_TITLE" ]; then
|
||||||
|
RELEASE_TITLE="$RELEASE_TAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
RELEASE_URL=$(echo "$RELEASE_INFO" | jq -r '.url // empty')
|
||||||
|
RELEASE_BODY=$(echo "$RELEASE_INFO" | jq -r '.body // ""')
|
||||||
|
|
||||||
AUTHOR_NAME="Portabase"
|
AUTHOR_NAME="Portabase"
|
||||||
AUTHOR_ICON="https://github.com/Portabase.png"
|
AUTHOR_ICON="https://github.com/Portabase.png"
|
||||||
|
|
||||||
PAYLOAD=$(jq -n \
|
jq -n \
|
||||||
--arg title "$RELEASE_TITLE" \
|
--arg title "$RELEASE_TITLE" \
|
||||||
--arg description "$RELEASE_BODY" \
|
--arg description "$RELEASE_BODY" \
|
||||||
--arg url "$RELEASE_URL" \
|
--arg url "$RELEASE_URL" \
|
||||||
--arg author "$AUTHOR_NAME" \
|
--arg author "$AUTHOR_NAME" \
|
||||||
--arg icon "$AUTHOR_ICON" \
|
--arg icon "$AUTHOR_ICON" \
|
||||||
--arg discord_title "${{ inputs.discord_title }}" \
|
--arg discord_title "$DISCORD_TITLE" \
|
||||||
--arg discord_footer "${{ inputs.discord_footer }}" \
|
--arg discord_footer "$DISCORD_FOOTER" \
|
||||||
--argjson discord_color ${{ inputs.discord_color }} \
|
--argjson discord_color "$DISCORD_COLOR" \
|
||||||
'{
|
'{
|
||||||
content: $discord_title,
|
content: (
|
||||||
|
if ($discord_title | length) > 1900
|
||||||
|
then ($discord_title[0:1900] + "...")
|
||||||
|
else $discord_title
|
||||||
|
end
|
||||||
|
),
|
||||||
embeds: [{
|
embeds: [{
|
||||||
title: $title,
|
title: (
|
||||||
|
if ($title | length) > 256
|
||||||
|
then ($title[0:253] + "...")
|
||||||
|
else $title
|
||||||
|
end
|
||||||
|
),
|
||||||
url: $url,
|
url: $url,
|
||||||
description: $description,
|
description: (
|
||||||
|
if ($description | length) > 3800
|
||||||
|
then ($description[0:3800] + "\n\n...")
|
||||||
|
else $description
|
||||||
|
end
|
||||||
|
),
|
||||||
color: $discord_color,
|
color: $discord_color,
|
||||||
author: {
|
author: {
|
||||||
name: $author,
|
name: (
|
||||||
|
if ($author | length) > 256
|
||||||
|
then ($author[0:253] + "...")
|
||||||
|
else $author
|
||||||
|
end
|
||||||
|
),
|
||||||
icon_url: $icon
|
icon_url: $icon
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
text: $discord_footer
|
text: (
|
||||||
|
if ($discord_footer | length) > 512
|
||||||
|
then ($discord_footer[0:509] + "...")
|
||||||
|
else $discord_footer
|
||||||
|
end
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}'
|
}' > payload.json
|
||||||
)
|
|
||||||
|
|
||||||
curl -H "Content-Type: application/json" \
|
cat payload.json | jq .
|
||||||
-d "$PAYLOAD" \
|
|
||||||
"$DISCORD_WEBHOOK"
|
curl --fail-with-body -sS \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @payload.json \
|
||||||
|
"$DISCORD_WEBHOOK?wait=true"
|
||||||
+1
-1
@@ -33,5 +33,5 @@ keywords:
|
|||||||
- web-ui
|
- web-ui
|
||||||
- agent
|
- agent
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
version: 1.15.2
|
version: 1.17.0
|
||||||
date-released: '2026-03-02'
|
date-released: '2026-03-02'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 "@/components/wrappers/auth/login/forgot-password-form/forgot-password-form";
|
import { ForgotPasswordForm } from "@/features/auth/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/card-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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 "@/components/wrappers/auth/guard/guard-form";
|
import {GuardForm} from "@/features/auth/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/card-auth";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 "@/components/wrappers/auth/auth-logo-section";
|
import { AuthLogoSection } from "@/features/auth/auth-logo-section";
|
||||||
import { Heart } from "lucide-react";
|
import { Heart } from "lucide-react";
|
||||||
|
|
||||||
export default async function Layout({
|
export default async function Layout({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { LoginForm } from "@/components/wrappers/auth/login/login-form/login-form";
|
import { LoginForm } from "@/features/auth/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 "@/components/wrappers/auth/social-buttons";
|
import { SocialAuthButtons } from "@/features/auth/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";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PageParams } from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import { RegisterForm } from "@/components/wrappers/auth/register/register-form/register-form";
|
import { RegisterForm } from "@/features/auth/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,6 +1,6 @@
|
|||||||
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 "@/components/wrappers/auth/login/reset-password-form/reset-password-form";
|
import { ResetPasswordForm } from "@/features/auth/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";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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/page";
|
||||||
import {OrganizationManagement} from "@/components/wrappers/dashboard/admin/organizations/organization/organization-management";
|
import {OrganizationManagement} from "@/features/organizations/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";
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/
|
|||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {
|
import {
|
||||||
AdminOrganizationAddModal
|
AdminOrganizationAddModal
|
||||||
} from "@/components/wrappers/dashboard/admin/organizations/organization/admin-organization-add-modal";
|
} from "@/features/organizations/admin-organization-add-modal";
|
||||||
import {AdminOrganizationList} from "@/components/wrappers/dashboard/admin/organizations/organization/admin-orgnization-list";
|
import {AdminOrganizationList} from "@/features/organizations/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<{}>) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/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 "@/components/wrappers/dashboard/admin/settings/settings-tabs";
|
import {SettingsTabs} from "@/features/settings/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";
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import {PageParams} from "@/types/next";
|
|||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {desc, isNull} from "drizzle-orm";
|
import {desc, isNull} from "drizzle-orm";
|
||||||
import {AdminUserList} from "@/components/wrappers/dashboard/admin/users/admin-user-list";
|
import {AdminUserList} from "@/features/users/admin-user-list";
|
||||||
import {AdminUserAddModal} from "@/components/wrappers/dashboard/admin/users/admin-user-add-modal";
|
import {AdminUserAddModal} from "@/features/users/admin-user-add-modal";
|
||||||
import {SUPPORTED_PROVIDERS} from "@/lib/auth/config";
|
import {SUPPORTED_PROVIDERS} from "@/lib/auth/config";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ 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 "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
|
import { ButtonDeleteAgent } from "@/features/agents/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 "@/components/wrappers/dashboard/agent/agent-content";
|
import { AgentContentPage } from "@/features/agents/agent-content";
|
||||||
import { AgentDialog } from "@/features/agents/components/agent.dialog";
|
import { AgentDialog } from "@/features/agents/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 "@/components/wrappers/dashboard/agent/agent-card/agent-card";
|
import {AgentCard} from "@/features/agents/agent-card";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/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/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/components/agent.dialog";
|
import {AgentDialog} from "@/features/agents/agent-dialog";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Agents",
|
title: "Agents",
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ 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 "@/components/wrappers/dashboard/admin/channels/channels-section";
|
import {ChannelsSection} from "@/features/channel/channels-section";
|
||||||
import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
|
import {ChannelAddEditModal} from "@/features/channel/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/page";
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
import {NotificationLogsList} from "@/components/wrappers/dashboard/admin/notifications/logs/notification-logs-list";
|
import {NotificationLogsList} from "@/features/notifications/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/page";
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
import {ChannelsSection} from "@/components/wrappers/dashboard/admin/channels/channels-section";
|
import {ChannelsSection} from "@/features/channel/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 "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
|
import {ChannelAddEditModal} from "@/features/channel/channel-add-edit-modal";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Storage Channels",
|
title: "Storage Channels",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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 "@/components/wrappers/dashboard/organization/migration/migration-tool";
|
import {MigrationTool} from "@/features/migration/migration-tool";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Projects",
|
title: "Projects",
|
||||||
|
|||||||
+3
-3
@@ -4,10 +4,10 @@ import {Page} from "@/features/layout/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 "@/lib/services";
|
import {getOrganizationProjectDatabases} from "@/db/services/project";
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
|
import {BackupModalProvider} from "@/features/database/backup-modal-context";
|
||||||
import {DatabaseContent} from "@/components/wrappers/dashboard/projects/database/database-content";
|
import {DatabaseContent} from "@/features/database/database-content";
|
||||||
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
|
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
export default async function RoutePage(props: PageParams<{
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ import {PageParams} from "@/types/next";
|
|||||||
import {Page, PageContent, PageTitle} from "@/features/layout/page";
|
import {Page, PageContent, PageTitle} from "@/features/layout/page";
|
||||||
import {
|
import {
|
||||||
ButtonDeleteProject
|
ButtonDeleteProject
|
||||||
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
|
} from "@/features/projects/project-delete-button";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
||||||
import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
|
import {ProjectDatabaseCard} from "@/features/projects/project-database-card";
|
||||||
import {notFound, redirect} from "next/navigation";
|
import {notFound, redirect} from "next/navigation";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {capitalizeFirstLetter} from "@/utils/text";
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
import {ProjectDialog} from "@/features/projects/components/project.dialog";
|
import {ProjectDialog} from "@/features/projects/project-dialog";
|
||||||
import {ProjectWith} from "@/db/schema/06_project";
|
import {ProjectWith} from "@/db/schema/06_project";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import {getOrganizationAvailableDatabases} from "@/db/services/database";
|
import {getOrganizationAvailableDatabases} from "@/db/services/database";
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/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/page";
|
||||||
import {ProjectCard} from "@/components/wrappers/dashboard/projects/project-card/project-card";
|
import {ProjectCard} from "@/features/projects/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/wrappers/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/components/project.dialog";
|
import {ProjectDialog} from "@/features/projects/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";
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ 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 "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
|
import {ButtonDeleteAgent} from "@/features/agents/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 "@/components/wrappers/dashboard/agent/agent-content";
|
import {AgentContentPage} from "@/features/agents/agent-content";
|
||||||
import {AgentDialog} from "@/features/agents/components/agent.dialog";
|
import {AgentDialog} from "@/features/agents/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";
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ 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 "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
import {OrganizationTabs} from "@/features/organizations/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 "@/components/wrappers/dashboard/organization/delete-organization-button";
|
import {DeleteOrganizationButton} from "@/features/organizations/organization-delete-button";
|
||||||
import {EditOrganizationDialog} from "@/features/organization/components/edit-organization.dialog";
|
import {EditOrganizationDialog} from "@/features/organizations/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/page";
|
||||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
|
import {EvolutionLineChart} from "@/features/statistics/evolution-line-chart";
|
||||||
import {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
|
import {PercentageLineChart} from "@/features/statistics/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";
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ 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 "@/components/wrappers/dashboard/common/sidebar/app-sidebar";
|
import { AppSidebar } from "@/features/layout/app-sidebar";
|
||||||
import { Header } from "@/features/layout/Header";
|
import { Header } from "@/features/layout/header";
|
||||||
import { currentUser } from "@/lib/auth/current-user";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
import { ThemeMetaUpdater } from "@/features/browser/theme-meta-updater";
|
import { ThemeMetaUpdater } from "@/features/theme/theme-meta-updater";
|
||||||
|
import { ModeToggle } from "@/features/theme/mode-toggle";
|
||||||
|
import { UpdateNotification } from "@/features/updates/update-notification";
|
||||||
|
|
||||||
export default async function Layout({ children }: { children: ReactNode }) {
|
export default async function Layout({ children }: { children: ReactNode }) {
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
@@ -15,9 +17,9 @@ export default async function Layout({ children }: { children: ReactNode }) {
|
|||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<div className="flex flex-col lg:flex-row w-full">
|
<div className="flex flex-col lg:flex-row w-full">
|
||||||
<ThemeMetaUpdater />
|
<ThemeMetaUpdater />
|
||||||
<AppSidebar />
|
<AppSidebar updateNotification={<UpdateNotification />} />
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<Header />
|
<Header actions={<ModeToggle />} />
|
||||||
<main className="h-full">{children}</main>
|
<main className="h-full">{children}</main>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LoadingSpinner } from "@/components/wrappers/common/loading/loading-spinner";
|
import { LoadingSpinner } from "@/components/common/loading-spinner";
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
|
import { getCurrentOrganizationSlug } from "@/features/organizations/organization-cookie";
|
||||||
import { currentUser } from "@/lib/auth/current-user";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
|
|
||||||
export default async function Index() {
|
export default async function Index() {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import {db as dbClient, db} from "@/db";
|
|||||||
import {getDatabaseOrThrow, withAgentCheck} from "./helpers";
|
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 "@/features/shared/event";
|
import {eventEmitter} from "@/lib/event";
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers";
|
||||||
import {EventKind} from "@/features/notifications/types";
|
import {EventKind} from "@/features/notifications/notifications.types";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
const log = logger.child({module: "api/agent/backup/route"});
|
const log = logger.child({module: "api/agent/backup/route"});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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 {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import {eventEmitter} from "@/features/shared/event";
|
import {eventEmitter} from "@/lib/event";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
const log = logger.child({module: "api/agent/backup/upload/init"});
|
const log = logger.child({module: "api/agent/backup/upload/init"});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as drizzleDb from "@/db";
|
|||||||
import {db as dbClient, db} from "@/db";
|
import {db as dbClient, db} from "@/db";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
||||||
import {eventEmitter} from "@/features/shared/event";
|
import {eventEmitter} from "@/lib/event";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
const log = logger.child({module: "api/agent/backup/upload/status"});
|
const log = logger.child({module: "api/agent/backup/upload/status"});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {isUuidv4} from "@/utils/verify-uuid";
|
|||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {db, db as dbClient} from "@/db";
|
|||||||
import {and, eq, inArray} from "drizzle-orm";
|
import {and, eq, inArray} 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/types";
|
import type {StorageInput} from "@/features/storages/storages.types";
|
||||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
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";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,35 @@
|
|||||||
import { auth } from "@/lib/auth/auth";
|
import { auth } from "@/lib/auth/auth";
|
||||||
import { toNextJsHandler } from "better-auth/next-js";
|
import { toNextJsHandler } from "better-auth/next-js";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
|
||||||
export const { GET, POST } = toNextJsHandler(auth.handler);
|
const authHandler = toNextJsHandler(auth.handler);
|
||||||
|
|
||||||
|
async function blockApiKeyCreateForRestrictedUsers(req: NextRequest): Promise<NextResponse | null> {
|
||||||
|
const url = req.nextUrl;
|
||||||
|
if (req.method !== "POST" || !url.pathname.endsWith("/api-key/create")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
|
if (!session?.user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
if (session.user.banned || (session.user.role as string) === "pending") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Account not eligible to create API keys" },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
return authHandler.GET(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const guard = await blockApiKeyCreateForRestrictedUsers(req);
|
||||||
|
if (guard) return guard;
|
||||||
|
return authHandler.POST(req);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ export async function GET() {
|
|||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
PROJECT_URL: process.env.PROJECT_URL,
|
PROJECT_URL: process.env.PROJECT_URL,
|
||||||
PROJECT_NAME: process.env.PROJECT_NAME,
|
PROJECT_NAME: process.env.PROJECT_NAME,
|
||||||
PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION,
|
PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {auth} from "@/lib/auth/auth";
|
import {auth} from "@/lib/auth/auth";
|
||||||
import {headers} from "next/headers";
|
import {headers} from "next/headers";
|
||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {eventEmitter} from "@/features/shared/event";
|
import {eventEmitter} from "@/lib/event";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
const log = logger.child({module: "api/events"});
|
const log = logger.child({module: "api/events"});
|
||||||
|
|||||||
@@ -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/types";
|
import type {StorageInput} from "@/features/storages/storages.types";
|
||||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
import {dispatchStorage} from "@/features/storages/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/types";
|
import {StorageInput} from "@/features/storages/storages.types";
|
||||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
import {dispatchStorage} from "@/features/storages/storages.dispatch";
|
||||||
import {Readable} from "node:stream";
|
import {Readable} from "node:stream";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import {withApiKey} from "@/lib/api-v1/middleware";
|
||||||
|
import {ApiKeyContext} from "@/lib/api-v1/types";
|
||||||
|
import {NextResponse} from "next/server";
|
||||||
|
import {getAgent, resolveAgentAccess} from "@/lib/api-v1/services/agents";
|
||||||
|
import {logger} from "@/lib/logger";
|
||||||
|
import {generateEdgeKey} from "@/utils/edge_key";
|
||||||
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
|
|
||||||
|
const log = logger.child({ module: "api/v1/agents/[id]/key" });
|
||||||
|
|
||||||
|
export const GET = withApiKey(
|
||||||
|
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
|
try {
|
||||||
|
const id = params?.id;
|
||||||
|
if (!id) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
|
||||||
|
const access = await resolveAgentAccess(id, ctx.user);
|
||||||
|
|
||||||
|
if (access === "forbidden") return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||||
|
if (access === "not_found") return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
|
||||||
|
const agent = await getAgent(id);
|
||||||
|
|
||||||
|
if (!agent) return NextResponse.json({ error: "Agent not found" }, { status: 404 });
|
||||||
|
|
||||||
|
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
|
||||||
|
|
||||||
|
return NextResponse.json({ data: edgeKey });
|
||||||
|
} catch (error) {
|
||||||
|
log.error({ error }, "Error in GET /api/v1/agents/[id]");
|
||||||
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import {NextResponse} from "next/server";
|
||||||
|
import {withApiKey} from "@/lib/api-v1/middleware";
|
||||||
|
import {logger} from "@/lib/logger";
|
||||||
|
import {ApiKeyContext} from "@/lib/api-v1/types";
|
||||||
|
import {getAgent, resolveAgentAccess} from "@/lib/api-v1/services/agents";
|
||||||
|
import {deleteAgentService} from "@/features/agents/agent-delete.action";
|
||||||
|
|
||||||
|
const log = logger.child({module: "api/v1/agents/[id]"});
|
||||||
|
|
||||||
|
export const GET = withApiKey(
|
||||||
|
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
|
try {
|
||||||
|
const id = params?.id;
|
||||||
|
if (!id) return NextResponse.json({error: "Not found"}, {status: 404});
|
||||||
|
|
||||||
|
const access = await resolveAgentAccess(id, ctx.user);
|
||||||
|
|
||||||
|
if (access === "forbidden") return NextResponse.json({error: "Forbidden"}, {status: 403});
|
||||||
|
if (access === "not_found") return NextResponse.json({error: "Not found"}, {status: 404});
|
||||||
|
|
||||||
|
const agent = await getAgent(id, {
|
||||||
|
includeDatabases: true,
|
||||||
|
includeOrganizations: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!agent) return NextResponse.json({error: "Not found"}, {status: 404});
|
||||||
|
return NextResponse.json({data: agent});
|
||||||
|
} catch (error) {
|
||||||
|
log.error({error}, "Error in GET /api/v1/agents/[id]");
|
||||||
|
return NextResponse.json({error: "Internal server error"}, {status: 500});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DELETE = withApiKey(
|
||||||
|
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
|
try {
|
||||||
|
const id = params?.id;
|
||||||
|
if (!id) return NextResponse.json({error: "Not found"}, {status: 404});
|
||||||
|
|
||||||
|
const access = await resolveAgentAccess(id, ctx.user);
|
||||||
|
if (access === "forbidden") return NextResponse.json({error: "Forbidden"}, {status: 403});
|
||||||
|
if (access === "not_found") return NextResponse.json({error: "Not found"}, {status: 404});
|
||||||
|
|
||||||
|
|
||||||
|
const agent = await getAgent(id, {
|
||||||
|
includeDatabases: false,
|
||||||
|
includeOrganizations: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!agent) return NextResponse.json({error: "Agent no found"}, {status: 404});
|
||||||
|
|
||||||
|
const organizationIds = agent.organizations.map(org => org.organizationId)
|
||||||
|
|
||||||
|
const canDeleteGlobalAgent =
|
||||||
|
ctx.user.permissions.isAdmin || ctx.user.permissions.isSuperAdmin;
|
||||||
|
|
||||||
|
if (!agent.organizationId && !canDeleteGlobalAgent) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Forbidden" },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteAgentService({
|
||||||
|
agentId: agent.id,
|
||||||
|
organizationId: agent.organizationId ?? undefined,
|
||||||
|
organizationIds: organizationIds
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(null, {status: 204});
|
||||||
|
} catch (error) {
|
||||||
|
log.error({error}, "Error in DELETE /api/v1/agents/[id]");
|
||||||
|
return NextResponse.json({error: "Internal server error"}, {status: 500});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import { inArray, eq, and, or, isNull } from "drizzle-orm";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
import {createAgentService} from "@/features/agents/agents.action";
|
||||||
|
import { ActionError } from "@/lib/safe-actions/actions";
|
||||||
|
import {getAccessibleAgentIds} from "@/lib/api-v1/services/agents";
|
||||||
|
import {ApiKeyContext} from "@/lib/api-v1/types";
|
||||||
|
import {parseJsonBody} from "@/lib/api-v1/validation/json-body";
|
||||||
|
|
||||||
|
const log = logger.child({ module: "api/v1/agents" });
|
||||||
|
|
||||||
|
export const GET = withApiKey(async (_req: Request, ctx: ApiKeyContext) => {
|
||||||
|
try {
|
||||||
|
const agentIds = await getAccessibleAgentIds(ctx.user);
|
||||||
|
|
||||||
|
if (agentIds.length === 0) {
|
||||||
|
return NextResponse.json({ data: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const agents = await db.query.agent.findMany({
|
||||||
|
where: and(
|
||||||
|
inArray(drizzleDb.schemas.agent.id, agentIds),
|
||||||
|
or(
|
||||||
|
eq(drizzleDb.schemas.agent.isArchived, false),
|
||||||
|
isNull(drizzleDb.schemas.agent.isArchived)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ data: agents });
|
||||||
|
} catch (error) {
|
||||||
|
log.error({ error }, "Error in GET /api/v1/agents");
|
||||||
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const CreateAgentSchema = z.object({
|
||||||
|
name: z.string().min(1, "name is required"),
|
||||||
|
organizationId: z.string().uuid("organizationId must be a valid UUID").optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const POST = withApiKey(
|
||||||
|
async (req: Request, ctx: ApiKeyContext) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const body = await parseJsonBody(req, CreateAgentSchema);
|
||||||
|
|
||||||
|
if (!body.ok) {
|
||||||
|
return body.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, organizationId } = body.data;
|
||||||
|
|
||||||
|
const org = ctx.organizations.find(
|
||||||
|
(org) => org.id === organizationId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
organizationId &&
|
||||||
|
(!org || !org.permissions.canManageAgents)
|
||||||
|
) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Forbidden" },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const canCreateGlobalAgent =
|
||||||
|
ctx.user.permissions.isAdmin || ctx.user.permissions.isSuperAdmin;
|
||||||
|
|
||||||
|
if (!organizationId && !canCreateGlobalAgent) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Forbidden" },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdAgent = await createAgentService({
|
||||||
|
organizationId,
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
description: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ data: createdAgent },
|
||||||
|
{ status: 201 }
|
||||||
|
);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof ActionError) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error.message },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.error({error},
|
||||||
|
"Error in POST /api/v1/agents"
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
import { ApiKeyContext } from "@/lib/api-v1/types";
|
||||||
|
import {requireDatabaseAccess} from "@/lib/api-v1/services/databases";
|
||||||
|
|
||||||
|
const log = logger.child({
|
||||||
|
module: "api/v1/databases/[id]/backup/[backupId]",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GET = withApiKey(
|
||||||
|
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
|
try {
|
||||||
|
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||||
|
|
||||||
|
if (!guard.ok) {
|
||||||
|
return guard.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = guard.data;
|
||||||
|
const backupId = params?.backupId;
|
||||||
|
|
||||||
|
if (!backupId) {
|
||||||
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const backup = await db.query.backup.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.backup.id, backupId),
|
||||||
|
eq(drizzleDb.schemas.backup.databaseId, id),
|
||||||
|
isNull(drizzleDb.schemas.backup.deletedAt)
|
||||||
|
),
|
||||||
|
with: {
|
||||||
|
storages: {
|
||||||
|
where: (backupStorage, { isNull }) =>
|
||||||
|
isNull(backupStorage.deletedAt),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!backup) {
|
||||||
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ data: backup });
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
{ error },
|
||||||
|
"Error in GET /api/v1/databases/[id]/backup/[backupId]"
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {and, desc, eq, inArray, isNull} from "drizzle-orm";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
import { ApiKeyContext } from "@/lib/api-v1/types";
|
||||||
|
import {requireDatabaseAccess} from "@/lib/api-v1/services/databases";
|
||||||
|
|
||||||
|
const log = logger.child({ module: "api/v1/databases/[id]/backup" });
|
||||||
|
|
||||||
|
export const GET = withApiKey(
|
||||||
|
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
|
try {
|
||||||
|
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||||
|
|
||||||
|
if (!guard.ok) {
|
||||||
|
return guard.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = guard.data;
|
||||||
|
|
||||||
|
const backups = await db.query.backup.findMany({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.backup.databaseId, id),
|
||||||
|
isNull(drizzleDb.schemas.backup.deletedAt)
|
||||||
|
),
|
||||||
|
orderBy: [desc(drizzleDb.schemas.backup.createdAt)],
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ data: backups });
|
||||||
|
} catch (error) {
|
||||||
|
log.error({ error }, "Error in GET /api/v1/databases/[id]/backup");
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const POST = withApiKey(
|
||||||
|
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
|
try {
|
||||||
|
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||||
|
|
||||||
|
if (!guard.ok) {
|
||||||
|
return guard.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = guard.data;
|
||||||
|
|
||||||
|
const existingBackup = await db.query.backup.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.backup.databaseId, id),
|
||||||
|
inArray(drizzleDb.schemas.backup.status, ["waiting", "ongoing"])
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingBackup) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error:
|
||||||
|
"A backup is already waiting or ongoing for this database",
|
||||||
|
},
|
||||||
|
{ status: 409 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [createdBackup] = await db
|
||||||
|
.insert(drizzleDb.schemas.backup)
|
||||||
|
.values({
|
||||||
|
databaseId: id,
|
||||||
|
status: "waiting",
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!createdBackup) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to create backup" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ data: createdBackup }, { status: 201 });
|
||||||
|
} catch (error) {
|
||||||
|
log.error({ error }, "Error in POST /api/v1/databases/[id]/backup");
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {and, eq, inArray, isNull} from "drizzle-orm";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
import { ApiKeyContext } from "@/lib/api-v1/types";
|
||||||
|
import { parseJsonBody } from "@/lib/api-v1/validation/json-body";
|
||||||
|
import {requireDatabaseAccess} from "@/lib/api-v1/services/databases";
|
||||||
|
|
||||||
|
const log = logger.child({ module: "api/v1/databases/[id]/restore" });
|
||||||
|
|
||||||
|
const RestoreSchema = z.object({
|
||||||
|
backupId: z.string().uuid("backupId must be a valid UUID"),
|
||||||
|
backupStorageId: z.string().uuid("backupStorageId must be a valid UUID"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const POST = withApiKey(
|
||||||
|
async (req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
|
try {
|
||||||
|
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||||
|
|
||||||
|
if (!guard.ok) {
|
||||||
|
return guard.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = guard.data;
|
||||||
|
|
||||||
|
const body = await parseJsonBody(req, RestoreSchema);
|
||||||
|
|
||||||
|
if (!body.ok) {
|
||||||
|
return body.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { backupId, backupStorageId } = body.data;
|
||||||
|
|
||||||
|
const backupRecord = await db.query.backup.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.backup.id, backupId),
|
||||||
|
eq(drizzleDb.schemas.backup.databaseId, id),
|
||||||
|
isNull(drizzleDb.schemas.backup.deletedAt)
|
||||||
|
),
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!backupRecord) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Backup not found for this database" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupStorage = await db.query.backupStorage.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.backupStorage.id, backupStorageId),
|
||||||
|
eq(drizzleDb.schemas.backupStorage.backupId, backupId),
|
||||||
|
isNull(drizzleDb.schemas.backupStorage.deletedAt)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!backupStorage) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Backup storage not found" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupStorage.status !== "success") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Backup storage is not in a successful state" },
|
||||||
|
{ status: 422 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingRestorationBackup = await db.query.restoration.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.restoration.databaseId, id),
|
||||||
|
inArray(drizzleDb.schemas.restoration.status, ["waiting", "ongoing"])
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingRestorationBackup) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error:
|
||||||
|
"A restoration is already waiting or ongoing for this database",
|
||||||
|
},
|
||||||
|
{ status: 409 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [restoration] = await db
|
||||||
|
.insert(drizzleDb.schemas.restoration)
|
||||||
|
.values({
|
||||||
|
databaseId: id,
|
||||||
|
backupId,
|
||||||
|
backupStorageId,
|
||||||
|
status: "waiting",
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!restoration) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to create restoration" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ data: restoration }, { status: 201 });
|
||||||
|
} catch (error) {
|
||||||
|
log.error({ error }, "Error in POST /api/v1/databases/[id]/restore");
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
import { ApiKeyContext } from "@/lib/api-v1/types";
|
||||||
|
import {requireDatabaseAccess} from "@/lib/api-v1/services/databases";
|
||||||
|
|
||||||
|
const log = logger.child({ module: "api/v1/databases/[id]" });
|
||||||
|
|
||||||
|
export const GET = withApiKey(
|
||||||
|
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
|
try {
|
||||||
|
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||||
|
|
||||||
|
if (!guard.ok) {
|
||||||
|
return guard.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = guard.data;
|
||||||
|
|
||||||
|
const database = await db.query.database.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.database.id, id),
|
||||||
|
isNull(drizzleDb.schemas.database.deletedAt)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!database) {
|
||||||
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ data: database });
|
||||||
|
} catch (error) {
|
||||||
|
log.error({ error }, "Error in GET /api/v1/databases/[id]");
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import { and, desc, eq, isNull } from "drizzle-orm";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
import { ApiKeyContext } from "@/lib/api-v1/types";
|
||||||
|
import {requireDatabaseAccess} from "@/lib/api-v1/services/databases";
|
||||||
|
|
||||||
|
const log = logger.child({ module: "api/v1/databases/[id]/status" });
|
||||||
|
|
||||||
|
export const GET = withApiKey(
|
||||||
|
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
|
try {
|
||||||
|
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||||
|
|
||||||
|
if (!guard.ok) {
|
||||||
|
return guard.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = guard.data;
|
||||||
|
|
||||||
|
const [database, latestBackup, latestRestoration] = await Promise.all([
|
||||||
|
db.query.database.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.database.id, id),
|
||||||
|
isNull(drizzleDb.schemas.database.deletedAt)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
|
||||||
|
db.query.backup.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.backup.databaseId, id),
|
||||||
|
isNull(drizzleDb.schemas.backup.deletedAt)
|
||||||
|
),
|
||||||
|
orderBy: [desc(drizzleDb.schemas.backup.createdAt)],
|
||||||
|
}),
|
||||||
|
|
||||||
|
db.query.restoration.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.restoration.databaseId, id),
|
||||||
|
isNull(drizzleDb.schemas.restoration.deletedAt)
|
||||||
|
),
|
||||||
|
orderBy: [desc(drizzleDb.schemas.restoration.createdAt)],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!database) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Database not found" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: {
|
||||||
|
isWaitingForBackup: database.isWaitingForBackup,
|
||||||
|
lastContact: database.lastContact,
|
||||||
|
latestBackup: latestBackup ?? null,
|
||||||
|
latestRestoration: latestRestoration ?? null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
log.error({ error }, "Error in GET /api/v1/databases/[id]/status");
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
import { ApiKeyContext } from "@/lib/api-v1/types";
|
||||||
|
import { getAccessibleDatabases } from "@/lib/api-v1/services/databases";
|
||||||
|
|
||||||
|
const log = logger.child({ module: "api/v1/databases" });
|
||||||
|
|
||||||
|
export const GET = withApiKey(async (_req: Request, ctx: ApiKeyContext) => {
|
||||||
|
try {
|
||||||
|
const databases = await getAccessibleDatabases(ctx.user);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: databases,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
log.error({ error }, "Error in GET /api/v1/databases");
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
const html = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Portabase API Docs</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.32.6/swagger-ui.css"
|
||||||
|
integrity="sha384-9Q2fpS+xeS4ffJy6CagnwoUl+4ldAYhOs9pgZuEKxypVModhmZFzeMlvVsAjf7uT"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.32.6/swagger-ui-bundle.js" integrity="sha384-EYdOaiRwn44zNjrw+Tfs06qYz9BGQVo2f4/pLY5i7VorbjnZNhdplAbTBk8FXHUJ" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.32.6/swagger-ui-standalone-preset.js" integrity="sha384-49fpFaVrAWI/qdgl9Vv5E/4NXxRUiJX5vGuLws1NUpTWGtEqzWEx8gHTw2UTehFK" crossorigin="anonymous"></script>
|
||||||
|
<script>
|
||||||
|
window.onload = function () {
|
||||||
|
SwaggerUIBundle({
|
||||||
|
url: "/api/v1/openapi",
|
||||||
|
dom_id: "#swagger-ui",
|
||||||
|
presets: [
|
||||||
|
SwaggerUIBundle.presets.apis,
|
||||||
|
SwaggerUIStandalonePreset,
|
||||||
|
],
|
||||||
|
layout: "StandaloneLayout",
|
||||||
|
persistAuthorization: true,
|
||||||
|
filter: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
export function GET() {
|
||||||
|
return new Response(html, {
|
||||||
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||||
|
import { createPortabaseMcpServer } from "@/lib/mcp/server";
|
||||||
|
import type { ApiKeyContext } from "@/lib/api-v1/types";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP endpoint — Streamable HTTP transport, stateless mode.
|
||||||
|
*
|
||||||
|
* Auth is handled by withApiKey before MCP is ever touched.
|
||||||
|
* The validated key is forwarded by MCP tools to downstream /api/v1 REST calls.
|
||||||
|
*/
|
||||||
|
const apiEnabled = String(env.API_ENABLED) === "true";
|
||||||
|
const mcpEnabled = String(env.MCP_ENABLED) === "true";
|
||||||
|
|
||||||
|
export const POST = apiEnabled && mcpEnabled
|
||||||
|
? withApiKey(async (req: Request, ctx: ApiKeyContext) => {
|
||||||
|
const apiKey = req.headers.get("x-api-key")!;
|
||||||
|
|
||||||
|
const server = createPortabaseMcpServer(ctx, apiKey);
|
||||||
|
|
||||||
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
||||||
|
sessionIdGenerator: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.connect(transport);
|
||||||
|
|
||||||
|
return transport.handleRequest(req);
|
||||||
|
})
|
||||||
|
: () => NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { buildSpec } from "@/lib/api-v1/openapi/spec";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export function GET() {
|
||||||
|
return NextResponse.json(buildSpec());
|
||||||
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { ConsoleSilencer } from "@/components/wrappers/common/console-silencer";
|
import { ConsoleSilencer } from "@/components/common/console-silencer";
|
||||||
import { author, geistMono, poppins } from "@/fonts/fonts";
|
import { author, geistMono, poppins } from "@/fonts/fonts";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Providers } from "./providers";
|
import { Providers } from "./providers";
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import BackButton from "@/components/wrappers/common/button/back-button";
|
import BackButton from "@/components/common/back-button";
|
||||||
|
|
||||||
export default async function NotFound() {
|
export default async function NotFound() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
+2
-2
@@ -4,8 +4,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|||||||
import { type PropsWithChildren, Suspense } from "react";
|
import { type PropsWithChildren, Suspense } from "react";
|
||||||
|
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { ErrorLayout } from "@/components/wrappers/common/error-layout";
|
import { ErrorLayout } from "@/components/common/error-layout";
|
||||||
import { ThemeMetaUpdaterRoot } from "@/features/browser/theme-meta-updater-root";
|
import { ThemeMetaUpdaterRoot } from "@/features/theme/theme-meta-updater-root";
|
||||||
import { ThemeProvider } from "@/features/theme/theme-provider";
|
import { ThemeProvider } from "@/features/theme/theme-provider";
|
||||||
|
|
||||||
export type ProviderProps = PropsWithChildren<{}>;
|
export type ProviderProps = PropsWithChildren<{}>;
|
||||||
|
|||||||
@@ -31,8 +31,9 @@ const invalidChannelName = "Webhook E2E Invalid";
|
|||||||
// await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible();
|
// await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible();
|
||||||
// await create(page, "Webhook", optionalChannelName, async (page) => {
|
// await create(page, "Webhook", optionalChannelName, async (page) => {
|
||||||
// await page.getByLabel(/Webhook URL/).fill(getEnv("E2E_NOTIFICATION_WEBHOOK_URL"));
|
// await page.getByLabel(/Webhook URL/).fill(getEnv("E2E_NOTIFICATION_WEBHOOK_URL"));
|
||||||
|
// await page.getByRole("button", { name: "Add Header" }).click();
|
||||||
// await page.getByLabel(/^Header Name$/).fill(getEnv("E2E_NOTIFICATION_WEBHOOK_SECRET_HEADER"));
|
// await page.getByLabel(/^Header Name$/).fill(getEnv("E2E_NOTIFICATION_WEBHOOK_SECRET_HEADER"));
|
||||||
// await page.getByLabel(/^Secret Value$/).fill(getEnv("E2E_NOTIFICATION_WEBHOOK_SECRET"));
|
// await page.getByLabel(/^Header Value$/).fill(getEnv("E2E_NOTIFICATION_WEBHOOK_SECRET"));
|
||||||
// });
|
// });
|
||||||
// await submit(page);
|
// await submit(page);
|
||||||
// await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible();
|
// await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible();
|
||||||
@@ -49,8 +50,9 @@ test.describe.serial("Invalid channel", () => {
|
|||||||
await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible();
|
await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible();
|
||||||
await create(page, "Webhook", invalidChannelName, async (page) => {
|
await create(page, "Webhook", invalidChannelName, async (page) => {
|
||||||
await page.getByLabel(/Webhook URL/).fill("https://webhook.example.com/api/wrong-webhook");
|
await page.getByLabel(/Webhook URL/).fill("https://webhook.example.com/api/wrong-webhook");
|
||||||
|
await page.getByRole("button", { name: "Add Header" }).click();
|
||||||
await page.getByLabel(/^Header Name$/).fill(getEnv("E2E_NOTIFICATION_WEBHOOK_SECRET_HEADER"));
|
await page.getByLabel(/^Header Name$/).fill(getEnv("E2E_NOTIFICATION_WEBHOOK_SECRET_HEADER"));
|
||||||
await page.getByLabel(/^Secret Value$/).fill("wrong-webhook-secret");
|
await page.getByLabel(/^Header Value$/).fill("wrong-webhook-secret");
|
||||||
});
|
});
|
||||||
await submit(page);
|
await submit(page);
|
||||||
await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible();
|
await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible();
|
||||||
|
|||||||
+34
-31
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.15.2",
|
"version": "1.17.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 8887",
|
"dev": "next dev --turbopack -p 8887",
|
||||||
@@ -15,10 +15,13 @@
|
|||||||
"release": "release-it"
|
"release": "release-it"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@better-auth/core": "1.6.2",
|
"@asteasolutions/zod-to-openapi": "^8.5.0",
|
||||||
"@better-auth/passkey": "1.6.2",
|
"@better-auth/api-key": "1.6.11",
|
||||||
"@better-auth/sso": "1.6.2",
|
"@better-auth/core": "1.6.11",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@better-auth/passkey": "1.6.11",
|
||||||
|
"@better-auth/sso": "1.6.11",
|
||||||
|
"@hookform/resolvers": "^5.4.0",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"@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-aspect-ratio": "^1.1.8",
|
||||||
@@ -49,19 +52,19 @@
|
|||||||
"@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.97.0",
|
"@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",
|
"@types/ws": "^8.18.1",
|
||||||
"@zenstackhq/runtime": "2.14.2",
|
"@zenstackhq/runtime": "2.14.2",
|
||||||
"argon2": "^0.43.1",
|
"argon2": "^0.43.1",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"better-auth": "1.6.2",
|
"better-auth": "1.6.11",
|
||||||
"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.1.0",
|
"date-fns": "^4.3.0",
|
||||||
"dockerode": "^4.0.10",
|
"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",
|
||||||
@@ -70,7 +73,7 @@
|
|||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.553.0",
|
||||||
"minio": "^8.0.7",
|
"minio": "^8.0.7",
|
||||||
"motion": "^12.38.0",
|
"motion": "^12.40.0",
|
||||||
"next": "16.2.3",
|
"next": "16.2.3",
|
||||||
"next-safe-action": "^7.10.8",
|
"next-safe-action": "^7.10.8",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
@@ -78,17 +81,17 @@
|
|||||||
"node-forge": "^1.4.0",
|
"node-forge": "^1.4.0",
|
||||||
"nodemailer": "8.0.5",
|
"nodemailer": "8.0.5",
|
||||||
"npm-check-updates": "^18.3.1",
|
"npm-check-updates": "^18.3.1",
|
||||||
"pg": "^8.20.0",
|
"pg": "^8.21.0",
|
||||||
"pino": "^10.3.1",
|
"pino": "^10.3.1",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"prettier": "^3.8.2",
|
"prettier": "^3.8.3",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.6",
|
||||||
"react-day-picker": "9.7.0",
|
"react-day-picker": "9.7.0",
|
||||||
"react-dom": "^19.2.5",
|
"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.72.1",
|
"react-hook-form": "^7.76.1",
|
||||||
"react-qr-code": "^2.0.18",
|
"react-qr-code": "^2.0.21",
|
||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^3.0.6",
|
||||||
"react-twc": "^1.5.1",
|
"react-twc": "^1.5.1",
|
||||||
"react-use-measure": "^2.1.7",
|
"react-use-measure": "^2.1.7",
|
||||||
@@ -97,44 +100,44 @@
|
|||||||
"socket.io": "^4.8.3",
|
"socket.io": "^4.8.3",
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"swiper": "^12.1.3",
|
"swiper": "^12.1.4",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.1",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"ws": "^8.20.0",
|
"ws": "^8.21.0",
|
||||||
"zod": "4.3.6"
|
"zod": "4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
"@playwright/test": "1.58.2",
|
"@playwright/test": "1.58.2",
|
||||||
"@react-email/preview-server": "4.3.2",
|
"@react-email/preview-server": "4.3.2",
|
||||||
"@react-email/render": "^2.0.6",
|
"@react-email/render": "^2.0.8",
|
||||||
"@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.2.2",
|
"@tailwindcss/postcss": "^4.3.0",
|
||||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||||
"@types/node": "^22.19.17",
|
"@types/node": "^22.19.19",
|
||||||
"@types/node-forge": "^1.3.14",
|
"@types/node-forge": "^1.3.14",
|
||||||
"@types/pg": "^8.20.0",
|
"@types/pg": "^8.20.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.15",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@zenstackhq/openapi": "^2.22.1",
|
"@zenstackhq/openapi": "^2.22.1",
|
||||||
"@zenstackhq/tanstack-query": "^2.22.2",
|
"@zenstackhq/tanstack-query": "^2.22.2",
|
||||||
"baseline-browser-mapping": "^2.10.17",
|
"baseline-browser-mapping": "^2.10.32",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
"esbuild": "^0.27.7",
|
"esbuild": "^0.27.7",
|
||||||
"eslint": "^9.39.4",
|
"eslint": "^9.39.4",
|
||||||
"eslint-config-next": "^16.2.3",
|
"eslint-config-next": "^16.2.6",
|
||||||
"eslint-plugin-tailwindcss": "^3.18.2",
|
"eslint-plugin-tailwindcss": "^3.18.3",
|
||||||
"framer-motion": "^12.38.0",
|
"framer-motion": "^12.40.0",
|
||||||
"node-pty": "^1.1.0",
|
"node-pty": "^1.1.0",
|
||||||
"postcss": "8.5.10",
|
"postcss": "8.5.10",
|
||||||
"release-it": "^19.2.4",
|
"release-it": "^19.2.4",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.3.0",
|
||||||
"tsx": "^4.21.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"
|
"zenstack": "2.14.2"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.1.3"
|
"packageManager": "pnpm@11.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1441
-969
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@ export const PORTABASE_DEFAULT_SETTINGS = {
|
|||||||
"https://code.iconify.design",
|
"https://code.iconify.design",
|
||||||
"https://cdn.iconify.design",
|
"https://cdn.iconify.design",
|
||||||
"https://code.iconify.com",
|
"https://code.iconify.com",
|
||||||
|
"https://cdn.jsdelivr.net",
|
||||||
],
|
],
|
||||||
IMG_SRC: [
|
IMG_SRC: [
|
||||||
"'self'",
|
"'self'",
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { loggingMiddleware } from "@/middleware/loggingMiddleware";
|
|||||||
import { errorHandler } from "@/middleware/errorHandler";
|
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 {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();
|
||||||
@@ -17,11 +19,13 @@ export async function proxy(request: NextRequest) {
|
|||||||
new URL(`/login?redirect=${redirectUrl}`, request.url),
|
new URL(`/login?redirect=${redirectUrl}`, request.url),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (session.user.banned) {
|
const user = session.user as User
|
||||||
|
|
||||||
|
if (user.banned) {
|
||||||
await auth.api.signOut({ headers: await headers() });
|
await auth.api.signOut({ headers: await headers() });
|
||||||
return NextResponse.redirect(new URL("/login?error=banned", request.url));
|
return NextResponse.redirect(new URL("/login?error=banned", request.url));
|
||||||
}
|
}
|
||||||
if (session.user.role === "pending") {
|
if (user.role === "pending") {
|
||||||
await auth.api.signOut({ headers: await headers() });
|
await auth.api.signOut({ headers: await headers() });
|
||||||
return NextResponse.redirect(
|
return NextResponse.redirect(
|
||||||
new URL(`/login?error=pending?redirect=${redirectUrl}`, request.url),
|
new URL(`/login?error=pending?redirect=${redirectUrl}`, request.url),
|
||||||
@@ -38,6 +42,33 @@ export async function proxy(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (url.pathname.startsWith("/api")) {
|
if (url.pathname.startsWith("/api")) {
|
||||||
|
if (url.pathname.startsWith("/api/v1")) {
|
||||||
|
const apiEnabled = String(env.API_ENABLED) === "true";
|
||||||
|
if (!apiEnabled) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
message: "This API route does not exist.",
|
||||||
|
status: 404,
|
||||||
|
}),
|
||||||
|
{ status: 404, headers: { "Content-Type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const openapiEnabled = String(env.OPENAPI_ENABLED) === "true";
|
||||||
|
if (
|
||||||
|
!openapiEnabled &&
|
||||||
|
(url.pathname.startsWith("/api/v1/docs") ||
|
||||||
|
url.pathname.startsWith("/api/v1/openapi"))
|
||||||
|
) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
message: "This API route does not exist.",
|
||||||
|
status: 404,
|
||||||
|
}),
|
||||||
|
{ status: 404, headers: { "Content-Type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const routeExists = checkRouteExists(url.pathname);
|
const routeExists = checkRouteExists(url.pathname);
|
||||||
if (!routeExists) {
|
if (!routeExists) {
|
||||||
return new NextResponse(
|
return new NextResponse(
|
||||||
@@ -73,6 +104,19 @@ function checkRouteExists(pathname: string) {
|
|||||||
/^\/api\/config\/?$/,
|
/^\/api\/config\/?$/,
|
||||||
/^\/api\/health\/?$/,
|
/^\/api\/health\/?$/,
|
||||||
/^\/api\/google\/drive\/callback\/?$/,
|
/^\/api\/google\/drive\/callback\/?$/,
|
||||||
|
// v1 external API
|
||||||
|
/^\/api\/v1\/mcp\/?$/,
|
||||||
|
/^\/api\/v1\/docs\/?$/,
|
||||||
|
/^\/api\/v1\/openapi\/?$/,
|
||||||
|
/^\/api\/v1\/agents\/?$/,
|
||||||
|
/^\/api\/v1\/agents\/[^/]+\/?$/,
|
||||||
|
/^\/api\/v1\/agents\/[^/]+\/key\/?$/,
|
||||||
|
/^\/api\/v1\/databases\/?$/,
|
||||||
|
/^\/api\/v1\/databases\/[^/]+\/?$/,
|
||||||
|
/^\/api\/v1\/databases\/[^/]+\/backup\/?$/,
|
||||||
|
/^\/api\/v1\/databases\/[^/]+\/backup\/[^/]+\/?$/,
|
||||||
|
/^\/api\/v1\/databases\/[^/]+\/restore\/?$/,
|
||||||
|
/^\/api\/v1\/databases\/[^/]+\/status\/?$/,
|
||||||
];
|
];
|
||||||
return routePatterns.some((pattern) => pattern.test(pathname));
|
return routePatterns.some((pattern) => pattern.test(pathname));
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-59
@@ -1,62 +1,4 @@
|
|||||||
// "use client";
|
"use client"
|
||||||
//
|
|
||||||
// import { Button } from "@/components/ui/button";
|
|
||||||
// import { ButtonHTMLAttributes } from "react";
|
|
||||||
// import { Loader2 } from "lucide-react";
|
|
||||||
//
|
|
||||||
// export type VariantButton = {
|
|
||||||
// secondary: string;
|
|
||||||
// default: string;
|
|
||||||
// outline: string;
|
|
||||||
// ghost: string;
|
|
||||||
// link: string;
|
|
||||||
// destructive: string;
|
|
||||||
// };
|
|
||||||
// export type sizeButton = {
|
|
||||||
// default: string;
|
|
||||||
// icon: string;
|
|
||||||
// sm: string;
|
|
||||||
// lg: string;
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// export type ButtonWithConfirmProps = {
|
|
||||||
// icon?: any;
|
|
||||||
// text: string;
|
|
||||||
// variant?: keyof VariantButton;
|
|
||||||
// className?: string;
|
|
||||||
// onClick: () => void;
|
|
||||||
// isPending?: boolean;
|
|
||||||
// size: keyof sizeButton;
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// export const ButtonWithLoading = ({
|
|
||||||
// icon,
|
|
||||||
// text,
|
|
||||||
// variant,
|
|
||||||
// className,
|
|
||||||
// onClick,
|
|
||||||
// isPending,
|
|
||||||
// size,
|
|
||||||
// ...props // catch all remaining props
|
|
||||||
// }: ButtonWithConfirmProps & ButtonHTMLAttributes<HTMLButtonElement>) => {
|
|
||||||
// return (
|
|
||||||
// <Button
|
|
||||||
// onClick={() => {
|
|
||||||
// onClick();
|
|
||||||
// }}
|
|
||||||
// variant={variant ? variant : "default"}
|
|
||||||
// className={className}
|
|
||||||
// {...props} // forward the remaining props to the Button component
|
|
||||||
// size={size || "default"}
|
|
||||||
// >
|
|
||||||
// {isPending && <Loader2 className="animate-spin mr-4" size={16} />}
|
|
||||||
// {text}
|
|
||||||
// <>{icon ? icon : null}</>
|
|
||||||
// </Button>
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
|
||||||
'use client'
|
|
||||||
|
|
||||||
import { ButtonHTMLAttributes, ReactNode } from "react";
|
import { ButtonHTMLAttributes, ReactNode } from "react";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
+2
-2
@@ -3,8 +3,8 @@
|
|||||||
import React, { ComponentType, useState } from "react";
|
import React, { ComponentType, useState } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { PaginationNavigation } from "@/components/wrappers/common/pagination/pagination-navigation";
|
import { PaginationNavigation } from "@/components/common/pagination-navigation";
|
||||||
import { PaginationSize } from "@/components/wrappers/common/pagination/pagination-size";
|
import { PaginationSize } from "@/components/common/pagination-size";
|
||||||
|
|
||||||
interface CardsWithPaginationProps<T> {
|
interface CardsWithPaginationProps<T> {
|
||||||
className?: string;
|
className?: string;
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
import {PropsWithChildren, useState} from "react";
|
import {PropsWithChildren, useState} from "react";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {Check, Copy, Terminal} from "lucide-react";
|
import {Check, Copy, Terminal} from "lucide-react";
|
||||||
import {copyToClipboardWithMeta} from "@/components/wrappers/common/button/copy-button";
|
import {copyToClipboardWithMeta} from "@/components/common/copy-button";
|
||||||
import {cn} from "@/lib/utils";
|
import {cn} from "@/lib/utils";
|
||||||
|
|
||||||
export type CodeSnippetProps = PropsWithChildren<{
|
export type CodeSnippetProps = PropsWithChildren<{
|
||||||
+3
-21
@@ -4,35 +4,17 @@ import Link from "next/link";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {Card} from "@/components/ui/card";
|
import {Card} from "@/components/ui/card";
|
||||||
import {ConnectionIndicator} from "@/components/wrappers/common/connection-indicator";
|
import {ConnectionIndicator} from "@/components/common/connection-indicator";
|
||||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||||
import {Database} from "@/db/schema/07_database";
|
import {Database} from "@/db/schema/07_database";
|
||||||
import {ChevronRight, Activity, Fingerprint, Copy, Check} from "lucide-react";
|
import {ChevronRight, Activity, Fingerprint, Copy, Check} from "lucide-react";
|
||||||
|
|
||||||
export type projectDatabaseCardProps = {
|
export type DatabaseCardProps = {
|
||||||
data: Database;
|
|
||||||
extendedProps: any;
|
|
||||||
organizationSlug: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
|
|
||||||
const {data: database, extendedProps: extendedProps} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
className="group block transition-all duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl"
|
|
||||||
href={`/dashboard/projects/${extendedProps.id}/database/${database.id}`}>
|
|
||||||
<DatabaseCard data={database}/>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type databaseCardProps = {
|
|
||||||
data: Database;
|
data: Database;
|
||||||
withDetails?: boolean;
|
withDetails?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DatabaseCard = (props: databaseCardProps) => {
|
export const DatabaseCard = (props: DatabaseCardProps) => {
|
||||||
const {data: database, withDetails = true} = props;
|
const {data: database, withDetails = true} = props;
|
||||||
const [isCopied, setIsCopied] = useState(false);
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
|
||||||
+42
-10
@@ -1,5 +1,3 @@
|
|||||||
//https://github.com/sersavan/shadcn-multi-select-component
|
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { CheckIcon, XCircle, ChevronDown, XIcon, WandSparkles } from "lucide-react";
|
import { CheckIcon, XCircle, ChevronDown, XIcon, WandSparkles } from "lucide-react";
|
||||||
@@ -233,53 +231,87 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto p-0" align="start" onEscapeKeyDown={() => setIsPopoverOpen(false)}>
|
<PopoverContent
|
||||||
|
className="w-auto p-0"
|
||||||
|
align="start"
|
||||||
|
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput placeholder="Search..." onKeyDown={handleInputKeyDown} />
|
<CommandInput placeholder="Search..." onKeyDown={handleInputKeyDown} />
|
||||||
<CommandList>
|
<CommandList className="max-h-none overflow-visible">
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
|
<div
|
||||||
|
className="max-h-[18rem] overflow-y-auto overflow-x-hidden overscroll-contain touch-pan-y pr-1"
|
||||||
|
style={{ WebkitOverflowScrolling: "touch" }}
|
||||||
|
onWheelCapture={(event) => event.stopPropagation()}
|
||||||
|
onTouchMoveCapture={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
<CommandItem key="all" onSelect={toggleAll} className="cursor-pointer">
|
<CommandItem key="all" onSelect={toggleAll} className="cursor-pointer">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||||
selectedValues.length === options.length ? "bg-primary text-primary-foreground" : "opacity-50 [&_svg]:invisible"
|
selectedValues.length === options.length
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "opacity-50 [&_svg]:invisible"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CheckIcon className="h-4 w-4" />
|
<CheckIcon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<span>(Select All)</span>
|
<span>(Select All)</span>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
|
|
||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
const isSelected = selectedValues.includes(option.value);
|
const isSelected = selectedValues.includes(option.value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandItem key={option.value} onSelect={() => toggleOption(option.value)} className="cursor-pointer">
|
<CommandItem
|
||||||
|
key={option.value}
|
||||||
|
onSelect={() => toggleOption(option.value)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||||
isSelected ? "bg-primary text-primary-foreground" : "opacity-50 [&_svg]:invisible"
|
isSelected
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "opacity-50 [&_svg]:invisible"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CheckIcon className="h-4 w-4" />
|
<CheckIcon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
{option.icon && <option.icon className="mr-2 h-4 w-4 text-muted-foreground" />}
|
|
||||||
|
{option.icon && (
|
||||||
|
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
|
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{selectedValues.length > 0 && (
|
{selectedValues.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<CommandItem onSelect={handleClear} className="flex-1 justify-center cursor-pointer">
|
<CommandItem
|
||||||
|
onSelect={handleClear}
|
||||||
|
className="flex-1 justify-center cursor-pointer"
|
||||||
|
>
|
||||||
Clear
|
Clear
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<Separator orientation="vertical" className="flex min-h-6 h-full" />
|
<Separator orientation="vertical" className="flex min-h-6 h-full" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<CommandItem onSelect={() => setIsPopoverOpen(false)} className="flex-1 justify-center cursor-pointer max-w-full">
|
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => setIsPopoverOpen(false)}
|
||||||
|
className="flex-1 justify-center cursor-pointer max-w-full"
|
||||||
|
>
|
||||||
Close
|
Close
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</div>
|
</div>
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
|
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
|
||||||
import { PaginationIndexes } from "@/components/wrappers/common/pagination/pagination-indexes";
|
import { PaginationIndexes } from "@/components/common/pagination-indexes";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export type paginationNavigationProps = {
|
export type paginationNavigationProps = {
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { PaginationNavigation } from "@/components/wrappers/common/pagination/pagination-navigation";
|
import { PaginationNavigation } from "@/components/common/pagination-navigation";
|
||||||
|
|
||||||
export type paginationNavigationProps = {
|
export type paginationNavigationProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {TablePaginationNavigation} from "@/components/wrappers/common/table/table-pagination-navigation";
|
import {TablePaginationNavigation} from "@/components/common/table-pagination-navigation";
|
||||||
import {TablePaginationSize} from "@/components/wrappers/common/table/table-pagination-size";
|
import {TablePaginationSize} from "@/components/common/table-pagination-size";
|
||||||
import {cn} from "@/lib/utils";
|
import {cn} from "@/lib/utils";
|
||||||
|
|
||||||
interface tablePaginationProps {
|
interface tablePaginationProps {
|
||||||
@@ -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/types";
|
// import type {EventPayload} from "@/features/notifications/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/types";
|
import type {EventPayload} from "@/features/notifications/notifications.types";
|
||||||
|
|
||||||
export interface EmailNotificationProps {
|
export interface EmailNotificationProps {
|
||||||
payload: EventPayload;
|
payload: EventPayload;
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {useRouter} from "next/navigation";
|
|
||||||
import {useMutation} from "@tanstack/react-query";
|
|
||||||
import {Form, FormControl, FormField, FormItem, FormLabel, useZodForm} from "@/components/ui/form";
|
|
||||||
import {PasswordStrengthInput} from "@/components/ui/password-input-indicator";
|
|
||||||
import {ResetPasswordSchema, ResetPasswordType} from "@/components/wrappers/auth/reset-password/reset-password-schema";
|
|
||||||
import {PasswordInput} from "@/components/ui/password-input";
|
|
||||||
import {CardContent, CardHeader} from "@/components/ui/card";
|
|
||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
|
||||||
import {authClient} from "@/lib/auth/auth-client";
|
|
||||||
import {toast} from "sonner";
|
|
||||||
import {CardAuth} from "@/features/layout/card-auth";
|
|
||||||
|
|
||||||
type ResetPasswordFormProps = {
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ResetPasswordForm = ({token}: ResetPasswordFormProps) => {
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const form = useZodForm({
|
|
||||||
schema: ResetPasswordSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mutationResetPassword = useMutation({
|
|
||||||
mutationFn: async (data: ResetPasswordType) => {
|
|
||||||
|
|
||||||
await authClient.resetPassword({
|
|
||||||
newPassword: data.password,
|
|
||||||
token,
|
|
||||||
}, {
|
|
||||||
onSuccess: (response) => {
|
|
||||||
toast.success("Password changed successfully.");
|
|
||||||
setTimeout(() => router.push("/"), 1000);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error(error);
|
|
||||||
toast.error(error.error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CardAuth>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="grid gap-2 text-center mb-2">
|
|
||||||
<h1 className="text-3xl font-bold">Reset Password</h1>
|
|
||||||
<p className="text-balance text-muted-foreground">Fill information below to change your
|
|
||||||
password</p>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
className="flex flex-col gap-4"
|
|
||||||
onSubmit={async (values) => {
|
|
||||||
await mutationResetPassword.mutateAsync(values);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="password"
|
|
||||||
defaultValue=""
|
|
||||||
render={({field}) => (
|
|
||||||
<FormItem>
|
|
||||||
<PasswordStrengthInput label={"Enter new password"} field={field}/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="confirmPassword"
|
|
||||||
defaultValue=""
|
|
||||||
render={({field}) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Confirmation Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<PasswordInput placeholder={"Confirm password"} {...field}
|
|
||||||
value={field.value ?? ""}/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ButtonWithLoading type="submit" isPending={mutationResetPassword.isPending}>
|
|
||||||
Validate
|
|
||||||
</ButtonWithLoading>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
</CardAuth>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import {useRouter, useSearchParams} from "next/navigation";
|
|
||||||
import {ResetPasswordForm} from "@/components/wrappers/auth/reset-password/reset-password-form";
|
|
||||||
import {toast} from "sonner";
|
|
||||||
import {useEffect} from "react";
|
|
||||||
import {LoadingSpinner} from "@/components/wrappers/common/loading/loading-spinner";
|
|
||||||
|
|
||||||
export const ResetPasswordSection = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
const token = searchParams.get("token");
|
|
||||||
const error = searchParams.get("error");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (error || !token) {
|
|
||||||
if (error == "INVALID_TOKEN") {
|
|
||||||
toast.error("Invalid reset token");
|
|
||||||
} else {
|
|
||||||
toast.error("An error occurred.");
|
|
||||||
}
|
|
||||||
setTimeout(() => router.push("/"), 1000);
|
|
||||||
}
|
|
||||||
}, [error, token, router]);
|
|
||||||
|
|
||||||
|
|
||||||
if (!token) return (
|
|
||||||
<div className="w-full h-full flex flex-col items-center justify-center">
|
|
||||||
<LoadingSpinner size={50}/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResetPasswordForm token={token}/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-72
@@ -1,72 +0,0 @@
|
|||||||
import { UseFormReturn } from "react-hook-form";
|
|
||||||
import {
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import { PasswordInput } from "@/components/ui/password-input";
|
|
||||||
|
|
||||||
type NotifierWebhookFormProps = {
|
|
||||||
form: UseFormReturn<any, any, any>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NotifierWebhookForm = ({ form }: NotifierWebhookFormProps) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Separator className="my-1" />
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="config.webhookUrl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Webhook URL *</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="e.g. https://example.com/api/webhook" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<div className="flex-1">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="config.webhookSecretHeader"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Header Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} placeholder="e.g. X-Webhook-Secret" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="config.webhookSecret"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Secret Value</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<PasswordInput {...field} placeholder="e.g. webhook-secret" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground mt-2">
|
|
||||||
If provided, the secret will be sent in the specified header (defaults
|
|
||||||
to <code>X-Webhook-Secret</code>).
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
import {z} from "zod";
|
|
||||||
|
|
||||||
export const WebhookChannelConfigSchema = z.object({
|
|
||||||
webhookUrl: z.string().url("Must be a valid URL"),
|
|
||||||
webhookSecretHeader: z.string().optional(),
|
|
||||||
webhookSecret: z.string().optional(),
|
|
||||||
});
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {Database} from "@/db/schema/07_database";
|
|
||||||
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
|
|
||||||
|
|
||||||
export type agentDatabaseCardProps = {
|
|
||||||
data: Database;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AgentDatabaseCard = (props: agentDatabaseCardProps) => {
|
|
||||||
const {data: database} = props;
|
|
||||||
|
|
||||||
return <DatabaseCard withDetails={false} data={database}/>;
|
|
||||||
|
|
||||||
//Todo: Add again when impersonation system will be implemented
|
|
||||||
/* if (!database.projectId) {
|
|
||||||
return <DatabaseCard data={database}/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link className="group block transition-all duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl"
|
|
||||||
href={`/dashboard/projects/${database.projectId}/database/${database.id}`}>
|
|
||||||
<DatabaseCard data={database}/>
|
|
||||||
</Link>
|
|
||||||
);*/
|
|
||||||
};
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
"use server";
|
|
||||||
|
|
||||||
import {z} from "zod";
|
|
||||||
import {v4 as uuidv4} from "uuid";
|
|
||||||
import {ServerActionResult} from "@/types/action-type";
|
|
||||||
import {and, eq, inArray} from "drizzle-orm";
|
|
||||||
import {db} from "@/db";
|
|
||||||
import * as drizzleDb from "@/db";
|
|
||||||
import {Agent} from "@/db/schema/08_agent";
|
|
||||||
import {userAction} from "@/lib/safe-actions/actions";
|
|
||||||
import {zString} from "@/lib/zod";
|
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
|
||||||
|
|
||||||
export const deleteAgentAction = userAction
|
|
||||||
.schema(
|
|
||||||
z.object({
|
|
||||||
agentId: zString(),
|
|
||||||
organizationId: zString().optional(),
|
|
||||||
organizationIds: z.array(z.string()).optional()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.action(async ({parsedInput}): Promise<ServerActionResult<Agent>> => {
|
|
||||||
const {agentId, organizationId, organizationIds} = parsedInput;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let projectIds: string[] = [];
|
|
||||||
|
|
||||||
const uuid = uuidv4();
|
|
||||||
if (organizationId) {
|
|
||||||
await db
|
|
||||||
.delete(drizzleDb.schemas.organizationAgent)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(drizzleDb.schemas.organizationAgent.organizationId, organizationId),
|
|
||||||
eq(drizzleDb.schemas.organizationAgent.agentId, agentId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const organization = await db.query.organization.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.organization.id, organizationId),
|
|
||||||
with: {
|
|
||||||
projects: true,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
projectIds = organization?.projects?.map(project => project.id) ?? [];
|
|
||||||
|
|
||||||
|
|
||||||
} else if (organizationIds) {
|
|
||||||
|
|
||||||
const organizationsToRemoveDetails = await db.query.organization.findMany({
|
|
||||||
where: inArray(drizzleDb.schemas.organization.id, organizationIds),
|
|
||||||
with: {
|
|
||||||
projects: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
projectIds = organizationsToRemoveDetails.flatMap(org =>
|
|
||||||
org.projects.map(project => project.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (projectIds?.length > 0) {
|
|
||||||
const databases = await db.query.database.findMany({
|
|
||||||
where: (db, {inArray}) => inArray(db.projectId, projectIds),
|
|
||||||
columns: {id: true}
|
|
||||||
});
|
|
||||||
|
|
||||||
const databaseIds = databases.map(d => d.id);
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(drizzleDb.schemas.database)
|
|
||||||
.set(withUpdatedAt({
|
|
||||||
backupPolicy: null,
|
|
||||||
projectId: null
|
|
||||||
}))
|
|
||||||
.where(inArray(drizzleDb.schemas.database.projectId, projectIds))
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.delete(drizzleDb.schemas.retentionPolicy)
|
|
||||||
.where(inArray(drizzleDb.schemas.retentionPolicy.databaseId, databaseIds))
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.delete(drizzleDb.schemas.alertPolicy)
|
|
||||||
.where(inArray(drizzleDb.schemas.alertPolicy.databaseId, databaseIds))
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.delete(drizzleDb.schemas.storagePolicy)
|
|
||||||
.where(inArray(drizzleDb.schemas.storagePolicy.databaseId, databaseIds))
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const updatedAgent = await db
|
|
||||||
.update(drizzleDb.schemas.agent)
|
|
||||||
.set(withUpdatedAt({
|
|
||||||
isArchived: true,
|
|
||||||
slug: uuid,
|
|
||||||
deletedAt: new Date()
|
|
||||||
}))
|
|
||||||
.where(eq(drizzleDb.schemas.agent.id, agentId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
|
|
||||||
if (!updatedAgent[0]) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
actionError: {
|
|
||||||
message: "Agent not found or update failed",
|
|
||||||
status: 404,
|
|
||||||
messageParams: {agentId: agentId},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: updatedAgent[0],
|
|
||||||
actionSuccess: {
|
|
||||||
message: "Agent has been successfully deleted.",
|
|
||||||
messageParams: {projectId: agentId},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
actionError: {
|
|
||||||
message: "Failed to delete agent.",
|
|
||||||
status: 500,
|
|
||||||
cause: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
messageParams: {agentId: agentId},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import {useSidebar} from "@/components/ui/sidebar";
|
|
||||||
import {useTheme} from "next-themes";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export type SideBarLogoProps = {};
|
|
||||||
|
|
||||||
export const SideBarLogo = (props: SideBarLogoProps) => {
|
|
||||||
const {state, isMobile} = useSidebar();
|
|
||||||
const {resolvedTheme} = useTheme();
|
|
||||||
return state === "collapsed" && !isMobile ? (
|
|
||||||
<Link href={"/dashboard"}>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<Image
|
|
||||||
src={"/images/logo/picto.png"}
|
|
||||||
alt="Logo"
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
loading="eager"
|
|
||||||
className="object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Link href={"/dashboard"}>
|
|
||||||
|
|
||||||
<div className="w-full h-auto p-4 flex items-center justify-center">
|
|
||||||
<Image
|
|
||||||
loading="eager"
|
|
||||||
src={"/images/logo/logo.png"}
|
|
||||||
alt="Logo"
|
|
||||||
width={190}
|
|
||||||
height={190}
|
|
||||||
className="object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const ImmediateExecutionSchema = z.object({
|
|
||||||
executionMode: z.literal("immediate"),
|
|
||||||
});
|
|
||||||
|
|
||||||
const ScheduledExecutionSchema = z.object({
|
|
||||||
executionMode: z.literal("scheduled"),
|
|
||||||
scheduledDatetime: z.date(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const RemoteImmediateSchema = z
|
|
||||||
.object({
|
|
||||||
backupLocation: z.literal("remote-file"),
|
|
||||||
})
|
|
||||||
.merge(ImmediateExecutionSchema);
|
|
||||||
|
|
||||||
const RemoteScheduledSchema = z
|
|
||||||
.object({
|
|
||||||
backupLocation: z.literal("remote-file"),
|
|
||||||
})
|
|
||||||
.merge(ScheduledExecutionSchema);
|
|
||||||
|
|
||||||
const DesktopImmediateSchema = z
|
|
||||||
.object({
|
|
||||||
backupLocation: z.literal("desktop-file"),
|
|
||||||
uploadedBackupFile: z.instanceof(File),
|
|
||||||
})
|
|
||||||
.merge(ImmediateExecutionSchema);
|
|
||||||
|
|
||||||
const DesktopScheduledSchema = z
|
|
||||||
.object({
|
|
||||||
backupLocation: z.literal("desktop-file"),
|
|
||||||
uploadedBackupFile: z.instanceof(File),
|
|
||||||
})
|
|
||||||
.merge(ScheduledExecutionSchema);
|
|
||||||
|
|
||||||
export const RestoreSchema = z.union([RemoteImmediateSchema, RemoteScheduledSchema, DesktopImmediateSchema, DesktopScheduledSchema]);
|
|
||||||
export type RestoreType = z.infer<typeof RestoreSchema>;
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
// "use client";
|
|
||||||
//
|
|
||||||
// import { useState } from "react";
|
|
||||||
//
|
|
||||||
// import { DateTimePicker } from "@/components/wrappers/common/day-time-picker";
|
|
||||||
// import { Button } from "@/components/ui/button";
|
|
||||||
// import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
|
||||||
// import { Input } from "@/components/ui/input";
|
|
||||||
// import { RestoreSchema } from "@/components/wrappers/dashboard/database/restore-form.schema";
|
|
||||||
// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
// import { ComboBox } from "@/components/wrappers/common/combobox";
|
|
||||||
// import { Label } from "@/components/ui/label";
|
|
||||||
//
|
|
||||||
// export type restoreFormProps = {
|
|
||||||
// databaseToRestore: any;
|
|
||||||
// databases: any[];
|
|
||||||
// backups: any[];
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// export const RestoreForm = (props: restoreFormProps) => {
|
|
||||||
// const { databaseToRestore, databases, backups } = props;
|
|
||||||
//
|
|
||||||
// const backupLocations = [
|
|
||||||
// {
|
|
||||||
// value: "remote-file",
|
|
||||||
// label: "Remote File",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// value: "desktop-file",
|
|
||||||
// label: "Desktop File",
|
|
||||||
// },
|
|
||||||
// ];
|
|
||||||
//
|
|
||||||
// const executionModes = [
|
|
||||||
// {
|
|
||||||
// value: "immediate",
|
|
||||||
// label: "Immediate",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// value: "scheduled",
|
|
||||||
// label: "Scheduled",
|
|
||||||
// },
|
|
||||||
// ];
|
|
||||||
//
|
|
||||||
// const [selectedDatabaseId, setSelectedDatabaseId] = useState(databaseToRestore.id);
|
|
||||||
//
|
|
||||||
// const filteredBackups = backups.filter((backup) => backup.databaseId == selectedDatabaseId);
|
|
||||||
//
|
|
||||||
// const defaultValues = {
|
|
||||||
// executionMode: "immediate" as const,
|
|
||||||
// backupLocation: "remote-file" as const,
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// const form = useZodForm({
|
|
||||||
// schema: RestoreSchema,
|
|
||||||
// defaultValues: defaultValues,
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// const values = form.getValues();
|
|
||||||
//
|
|
||||||
// return (
|
|
||||||
// <div>
|
|
||||||
// <Form
|
|
||||||
// form={form}
|
|
||||||
// onSubmit={async (values) => {
|
|
||||||
// console.log(values);
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// <FormField
|
|
||||||
// control={form.control}
|
|
||||||
// name="backupLocation"
|
|
||||||
// render={({ field }) => (
|
|
||||||
// <FormItem>
|
|
||||||
// <FormLabel>Backup location</FormLabel>
|
|
||||||
// <Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
||||||
// <FormControl>
|
|
||||||
// <SelectTrigger>
|
|
||||||
// <SelectValue />
|
|
||||||
// </SelectTrigger>
|
|
||||||
// </FormControl>
|
|
||||||
// <SelectContent>
|
|
||||||
// {backupLocations.map((backupLocation, key) => (
|
|
||||||
// <SelectItem key={key} value={backupLocation.value}>
|
|
||||||
// {backupLocation.label}
|
|
||||||
// </SelectItem>
|
|
||||||
// ))}
|
|
||||||
// </SelectContent>
|
|
||||||
// </Select>
|
|
||||||
// <FormMessage />
|
|
||||||
// </FormItem>
|
|
||||||
// )}
|
|
||||||
// />
|
|
||||||
//
|
|
||||||
// {values.backupLocation == "remote-file" ? (
|
|
||||||
// <>
|
|
||||||
// <div className="flex flex-col">
|
|
||||||
// <Label>Database</Label>
|
|
||||||
// <ComboBox
|
|
||||||
// values={databases.map((database) => ({ value: database.id, label: database.name }))}
|
|
||||||
// onValueChange={setSelectedDatabaseId}
|
|
||||||
// defaultValue={selectedDatabaseId}
|
|
||||||
// searchField
|
|
||||||
// />
|
|
||||||
// </div>
|
|
||||||
// <FormField
|
|
||||||
// control={form.control}
|
|
||||||
// name="remoteBackup"
|
|
||||||
// render={({ field }) => (
|
|
||||||
// <FormItem className="flex flex-col">
|
|
||||||
// <FormLabel>Remote backup</FormLabel>
|
|
||||||
// <ComboBoxFormItem
|
|
||||||
// values={filteredBackups.map((backup) => ({
|
|
||||||
// value: backup.id,
|
|
||||||
// label: backup.createdAt.toString(),
|
|
||||||
// }))}
|
|
||||||
// {...field}
|
|
||||||
// />
|
|
||||||
// <FormMessage />
|
|
||||||
// </FormItem>
|
|
||||||
// )}
|
|
||||||
// />
|
|
||||||
// </>
|
|
||||||
// ) : null}
|
|
||||||
//
|
|
||||||
// {values.backupLocation == "desktop-file" ? (
|
|
||||||
// <FormField
|
|
||||||
// control={form.control}
|
|
||||||
// name="uploadedBackupFile"
|
|
||||||
// render={({ field }) => (
|
|
||||||
// <FormItem>
|
|
||||||
// <FormLabel>File</FormLabel>
|
|
||||||
// <FormControl>
|
|
||||||
// <Input id="uploadedBackupFile" type="file" {...field} />
|
|
||||||
// </FormControl>
|
|
||||||
// <FormMessage />
|
|
||||||
// </FormItem>
|
|
||||||
// )}
|
|
||||||
// />
|
|
||||||
// ) : null}
|
|
||||||
//
|
|
||||||
// <FormField
|
|
||||||
// control={form.control}
|
|
||||||
// name="executionMode"
|
|
||||||
// render={({ field }) => (
|
|
||||||
// <FormItem>
|
|
||||||
// <FormLabel>Execution mode</FormLabel>
|
|
||||||
// <Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
||||||
// <FormControl>
|
|
||||||
// <SelectTrigger>
|
|
||||||
// <SelectValue />
|
|
||||||
// </SelectTrigger>
|
|
||||||
// </FormControl>
|
|
||||||
// <SelectContent>
|
|
||||||
// {executionModes.map((executionMode, key) => (
|
|
||||||
// <SelectItem key={key} value={executionMode.value}>
|
|
||||||
// {executionMode.label}
|
|
||||||
// </SelectItem>
|
|
||||||
// ))}
|
|
||||||
// </SelectContent>
|
|
||||||
// </Select>
|
|
||||||
// <FormMessage />
|
|
||||||
// </FormItem>
|
|
||||||
// )}
|
|
||||||
// />
|
|
||||||
//
|
|
||||||
// {values.executionMode == "scheduled" ? (
|
|
||||||
// <FormField
|
|
||||||
// control={form.control}
|
|
||||||
// name="scheduledDatetime"
|
|
||||||
// render={({ field }) => (
|
|
||||||
// <FormItem>
|
|
||||||
// <FormLabel>Scheduled date</FormLabel>
|
|
||||||
// <FormControl>
|
|
||||||
// <DateTimePicker value={field.value} onSelect={field.onChange} />
|
|
||||||
// </FormControl>
|
|
||||||
// <FormMessage />
|
|
||||||
// </FormItem>
|
|
||||||
// )}
|
|
||||||
// />
|
|
||||||
// ) : null}
|
|
||||||
//
|
|
||||||
// <Button type="submit">Launch restore</Button>
|
|
||||||
// </Form>
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user