mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebdef2d302 | ||
|
|
a6bb50c5e2 | ||
|
|
50eb48fcf9 | ||
|
|
6622565b14 | ||
|
|
f001de1e97 | ||
|
|
6711fab413 | ||
|
|
e2f28a84bc | ||
|
|
d1941c2f3d | ||
|
|
f35e6abb05 | ||
|
|
0598f78a34 | ||
|
|
f30b55ba47 | ||
|
|
751f29e340 | ||
|
|
2e629f8423 | ||
|
|
ae431f5b85 | ||
|
|
f5f697ceb0 | ||
|
|
02a8f0baf9 | ||
|
|
2932149b92 | ||
|
|
e1e4d67114 | ||
|
|
1e3f112d9c | ||
|
|
e3600949b0 | ||
|
|
856c80cc20 | ||
|
|
8c77796f91 | ||
|
|
dbeaaa4bac | ||
|
|
f9986e1b7f | ||
|
|
895008ba1d | ||
|
|
09791ae86e | ||
|
|
564dee512f | ||
|
|
56a9922057 | ||
|
|
44f0112620 | ||
|
|
2d7a5ce264 | ||
|
|
48f8d00d2e | ||
|
|
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 | ||
|
|
8db995010f | ||
|
|
539fec4dbc | ||
|
|
3fa0f9b76d | ||
|
|
41d8b2c375 | ||
|
|
4fadfcfaf2 | ||
|
|
af3df7f72a | ||
|
|
3b8fb93fb2 | ||
|
|
2cd5bda416 | ||
|
|
965b215224 | ||
|
|
27f6b746e0 | ||
|
|
7d463d7da8 | ||
|
|
55be9374e1 | ||
|
|
ce1bade773 | ||
|
|
332540488d | ||
|
|
ab52ef6c27 | ||
|
|
9fe046617c | ||
|
|
01763a6b36 | ||
|
|
b28bb44f97 | ||
|
|
411d0b4565 | ||
|
|
2d55618898 | ||
|
|
6b3df405eb | ||
|
|
e91ef4c142 | ||
|
|
9b504ccd68 | ||
|
|
dd3f4411cf | ||
|
|
0ab94b6221 | ||
|
|
8477889cea | ||
|
|
2eb4a7a0fe | ||
|
|
1f68adda90 | ||
|
|
16697c442c | ||
|
|
ffba2c56a3 | ||
|
|
a3b7af3ba5 | ||
|
|
33492b8f3a | ||
|
|
150ab83f73 | ||
|
|
35db7a4416 | ||
|
|
1ce93af144 | ||
|
|
f410a8fdd5 | ||
|
|
88914169bc | ||
|
|
8d51bcb74d | ||
|
|
5a87529934 | ||
|
|
0d7a41996e | ||
|
|
dc97de03d0 | ||
|
|
5d4a1248e4 | ||
|
|
f876d8eb09 | ||
|
|
1d1d92a5cf |
+15
-1
@@ -15,6 +15,15 @@ PROJECT_NAME="Portabase"
|
|||||||
PROJECT_URL=http://localhost:8887
|
PROJECT_URL=http://localhost:8887
|
||||||
PROJECT_SECRET=
|
PROJECT_SECRET=
|
||||||
|
|
||||||
|
AUTH_DEFAULT_USER_NAME="Portabase Admin"
|
||||||
|
AUTH_DEFAULT_USER=user@example.com
|
||||||
|
# Password must contain at least 8 characters
|
||||||
|
# Password must contain at least 1 number
|
||||||
|
# Password must contain at least 1 lowercase letter
|
||||||
|
# Password must contain at least 1 uppercase letter
|
||||||
|
# Password must contain at least 1 special character
|
||||||
|
AUTH_DEFAULT_PASSWORD=testPASS123456!
|
||||||
|
|
||||||
# SMTP (email)
|
# SMTP (email)
|
||||||
SMTP_HOST=
|
SMTP_HOST=
|
||||||
SMTP_PORT=
|
SMTP_PORT=
|
||||||
@@ -45,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=""
|
||||||
@@ -67,3 +75,9 @@ 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
|
||||||
|
|
||||||
|
# Default to false
|
||||||
|
#TUSD_BEHIND_PROXY=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"
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
create-release:
|
create-release:
|
||||||
needs: check-skip
|
needs: check-skip
|
||||||
if: ${{ needs.check-skip.outputs.skip == 'false' }}
|
if: ${{ needs.check-skip.outputs.skip == 'false' && github.event.pull_request.merged == true }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
draft_tag: ${{ steps.release_step.outputs.draft_tag }}
|
draft_tag: ${{ steps.release_step.outputs.draft_tag }}
|
||||||
|
|||||||
@@ -59,3 +59,4 @@ certificates
|
|||||||
.claude
|
.claude
|
||||||
.codex
|
.codex
|
||||||
/docs
|
/docs
|
||||||
|
.worktrees
|
||||||
|
|||||||
+1
-1
@@ -33,5 +33,5 @@ keywords:
|
|||||||
- web-ui
|
- web-ui
|
||||||
- agent
|
- agent
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
version: 1.14.1
|
version: 1.18.1
|
||||||
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<{}>) {
|
||||||
@@ -30,9 +30,11 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
<PageHeader className="flex flex-col">
|
<PageHeader className="flex flex-col">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<PageTitle className="mb-3">Active users</PageTitle>
|
<PageTitle className="mb-3">Active users</PageTitle>
|
||||||
<PageActions>
|
{isPasswordAuthEnabled && (
|
||||||
<AdminUserAddModal organizations={organizations}/>
|
<PageActions>
|
||||||
</PageActions>
|
<AdminUserAddModal organizations={organizations}/>
|
||||||
|
</PageActions>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="flex flex-col gap-5">
|
<PageContent className="flex flex-col gap-5">
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
+30
-23
@@ -4,11 +4,12 @@ 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";
|
||||||
|
import {LogsModalProvider} from "@/features/logs/logs-modal-context";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
export default async function RoutePage(props: PageParams<{
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -34,7 +35,7 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
project: true,
|
project: true,
|
||||||
retentionPolicy: true,
|
retentionPolicy: true,
|
||||||
alertPolicies: true,
|
alertPolicies: true,
|
||||||
storagePolicies: true
|
storagePolicies: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,13 +51,17 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
with: {
|
with: {
|
||||||
storageChannel: true
|
storageChannel: true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
logs: true
|
||||||
},
|
},
|
||||||
orderBy: (b, {desc}) => [desc(b.createdAt)],
|
orderBy: (b, {desc}) => [desc(b.createdAt)],
|
||||||
});
|
});
|
||||||
|
|
||||||
const restorations = await db.query.restoration.findMany({
|
const restorations = await db.query.restoration.findMany({
|
||||||
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
|
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
|
||||||
|
with: {
|
||||||
|
logs: true
|
||||||
|
},
|
||||||
orderBy: (r, {desc}) => [desc(r.createdAt)],
|
orderBy: (r, {desc}) => [desc(r.createdAt)],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,7 +89,7 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const databaseHealthLogs = dbItem ? await getHealthLast12hLogs({ id: dbItem.id }) : []
|
const databaseHealthLogs = dbItem ? await getHealthLast12hLogs({id: dbItem.id}) : []
|
||||||
|
|
||||||
|
|
||||||
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||||
@@ -93,23 +98,25 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<BackupModalProvider>
|
<LogsModalProvider>
|
||||||
<DatabaseContent
|
<BackupModalProvider>
|
||||||
activeMember={activeMember}
|
<DatabaseContent
|
||||||
settings={settings}
|
activeMember={activeMember}
|
||||||
database={dbItem}
|
settings={settings}
|
||||||
databaseHealthLogs={databaseHealthLogs}
|
database={dbItem}
|
||||||
isAlreadyRestore={isAlreadyRestore}
|
databaseHealthLogs={databaseHealthLogs}
|
||||||
restorations={restorations}
|
isAlreadyRestore={isAlreadyRestore}
|
||||||
backups={backups}
|
restorations={restorations}
|
||||||
totalBackups={totalBackups}
|
backups={backups}
|
||||||
availableBackups={availableBackups}
|
totalBackups={totalBackups}
|
||||||
successRate={successRate}
|
availableBackups={availableBackups}
|
||||||
organizationId={organization.id}
|
successRate={successRate}
|
||||||
activeOrganizationChannels={[]}
|
organizationId={organization.id}
|
||||||
activeOrganizationStorageChannels={[]}
|
activeOrganizationChannels={[]}
|
||||||
/>
|
activeOrganizationStorageChannels={[]}
|
||||||
</BackupModalProvider>
|
/>
|
||||||
|
</BackupModalProvider>
|
||||||
|
</LogsModalProvider>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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";
|
||||||
@@ -85,7 +85,9 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
<PageContent className="flex flex-col w-full h-full">
|
<PageContent className="flex flex-col w-full h-full">
|
||||||
{proj.databases.length > 0 ? (
|
{proj.databases.length > 0 ? (
|
||||||
<CardsWithPagination
|
<CardsWithPagination
|
||||||
data={proj.databases}
|
data={[...proj.databases].sort((a, b) =>
|
||||||
|
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||||
|
)}
|
||||||
organizationSlug={organization.slug}
|
organizationSlug={organization.slug}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
cardItem={ProjectDatabaseCard}
|
cardItem={ProjectDatabaseCard}
|
||||||
|
|||||||
@@ -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,10 +5,11 @@ 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";
|
||||||
|
import {JobLogEntry} from "@/features/logs/types";
|
||||||
|
|
||||||
const log = logger.child({module: "api/agent/backup/route"});
|
const log = logger.child({module: "api/agent/backup/route"});
|
||||||
|
|
||||||
@@ -22,6 +23,8 @@ export type BodyPatch = {
|
|||||||
status: "success" | "failed"
|
status: "success" | "failed"
|
||||||
size: number
|
size: number
|
||||||
generatedId: string
|
generatedId: string
|
||||||
|
logs: JobLogEntry[]
|
||||||
|
durationMs: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
||||||
@@ -30,6 +33,7 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const body: BodyPost = await request.json();
|
const body: BodyPost = await request.json();
|
||||||
|
|
||||||
const method = body.method
|
const method = body.method
|
||||||
const database = await getDatabaseOrThrow(body.generatedId);
|
const database = await getDatabaseOrThrow(body.generatedId);
|
||||||
|
|
||||||
@@ -127,12 +131,37 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
.update(drizzleDb.schemas.backup)
|
.update(drizzleDb.schemas.backup)
|
||||||
.set(withUpdatedAt({
|
.set(withUpdatedAt({
|
||||||
status: status,
|
status: status,
|
||||||
fileSize: backupSize
|
fileSize: backupSize,
|
||||||
|
durationMs: body.durationMs
|
||||||
}))
|
}))
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup.id))
|
.where(eq(drizzleDb.schemas.backup.id, backup.id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
|
const logsToInsert = body.logs.map((entry) => ({
|
||||||
|
backupId: backup.id,
|
||||||
|
restorationId: null,
|
||||||
|
|
||||||
|
loggedAt: new Date(entry.timestamp),
|
||||||
|
|
||||||
|
entryType: entry.type,
|
||||||
|
level: entry.level,
|
||||||
|
|
||||||
|
message: entry.message,
|
||||||
|
command: entry.command ?? null,
|
||||||
|
output: entry.output ?? null,
|
||||||
|
|
||||||
|
exitCode: entry.exit_code ?? null,
|
||||||
|
durationMs: entry.duration_ms ?? null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (logsToInsert.length > 0) {
|
||||||
|
await dbClient
|
||||||
|
.insert(drizzleDb.schemas.jobLog)
|
||||||
|
.values(logsToInsert);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
eventEmitter.emit('modification', {update: true});
|
||||||
await sendNotificationsBackupRestore(database, status == "failed" ? "error_backup" : "success_backup" as EventKind);
|
await sendNotificationsBackupRestore(database, status == "failed" ? "error_backup" : "success_backup" as EventKind);
|
||||||
|
|
||||||
|
|||||||
@@ -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"});
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import {db as dbClient, db} from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/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";
|
||||||
|
import {JobLogEntry} from "@/features/logs/types";
|
||||||
|
|
||||||
const log = logger.child({module: "api/agent/restore"});
|
const log = logger.child({module: "api/agent/restore"});
|
||||||
|
|
||||||
export type BodyResultRestore = {
|
export type BodyResultRestore = {
|
||||||
generatedId: string
|
generatedId: string
|
||||||
status: string
|
status: string
|
||||||
|
logs: JobLogEntry[]
|
||||||
|
durationMs: number
|
||||||
}
|
}
|
||||||
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
|
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
|
||||||
|
|
||||||
@@ -60,11 +63,34 @@ export async function POST(
|
|||||||
return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404})
|
return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [restorationUpdated] = await db
|
||||||
await db
|
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set(withUpdatedAt({status: body.status as RestorationStatus}))
|
.set(withUpdatedAt({status: body.status as RestorationStatus, durationMs: body.durationMs}))
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id)).returning();
|
||||||
|
|
||||||
|
|
||||||
|
const logsToInsert = body.logs.map((entry) => ({
|
||||||
|
backupId: null,
|
||||||
|
restorationId: restorationUpdated.id,
|
||||||
|
|
||||||
|
loggedAt: new Date(entry.timestamp),
|
||||||
|
|
||||||
|
entryType: entry.type,
|
||||||
|
level: entry.level,
|
||||||
|
|
||||||
|
message: entry.message,
|
||||||
|
command: entry.command ?? null,
|
||||||
|
output: entry.output ?? null,
|
||||||
|
|
||||||
|
exitCode: entry.exit_code ?? null,
|
||||||
|
durationMs: entry.duration_ms ?? null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (logsToInsert.length > 0) {
|
||||||
|
await dbClient
|
||||||
|
.insert(drizzleDb.schemas.jobLog)
|
||||||
|
.values(logsToInsert);
|
||||||
|
}
|
||||||
|
|
||||||
await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore");
|
await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore");
|
||||||
|
|
||||||
|
|||||||
@@ -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,5 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
}
|
||||||
@@ -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 = env.API_ENABLED;
|
||||||
|
const mcpEnabled = env.MCP_ENABLED;
|
||||||
|
|
||||||
|
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<{}>;
|
||||||
|
|||||||
+11
-5
@@ -1,10 +1,10 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
# build:
|
# build:
|
||||||
# context: .
|
# context: .
|
||||||
# dockerfile: docker/dockerfile/Dockerfile
|
# dockerfile: docker/dockerfile/Dockerfile
|
||||||
# target: prod
|
# target: prod
|
||||||
image: portabase/portabase:1.7.1
|
image: portabase/portabase:1
|
||||||
ports:
|
ports:
|
||||||
- '8887:80'
|
- '8887:80'
|
||||||
environment:
|
environment:
|
||||||
@@ -17,6 +17,12 @@ services:
|
|||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
container_name: portabase-app-prod
|
container_name: portabase-app-prod
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 60s
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ ENV PORT=80
|
|||||||
ENV HOSTNAME="0.0.0.0"
|
ENV HOSTNAME="0.0.0.0"
|
||||||
ENV PGDATA=/data/postgres
|
ENV PGDATA=/data/postgres
|
||||||
ENV PRIVATE_PATH=/data/private
|
ENV PRIVATE_PATH=/data/private
|
||||||
|
ENV TUSD_BEHIND_PROXY=false
|
||||||
|
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
|||||||
@@ -72,7 +72,18 @@ fi
|
|||||||
|
|
||||||
mkdir -p /data/private/uploads/tmp
|
mkdir -p /data/private/uploads/tmp
|
||||||
echo "▶ Starting tusd server..."
|
echo "▶ Starting tusd server..."
|
||||||
tusd --base-path /tus/files/ --upload-dir /data/private/uploads/tmp --hooks-http http://127.0.0.1:3000/api/tus/hooks --port 1080 --max-size 21474836480 &
|
TUSD_BEHIND_PROXY_FLAG=""
|
||||||
|
if [ "${TUSD_BEHIND_PROXY:-false}" = "true" ]; then
|
||||||
|
TUSD_BEHIND_PROXY_FLAG="--behind-proxy"
|
||||||
|
echo "[INFO] TUSD_BEHIND_PROXY=true, enabling tusd proxy mode"
|
||||||
|
fi
|
||||||
|
tusd \
|
||||||
|
--base-path /tus/files/ \
|
||||||
|
--upload-dir /data/private/uploads/tmp \
|
||||||
|
--hooks-http http://127.0.0.1:3000/api/tus/hooks \
|
||||||
|
--port 1080 \
|
||||||
|
--max-size 21474836480 \
|
||||||
|
$TUSD_BEHIND_PROXY_FLAG &
|
||||||
|
|
||||||
echo "▶ Starting Next.js server..."
|
echo "▶ Starting Next.js server..."
|
||||||
PORT=3000 node server.js &
|
PORT=3000 node server.js &
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
events {}
|
events {}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
|
map $http_x_forwarded_proto $forwarded_proto {
|
||||||
|
default $scheme;
|
||||||
|
"~*^\s*(https?)\s*(?:,|$)" $1;
|
||||||
|
}
|
||||||
|
|
||||||
client_max_body_size 20G;
|
client_max_body_size 20G;
|
||||||
ignore_invalid_headers off;
|
ignore_invalid_headers off;
|
||||||
|
|
||||||
@@ -18,7 +23,7 @@ http {
|
|||||||
|
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Forwarded-Host $http_host;
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
@@ -34,7 +39,7 @@ http {
|
|||||||
|
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Forwarded-Host $http_host;
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -24,9 +24,35 @@ spec:
|
|||||||
value: {{ .Values.project.url }}
|
value: {{ .Values.project.url }}
|
||||||
- name: PROJECT_SECRET
|
- name: PROJECT_SECRET
|
||||||
value: {{ .Values.project.secret }}
|
value: {{ .Values.project.secret }}
|
||||||
|
- name: TUSD_BEHIND_PROXY
|
||||||
|
value: {{ .Values.project.behindProxy | quote }}
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: data
|
- name: data
|
||||||
mountPath: /data
|
mountPath: /data
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/health
|
||||||
|
port: 80
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 12
|
||||||
|
timeoutSeconds: 5
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/health
|
||||||
|
port: 80
|
||||||
|
initialDelaySeconds: 0
|
||||||
|
periodSeconds: 30
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/health
|
||||||
|
port: 80
|
||||||
|
initialDelaySeconds: 0
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
volumes:
|
volumes:
|
||||||
- name: data
|
- name: data
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ timezone: Europe/Paris
|
|||||||
project:
|
project:
|
||||||
url: http://localhost:8887
|
url: http://localhost:8887
|
||||||
secret: "change_me_generate_secure_secret"
|
secret: "change_me_generate_secure_secret"
|
||||||
|
behindProxy: false
|
||||||
|
|
||||||
persistence:
|
persistence:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
+34
-31
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.14.1",
|
"version": "1.18.1",
|
||||||
"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.2+sha512.415a1cc25974731e75455c1468371be74c5aa5fb7621b50d4056d222451609f11412f23fd602e6169f1e060466641f798597e1be961a10688836a67b16569499"
|
"packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2586
-1663
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 = env.API_ENABLED;
|
||||||
|
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 = env.OPENAPI_ENABLED;
|
||||||
|
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(
|
||||||
@@ -71,7 +102,21 @@ function checkRouteExists(pathname: string) {
|
|||||||
/^\/api\/tus\/hooks\/?$/,
|
/^\/api\/tus\/hooks\/?$/,
|
||||||
/^\/api\/events\/?$/,
|
/^\/api\/events\/?$/,
|
||||||
/^\/api\/config\/?$/,
|
/^\/api\/config\/?$/,
|
||||||
|
/^\/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);
|
||||||
|
|
||||||
+67
-35
@@ -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>
|
||||||
<CommandGroup>
|
<div
|
||||||
<CommandItem key="all" onSelect={toggleAll} className="cursor-pointer">
|
className="max-h-[18rem] overflow-y-auto overflow-x-hidden overscroll-contain touch-pan-y pr-1"
|
||||||
<div
|
style={{ WebkitOverflowScrolling: "touch" }}
|
||||||
className={cn(
|
onWheelCapture={(event) => event.stopPropagation()}
|
||||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
onTouchMoveCapture={(event) => event.stopPropagation()}
|
||||||
selectedValues.length === options.length ? "bg-primary text-primary-foreground" : "opacity-50 [&_svg]:invisible"
|
>
|
||||||
)}
|
<CommandGroup>
|
||||||
>
|
<CommandItem key="all" onSelect={toggleAll} className="cursor-pointer">
|
||||||
<CheckIcon className="h-4 w-4" />
|
<div
|
||||||
</div>
|
className={cn(
|
||||||
<span>(Select All)</span>
|
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||||
</CommandItem>
|
selectedValues.length === options.length
|
||||||
{options.map((option) => {
|
? "bg-primary text-primary-foreground"
|
||||||
const isSelected = selectedValues.includes(option.value);
|
: "opacity-50 [&_svg]:invisible"
|
||||||
return (
|
)}
|
||||||
<CommandItem key={option.value} onSelect={() => toggleOption(option.value)} className="cursor-pointer">
|
>
|
||||||
<div
|
<CheckIcon className="h-4 w-4" />
|
||||||
className={cn(
|
</div>
|
||||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
<span>(Select All)</span>
|
||||||
isSelected ? "bg-primary text-primary-foreground" : "opacity-50 [&_svg]:invisible"
|
</CommandItem>
|
||||||
)}
|
|
||||||
|
{options.map((option) => {
|
||||||
|
const isSelected = selectedValues.includes(option.value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
key={option.value}
|
||||||
|
onSelect={() => toggleOption(option.value)}
|
||||||
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
<CheckIcon className="h-4 w-4" />
|
<div
|
||||||
</div>
|
className={cn(
|
||||||
{option.icon && <option.icon className="mr-2 h-4 w-4 text-muted-foreground" />}
|
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||||
<span>{option.label}</span>
|
isSelected
|
||||||
</CommandItem>
|
? "bg-primary text-primary-foreground"
|
||||||
);
|
: "opacity-50 [&_svg]:invisible"
|
||||||
})}
|
)}
|
||||||
</CommandGroup>
|
>
|
||||||
|
<CheckIcon className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{option.icon && (
|
||||||
|
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span>{option.label}</span>
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</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;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user