mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3de7ef1a26 | ||
|
|
1713780528 | ||
|
|
6db39b829e | ||
|
|
174ff0886c | ||
|
|
d49d8f52c4 | ||
|
|
c9552e448a | ||
|
|
47d3059d78 | ||
|
|
c3bafef6d8 | ||
|
|
1fbbc935c1 | ||
|
|
ad0e6be10d | ||
|
|
5735d47889 | ||
|
|
294bdfa296 | ||
|
|
83eb012cb5 | ||
|
|
51d165fe84 | ||
|
|
3b7eb6c606 | ||
|
|
333146c590 | ||
|
|
6adb310f0c | ||
|
|
a64fe1c08c | ||
|
|
40cdb3d0ab | ||
|
|
7a97721abf | ||
|
|
c1b71e7772 | ||
|
|
56517226b6 | ||
|
|
337d270ba6 | ||
|
|
373ab315a3 | ||
|
|
685e1fc300 | ||
|
|
274aded8fd | ||
|
|
39b6b5e116 | ||
|
|
3f52d8c76e | ||
|
|
11f814777b | ||
|
|
dd66ecdb78 | ||
|
|
ec90a34262 | ||
|
|
5a2ce4968d | ||
|
|
03659b4a15 | ||
|
|
b032c96a7c | ||
|
|
d887d44bd4 | ||
|
|
7115ce8484 | ||
|
|
96cc9822d0 | ||
|
|
68fb74f441 | ||
|
|
06ed4f3517 | ||
|
|
3a4dd10b4c | ||
|
|
0316fdc0a3 | ||
|
|
72ecd7f054 | ||
|
|
539fec4dbc | ||
|
|
3fa0f9b76d | ||
|
|
4fadfcfaf2 | ||
|
|
af3df7f72a | ||
|
|
2cd5bda416 | ||
|
|
965b215224 | ||
|
|
27f6b746e0 | ||
|
|
7d463d7da8 | ||
|
|
332540488d | ||
|
|
ab52ef6c27 | ||
|
|
2d55618898 | ||
|
|
6b3df405eb | ||
|
|
e91ef4c142 | ||
|
|
9b504ccd68 | ||
|
|
dd3f4411cf | ||
|
|
0ab94b6221 | ||
|
|
8477889cea | ||
|
|
2eb4a7a0fe | ||
|
|
1f68adda90 | ||
|
|
ffba2c56a3 | ||
|
|
02be78a26e | ||
|
|
711d4613ed | ||
|
|
a5fe5225e1 | ||
|
|
b752d9437c | ||
|
|
18df92e048 | ||
|
|
4e66b8ed04 | ||
|
|
4f14d16452 | ||
|
|
592588ef76 | ||
|
|
3fec7c3392 | ||
|
|
2335caefd7 | ||
|
|
194eedeaa8 | ||
|
|
ecd09aedb9 | ||
|
|
b5350b5fe3 | ||
|
|
ceaafd5531 | ||
|
|
654accad4a | ||
|
|
173c7894b4 | ||
|
|
1ab51d15f9 | ||
|
|
ddce51d2d3 | ||
|
|
fc9fbf4357 | ||
|
|
5e8cd7e0e6 | ||
|
|
1eb5fbb983 | ||
|
|
d9ccb389f0 | ||
|
|
dc80d1d752 | ||
|
|
ff28a80d7c | ||
|
|
ab55ab349a | ||
|
|
a1ec7dbec2 | ||
|
|
e29846ae25 | ||
|
|
e38519aec2 | ||
|
|
a620d7a9f7 | ||
|
|
88ebd79681 |
@@ -1,6 +1,12 @@
|
|||||||
# Environment
|
# Environment
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
# Controls minimum log level. Options: debug | info | warn | error
|
||||||
|
# Set to "warn" or "error" in production to suppress health-check ping noise.
|
||||||
|
# Use "debug" to see all request logs including agent status pings.
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
DATABASE_URL=postgresql://devuser:changeme@localhost:5433/devdb?schema=public
|
DATABASE_URL=postgresql://devuser:changeme@localhost:5433/devdb?schema=public
|
||||||
|
|
||||||
@@ -9,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=
|
||||||
|
|||||||
@@ -55,3 +55,7 @@ certificates
|
|||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
/playwright/.auth/
|
/playwright/.auth/
|
||||||
/e2e/local-storage.json
|
/e2e/local-storage.json
|
||||||
|
|
||||||
|
.claude
|
||||||
|
.codex
|
||||||
|
/docs
|
||||||
|
|||||||
+3
-1
@@ -21,6 +21,8 @@ keywords:
|
|||||||
- backups
|
- backups
|
||||||
- postgresql
|
- postgresql
|
||||||
- mysql
|
- mysql
|
||||||
|
- firebird
|
||||||
|
- mssql
|
||||||
- mariadb
|
- mariadb
|
||||||
- devops
|
- devops
|
||||||
- database
|
- database
|
||||||
@@ -31,5 +33,5 @@ keywords:
|
|||||||
- web-ui
|
- web-ui
|
||||||
- agent
|
- agent
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
version: 1.11.4
|
version: 1.16.0
|
||||||
date-released: '2026-03-02'
|
date-released: '2026-03-02'
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ seed-pocket:
|
|||||||
@docker compose -f docker-compose.func.yml stop pocket-id >/dev/null 2>&1 || true
|
@docker compose -f docker-compose.func.yml stop pocket-id >/dev/null 2>&1 || true
|
||||||
@docker compose -f docker-compose.func.yml rm -f -s pocket-id >/dev/null 2>&1 || true
|
@docker compose -f docker-compose.func.yml rm -f -s pocket-id >/dev/null 2>&1 || true
|
||||||
@docker volume rm portabase-dev-func_pocket-id-data >/dev/null 2>&1 || true
|
@docker volume rm portabase-dev-func_pocket-id-data >/dev/null 2>&1 || true
|
||||||
@docker compose -f docker-compose.func.yml run --rm -v ./seeds/pocket-id/portabase.zip:/tmp/portabase.zip pocket-id ./pocket-id import --yes --path /tmp/portabase.zip >/dev/null
|
@docker compose -f docker-compose.func.yml run --rm -v $$(pwd)/seeds/pocket-id/portabase.zip:/tmp/portabase.zip pocket-id ./pocket-id import --yes --path /tmp/portabase.zip >/dev/null
|
||||||
@docker compose -f docker-compose.func.yml up -d pocket-id
|
@docker compose -f docker-compose.func.yml up -d pocket-id
|
||||||
@sleep 2
|
@sleep 2
|
||||||
@docker compose -f docker-compose.func.yml exec pocket-id ./pocket-id one-time-access-token admin
|
@docker compose -f docker-compose.func.yml exec pocket-id ./pocket-id one-time-access-token admin
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
[](https://www.mongodb.com/)
|
[](https://www.mongodb.com/)
|
||||||
[](https://valkey.io/)
|
[](https://valkey.io/)
|
||||||
[](https://firebirdsql.org/)
|
[](https://firebirdsql.org/)
|
||||||
|
[](https://www.microsoft.com/en-us/sql-server)
|
||||||
|
|
||||||
[](https://github.com/Portabase/portabase)
|
[](https://github.com/Portabase/portabase)
|
||||||
[](https://github.com/Portabase/portabase)
|
[](https://github.com/Portabase/portabase)
|
||||||
@@ -66,7 +67,7 @@ You have 4 ways to install Portabase:
|
|||||||
## Supported databases
|
## Supported databases
|
||||||
|
|
||||||
| Engine | Support | Supported Versions | Restore |
|
| Engine | Support | Supported Versions | Restore |
|
||||||
|:-------------------|:-----------|:------------------------------|:--------|
|
|:-------------------|:----------|:------------------------------|:--------|
|
||||||
| **PostgreSQL** | ✅ Stable | 12, 13, 14, 15, 16, 17 and 18 | Yes |
|
| **PostgreSQL** | ✅ Stable | 12, 13, 14, 15, 16, 17 and 18 | Yes |
|
||||||
| **MySQL** | ✅ Stable | 5.7, 8 and 9 | Yes |
|
| **MySQL** | ✅ Stable | 5.7, 8 and 9 | Yes |
|
||||||
| **MariaDB** | ✅ Stable | 10 and 11 | Yes |
|
| **MariaDB** | ✅ Stable | 10 and 11 | Yes |
|
||||||
@@ -75,7 +76,7 @@ You have 4 ways to install Portabase:
|
|||||||
| **Redis** | ✅ Stable | 2.8+ | No |
|
| **Redis** | ✅ Stable | 2.8+ | No |
|
||||||
| **Valkey** | ✅ Stable | 7.2+ | No |
|
| **Valkey** | ✅ Stable | 7.2+ | No |
|
||||||
| **Firebird** | ✅ Stable | 3.0, 4.0, 5.0 | Yes |
|
| **Firebird** | ✅ Stable | 3.0, 4.0, 5.0 | Yes |
|
||||||
| **MSSQL Server** | ❌ Ongoing | - | Yes |
|
| **MSSQL Server** | ✅ Stable | 2017, 2019, 2022, Azure SQL | Yes |
|
||||||
|
|
||||||
See the [Database Servers documentation](https://portabase.io/docs/agent/db) for version-specific backup and restore details.
|
See the [Database Servers documentation](https://portabase.io/docs/agent/db) for version-specific backup and restore details.
|
||||||
|
|
||||||
|
|||||||
@@ -1,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<{}>) {
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
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 {db} from "@/db";
|
import {db} from "@/db";
|
||||||
|
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";
|
||||||
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
|
|
||||||
|
const log = logger.child({module: "dashboard/admin/settings"});
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
const settings = await db.query.setting.findFirst({
|
const settings = await db.query.setting.findFirst({
|
||||||
where: (fields, {eq}) => eq(fields.name, "system"),
|
where: (fields, {eq}) => eq(fields.name, "system"),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(settings)
|
log.debug({settings}, "Settings loaded");
|
||||||
|
|
||||||
const storageChannels = await db.query.storageChannel.findMany({
|
const storageChannels = await db.query.storageChannel.findMany({
|
||||||
with: {
|
with: {
|
||||||
@@ -45,7 +48,8 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="flex flex-col gap-5">
|
<PageContent className="flex flex-col gap-5">
|
||||||
<SettingsTabs storageChannels={storageChannels} notificationChannels={notificationChannels} settings={settings} />
|
<SettingsTabs storageChannels={storageChannels} notificationChannels={notificationChannels}
|
||||||
|
settings={settings}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
{isPasswordAuthEnabled && (
|
||||||
<PageActions>
|
<PageActions>
|
||||||
<AdminUserAddModal organizations={organizations}/>
|
<AdminUserAddModal organizations={organizations}/>
|
||||||
</PageActions>
|
</PageActions>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="flex flex-col gap-5">
|
<PageContent className="flex flex-col gap-5">
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import {
|
|||||||
} from "@/features/layout/page";
|
} from "@/features/layout/page";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import { eq } from "drizzle-orm";
|
import {eq, 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";
|
||||||
import { AgentType } from "@/features/agents/agents.schema";
|
|
||||||
|
|
||||||
export default async function RoutePage(
|
export default async function RoutePage(
|
||||||
props: PageParams<{ agentId: string }>,
|
props: PageParams<{ agentId: string }>,
|
||||||
@@ -26,13 +26,30 @@ export default async function RoutePage(
|
|||||||
where: eq(drizzleDb.schemas.agent.id, agentId),
|
where: eq(drizzleDb.schemas.agent.id, agentId),
|
||||||
with: {
|
with: {
|
||||||
databases: true,
|
databases: true,
|
||||||
|
organizations: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const organizations = await db.query.organization.findMany({
|
||||||
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
|
with: {
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isOwnerByAnOrganization = agent.organizationId
|
||||||
|
|
||||||
|
if (isOwnerByAnOrganization){
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const organizationIds = agent.organizations.map(org => org.organizationId)
|
||||||
|
|
||||||
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
|
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -45,12 +62,14 @@ export default async function RoutePage(
|
|||||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AgentDialog
|
<AgentDialog
|
||||||
agent={agent as AgentType & { id: string }}
|
agent={agent}
|
||||||
typeTrigger={"edit"}
|
typeTrigger={"edit"}
|
||||||
|
adminView={true}
|
||||||
|
organizations={organizations}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"} />
|
<ButtonDeleteAgent organizationIds={organizationIds} agentId={agentId} text={"Delete Agent"} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
|
|||||||
@@ -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 {desc, eq, 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",
|
||||||
@@ -16,7 +16,7 @@ export const metadata: Metadata = {
|
|||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
const agents = await db.query.agent.findMany({
|
const agents = await db.query.agent.findMany({
|
||||||
where: not(eq(drizzleDb.schemas.agent.isArchived, true)),
|
where: and(not(eq(drizzleDb.schemas.agent.isArchived, true)),isNull(drizzleDb.schemas.agent.organizationId)),
|
||||||
with: {
|
with: {
|
||||||
databases: true
|
databases: true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
import {getOrganization} from "@/lib/auth/auth";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {MigrationTool} from "@/features/migration/migration-tool";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Projects",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const organization = await getOrganization({});
|
||||||
|
|
||||||
|
if (!organization) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const projects = await db.query.project.findMany({
|
||||||
|
where: (project, {eq, and, not}) =>
|
||||||
|
and(
|
||||||
|
eq(project.organizationId, organization.id),
|
||||||
|
not(eq(project.isArchived, true))
|
||||||
|
),
|
||||||
|
with: {
|
||||||
|
organization: true,
|
||||||
|
databases: {
|
||||||
|
where: (database, { isNull, not, inArray, and }) =>
|
||||||
|
and(
|
||||||
|
isNull(database.deletedAt),
|
||||||
|
not(inArray(database.dbms, ["valkey", "redis"]))
|
||||||
|
),
|
||||||
|
|
||||||
|
with: {
|
||||||
|
backups: {
|
||||||
|
where: (backup, { isNull, eq, and }) =>
|
||||||
|
and(
|
||||||
|
isNull(backup.deletedAt),
|
||||||
|
eq(backup.status, "success")
|
||||||
|
),
|
||||||
|
orderBy: (backup, {desc}) => [desc(backup.createdAt)],
|
||||||
|
limit: 15,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader className="flex flex-col items-start justify-between mb-6">
|
||||||
|
<PageTitle className="mb-2">
|
||||||
|
Database Migration
|
||||||
|
</PageTitle>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Import backups from a source project into your target database
|
||||||
|
</p>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
<MigrationTool projects={projects}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
+3
-3
@@ -4,10 +4,10 @@ import {Page} from "@/features/layout/page";
|
|||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {eq, and, inArray} from "drizzle-orm";
|
import {eq, and, inArray} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {getOrganizationProjectDatabases} from "@/lib/services";
|
import {getOrganizationProjectDatabases} from "@/db/services/project";
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
|
import {BackupModalProvider} from "@/features/database/backup-modal-context";
|
||||||
import {DatabaseContent} from "@/components/wrappers/dashboard/projects/database/database-content";
|
import {DatabaseContent} from "@/features/database/database-content";
|
||||||
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
|
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
export default async function RoutePage(props: PageParams<{
|
||||||
|
|||||||
@@ -2,20 +2,19 @@ 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 {DatabaseWith} from "@/db/schema/07_database";
|
|
||||||
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";
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
export default async function RoutePage(props: PageParams<{
|
||||||
@@ -56,19 +55,7 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
redirect("/dashboard/projects");
|
redirect("/dashboard/projects");
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableDatabases = (
|
const availableDatabases = await getOrganizationAvailableDatabases(organization.id, proj.id)
|
||||||
await db.query.database.findMany({
|
|
||||||
where: (db, {or, eq, isNull}) => or(isNull(db.projectId), eq(db.projectId, proj.id)),
|
|
||||||
with: {
|
|
||||||
agent: true,
|
|
||||||
project: true,
|
|
||||||
backups: true,
|
|
||||||
restorations: true,
|
|
||||||
},
|
|
||||||
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
|
||||||
})
|
|
||||||
) as DatabaseWith[];
|
|
||||||
|
|
||||||
const isMember = activeMember?.role === "member";
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
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";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Projects",
|
title: "Projects",
|
||||||
@@ -35,18 +36,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
});
|
});
|
||||||
const isMember = activeMember?.role === "member";
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
const availableDatabases = (
|
const availableDatabases = await getOrganizationAvailableDatabases(organization.id)
|
||||||
await db.query.database.findMany({
|
|
||||||
where: (db, {isNull}) => isNull(db.projectId),
|
|
||||||
with: {
|
|
||||||
agent: true,
|
|
||||||
project: true,
|
|
||||||
backups: true,
|
|
||||||
restorations: true,
|
|
||||||
},
|
|
||||||
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
|
||||||
})
|
|
||||||
).filter((db) => db.project == null) as DatabaseWith[];
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -71,7 +61,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
pageSizeOptions={[12, 24, 48]}
|
pageSizeOptions={[12, 24, 48]}
|
||||||
/>
|
/>
|
||||||
) : isMember ? (
|
) : isMember ? (
|
||||||
<EmptyStatePlaceholder text="No project available"/>
|
<EmptyStatePlaceholder state={"empty"} text="No project available"/>
|
||||||
) : (
|
) : (
|
||||||
<ProjectDialog databases={availableDatabases} organization={organization} isEmpty={true}/>
|
<ProjectDialog databases={availableDatabases} organization={organization} isEmpty={true}/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
PageContent,
|
||||||
|
PageDescription,
|
||||||
|
PageTitle,
|
||||||
|
} from "@/features/layout/page";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
import {ButtonDeleteAgent} from "@/features/agents/agent-delete-button";
|
||||||
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
|
import {generateEdgeKey} from "@/utils/edge_key";
|
||||||
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
|
import {AgentContentPage} from "@/features/agents/agent-content";
|
||||||
|
import {AgentDialog} from "@/features/agents/agent-dialog";
|
||||||
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
|
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||||
|
|
||||||
|
|
||||||
|
export default async function RoutePage(
|
||||||
|
props: PageParams<{ agentId: string }>,
|
||||||
|
) {
|
||||||
|
const {agentId} = await props.params;
|
||||||
|
|
||||||
|
const organization = await getOrganization({});
|
||||||
|
const user = await currentUser();
|
||||||
|
const activeMember = await getActiveMember();
|
||||||
|
|
||||||
|
if (!organization || !activeMember || !user) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const {canManageAgents} = computeOrganizationPermissions(activeMember);
|
||||||
|
|
||||||
|
if (!canManageAgents){
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = await db.query.agent.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.agent.id, agentId),
|
||||||
|
with: {
|
||||||
|
databases: true,
|
||||||
|
organizations: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (!agent) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAccess =
|
||||||
|
agent.organizationId === organization.id ||
|
||||||
|
agent.organizations.some(org => org.organizationId === organization.id);
|
||||||
|
|
||||||
|
if (!hasAccess) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOwned = agent.organizationId
|
||||||
|
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<div className="justify-between gap-2 sm:flex">
|
||||||
|
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||||
|
<div className="min-w-full md:min-w-fit ">
|
||||||
|
{capitalizeFirstLetter(agent.name)}
|
||||||
|
</div>
|
||||||
|
{isOwned && (
|
||||||
|
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AgentDialog
|
||||||
|
agent={agent}
|
||||||
|
typeTrigger={"edit"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ButtonDeleteAgent organizationId={organization.id ?? null} agentId={agentId} text={"Delete Agent"}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PageTitle>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{agent.description && (
|
||||||
|
<PageDescription className="mt-5 sm:mt-0">
|
||||||
|
{agent.description}
|
||||||
|
</PageDescription>
|
||||||
|
)}
|
||||||
|
<PageContent className="flex flex-col w-full h-full justify-between gap-6">
|
||||||
|
<AgentContentPage agent={agent} edgeKey={edgeKey}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default async function RoutePage() {
|
||||||
|
redirect("/dashboard/settings?tab=agents");
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {
|
import {
|
||||||
Page,
|
Page,
|
||||||
PageActions,
|
|
||||||
PageContent,
|
PageContent,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
@@ -10,16 +9,18 @@ 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";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
|
import {getOrganizationAgents} from "@/db/services/agent";
|
||||||
|
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
@@ -36,6 +37,7 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
|||||||
|
|
||||||
const notificationChannels = await getOrganizationChannels(organization.id);
|
const notificationChannels = await getOrganizationChannels(organization.id);
|
||||||
const storageChannels = await getOrganizationStorageChannels(organization.id);
|
const storageChannels = await getOrganizationStorageChannels(organization.id);
|
||||||
|
const agents = await getOrganizationAgents(organization.id);
|
||||||
const permissions = computeOrganizationPermissions(activeMember);
|
const permissions = computeOrganizationPermissions(activeMember);
|
||||||
|
|
||||||
const users = await db.query.user.findMany({
|
const users = await db.query.user.findMany({
|
||||||
@@ -45,6 +47,7 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
|||||||
const organizationWithMembers = await db.query.organization.findFirst({
|
const organizationWithMembers = await db.query.organization.findFirst({
|
||||||
where: eq(drizzleDb.schemas.organization.id, organization.id),
|
where: eq(drizzleDb.schemas.organization.id, organization.id),
|
||||||
with: {
|
with: {
|
||||||
|
projects: true,
|
||||||
members: {
|
members: {
|
||||||
with: {
|
with: {
|
||||||
user: true,
|
user: true,
|
||||||
@@ -74,9 +77,24 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{permissions.canManageDangerZone &&
|
{permissions.canManageDangerZone &&
|
||||||
organization.slug !== "default" && (
|
organization.slug !== "default" && (
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div>
|
||||||
<DeleteOrganizationButton
|
<DeleteOrganizationButton
|
||||||
|
disabled={organizationWithMembers.projects.length > 0}
|
||||||
organizationSlug={organization.slug}
|
organizationSlug={organization.slug}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</TooltipTrigger>
|
||||||
|
{organizationWithMembers.projects.length > 0 && (
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Your organization has some projects associated with it. Please delete them before deleting the organization.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,6 +106,7 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
|||||||
organization={organization}
|
organization={organization}
|
||||||
notificationChannels={notificationChannels}
|
notificationChannels={notificationChannels}
|
||||||
storageChannels={storageChannels}
|
storageChannels={storageChannels}
|
||||||
|
agents={agents}
|
||||||
/>
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -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() {
|
||||||
@@ -9,8 +9,4 @@ export default async function Index() {
|
|||||||
redirect(`/dashboard/home`);
|
redirect(`/dashboard/home`);
|
||||||
}
|
}
|
||||||
redirect("/login");
|
redirect("/login");
|
||||||
//Do not delete
|
|
||||||
// return (
|
|
||||||
// <Home/>
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
@@ -12,7 +12,7 @@ export function withAgentCheck(handler: Function) {
|
|||||||
const agentId = (await context.params).agentId;
|
const agentId = (await context.params).agentId;
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({
|
const agent = await db.query.agent.findFirst({
|
||||||
where: eq(drizzleDb.schemas.agent.id, agentId),
|
where: and(eq(drizzleDb.schemas.agent.id, agentId), eq(drizzleDb.schemas.agent.isArchived, false)),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import {db as dbClient, db} from "@/db";
|
|||||||
import {getDatabaseOrThrow, withAgentCheck} from "./helpers";
|
import {getDatabaseOrThrow, withAgentCheck} from "./helpers";
|
||||||
import {Backup} from "@/db/schema/07_database";
|
import {Backup} from "@/db/schema/07_database";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
import {eventEmitter} from "@/features/shared/event";
|
import {eventEmitter} from "@/lib/event";
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers";
|
||||||
import {EventKind} from "@/features/notifications/types";
|
import {EventKind} from "@/features/notifications/notifications.types";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
const log = logger.child({module: "api/agent/backup/route"});
|
const log = logger.child({module: "api/agent/backup/route"});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as drizzleDb from "@/db";
|
|||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import {eventEmitter} from "@/features/shared/event";
|
import {eventEmitter} from "@/lib/event";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
const log = logger.child({module: "api/agent/backup/upload/init"});
|
const log = logger.child({module: "api/agent/backup/upload/init"});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as drizzleDb from "@/db";
|
|||||||
import {db as dbClient, db} from "@/db";
|
import {db as dbClient, db} from "@/db";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
||||||
import {eventEmitter} from "@/features/shared/event";
|
import {eventEmitter} from "@/lib/event";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
const log = logger.child({module: "api/agent/backup/upload/status"});
|
const log = logger.child({module: "api/agent/backup/upload/status"});
|
||||||
@@ -70,10 +70,10 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
if (hasSuccessfulStorage && backup.status !== "success") {
|
if (hasSuccessfulStorage && backup.status !== "success") {
|
||||||
await db
|
await db
|
||||||
.update(drizzleDb.schemas.backup)
|
.update(drizzleDb.schemas.backup)
|
||||||
.set({
|
.set(withUpdatedAt({
|
||||||
status: "success",
|
status: "success",
|
||||||
fileSize: fileSize,
|
fileSize: fileSize,
|
||||||
})
|
}))
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import {isUuidv4} from "@/utils/verify-uuid";
|
|||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
|
||||||
const log = logger.child({module: "api/agent/restore"});
|
const log = logger.child({module: "api/agent/restore"});
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ export async function POST(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({
|
const agent = await db.query.agent.findFirst({
|
||||||
where: eq(drizzleDb.schemas.agent.id, agentId)
|
where: and(eq(drizzleDb.schemas.agent.id, agentId), eq(drizzleDb.schemas.agent.isArchived, false)),
|
||||||
})
|
})
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
return NextResponse.json({error: "Agent not found"}, {status: 404})
|
return NextResponse.json({error: "Agent not found"}, {status: 404})
|
||||||
@@ -62,7 +63,7 @@ export async function POST(
|
|||||||
|
|
||||||
await db
|
await db
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set({status: body.status as RestorationStatus})
|
.set(withUpdatedAt({status: body.status as RestorationStatus}))
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||||
|
|
||||||
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";
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
} else {
|
} else {
|
||||||
await dbClient
|
await dbClient
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set({status: "failed"})
|
.set(withUpdatedAt({status: "failed"}))
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||||
|
|
||||||
const errorMessage = "Failed to get backup URL";
|
const errorMessage = "Failed to get backup URL";
|
||||||
@@ -194,14 +194,14 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
log.error({error: err, name: "handleDatabases"}, "Restoration crashed unexpectedly");
|
log.error({error: err, name: "handleDatabases"}, "Restoration crashed unexpectedly");
|
||||||
await dbClient
|
await dbClient
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set({status: "failed"})
|
.set(withUpdatedAt({status: "failed"}))
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await dbClient
|
await dbClient
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set({status: "ongoing"})
|
.set(withUpdatedAt({status: "ongoing"}))
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||||
}
|
}
|
||||||
const storages = await getDatabaseStorageChannels(databaseUpdated.id)
|
const storages = await getDatabaseStorageChannels(databaseUpdated.id)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {handleDatabases} from "./helpers";
|
|||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {EDbmsSchema} from "@/db/schema/types";
|
import {EDbmsSchema} from "@/db/schema/types";
|
||||||
import {eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
@@ -31,7 +31,7 @@ export async function POST(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const agentId = (await params).agentId
|
const agentId = (await params).agentId
|
||||||
log.info(`Agent ID: ${agentId}`)
|
log.debug(`Agent ID: ${agentId}`)
|
||||||
const body: Body = await request.json();
|
const body: Body = await request.json();
|
||||||
const lastContact = new Date();
|
const lastContact = new Date();
|
||||||
let message: string
|
let message: string
|
||||||
@@ -46,7 +46,7 @@ export async function POST(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({
|
const agent = await db.query.agent.findFirst({
|
||||||
where: eq(drizzleDb.schemas.agent.id, agentId),
|
where: and(eq(drizzleDb.schemas.agent.id, agentId), eq(drizzleDb.schemas.agent.isArchived, false)),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
|
|||||||
@@ -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,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<{}>;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ services:
|
|||||||
# 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
|
||||||
|
|||||||
@@ -32,5 +32,14 @@ services:
|
|||||||
- ./private/uploads/tmp:/data/uploads/tmp
|
- ./private/uploads/tmp:/data/uploads/tmp
|
||||||
user: "1000:1000"
|
user: "1000:1000"
|
||||||
|
|
||||||
|
mailpit:
|
||||||
|
image: axllent/mailpit:latest
|
||||||
|
container_name: portabase-mailpit-dev
|
||||||
|
hostname: mailpit
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8025:8025"
|
||||||
|
- "1025:1025"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -27,6 +27,30 @@ spec:
|
|||||||
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:
|
||||||
|
|||||||
+40
-37
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.11.4",
|
"version": "1.16.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 8887",
|
"dev": "next dev --turbopack -p 8887",
|
||||||
@@ -15,9 +15,12 @@
|
|||||||
"release": "release-it"
|
"release": "release-it"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@better-auth/passkey": "^1.5.6",
|
"@asteasolutions/zod-to-openapi": "^8.5.0",
|
||||||
"@better-auth/sso": "^1.5.6",
|
"@better-auth/api-key": "1.6.11",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@better-auth/core": "1.6.11",
|
||||||
|
"@better-auth/passkey": "1.6.11",
|
||||||
|
"@better-auth/sso": "1.6.11",
|
||||||
|
"@hookform/resolvers": "^5.4.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",
|
||||||
@@ -48,46 +51,46 @@
|
|||||||
"@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.95.2",
|
"@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.5.6",
|
"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.43.1",
|
"drizzle-orm": "0.45.2",
|
||||||
"drizzle-zod": "^0.7.1",
|
"drizzle-zod": "0.8.3",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"googleapis": "^170.1.0",
|
"googleapis": "^170.1.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.553.0",
|
||||||
"minio": "^8.0.7",
|
"minio": "^8.0.7",
|
||||||
"motion": "^12.38.0",
|
"motion": "^12.40.0",
|
||||||
"next": "16.2.1",
|
"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",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"node-forge": "^1.4.0",
|
"node-forge": "^1.4.0",
|
||||||
"nodemailer": "^7.0.13",
|
"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.1",
|
"prettier": "^3.8.3",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.6",
|
||||||
"react-day-picker": "9.7.0",
|
"react-day-picker": "9.7.0",
|
||||||
"react-dom": "^19.2.4",
|
"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.0",
|
"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",
|
||||||
@@ -96,44 +99,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": "^3.25.76"
|
"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.4",
|
"@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.15",
|
"@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.12",
|
"baseline-browser-mapping": "^2.10.32",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
"esbuild": "^0.27.4",
|
"esbuild": "^0.27.7",
|
||||||
"eslint": "^9.39.4",
|
"eslint": "^9.39.4",
|
||||||
"eslint-config-next": "^16.2.1",
|
"eslint-config-next": "^16.2.6",
|
||||||
"eslint-plugin-tailwindcss": "^3.18.2",
|
"eslint-plugin-tailwindcss": "^3.18.3",
|
||||||
"framer-motion": "^12.34.3",
|
"framer-motion": "^12.40.0",
|
||||||
"node-pty": "^1.1.0",
|
"node-pty": "^1.1.0",
|
||||||
"postcss": "^8.5.6",
|
"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@10.33.0"
|
"packageManager": "pnpm@11.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+3052
-2639
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,17 @@
|
|||||||
|
allowBuilds:
|
||||||
|
'@prisma/client': true
|
||||||
|
'@prisma/engines': true
|
||||||
|
argon2: true
|
||||||
|
bcrypt: true
|
||||||
|
cpu-features: true
|
||||||
|
esbuild: true
|
||||||
|
node-pty: true
|
||||||
|
prisma: true
|
||||||
|
protobufjs: true
|
||||||
|
sharp: true
|
||||||
|
ssh2: true
|
||||||
|
unrs-resolver: true
|
||||||
|
zenstack: true
|
||||||
ignoredBuiltDependencies:
|
ignoredBuiltDependencies:
|
||||||
- node-pty
|
- node-pty
|
||||||
onlyBuiltDependencies:
|
onlyBuiltDependencies:
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const PORTABASE_DEFAULT_SETTINGS = {
|
|||||||
"https://code.iconify.design",
|
"https://code.iconify.design",
|
||||||
"https://cdn.iconify.design",
|
"https://cdn.iconify.design",
|
||||||
"https://code.iconify.com",
|
"https://code.iconify.com",
|
||||||
|
"https://cdn.jsdelivr.net",
|
||||||
],
|
],
|
||||||
IMG_SRC: [
|
IMG_SRC: [
|
||||||
"'self'",
|
"'self'",
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { loggingMiddleware } from "@/middleware/loggingMiddleware";
|
|||||||
import { errorHandler } from "@/middleware/errorHandler";
|
import { errorHandler } from "@/middleware/errorHandler";
|
||||||
import { auth } from "@/lib/auth/auth";
|
import { auth } from "@/lib/auth/auth";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import {User} from "@/db/schema/02_user";
|
||||||
|
|
||||||
export async function proxy(request: NextRequest) {
|
export async function proxy(request: NextRequest) {
|
||||||
const url = request.nextUrl.clone();
|
const url = request.nextUrl.clone();
|
||||||
@@ -17,11 +19,13 @@ export async function proxy(request: NextRequest) {
|
|||||||
new URL(`/login?redirect=${redirectUrl}`, request.url),
|
new URL(`/login?redirect=${redirectUrl}`, request.url),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (session.user.banned) {
|
const user = session.user as User
|
||||||
|
|
||||||
|
if (user.banned) {
|
||||||
await auth.api.signOut({ headers: await headers() });
|
await auth.api.signOut({ headers: await headers() });
|
||||||
return NextResponse.redirect(new URL("/login?error=banned", request.url));
|
return NextResponse.redirect(new URL("/login?error=banned", request.url));
|
||||||
}
|
}
|
||||||
if (session.user.role === "pending") {
|
if (user.role === "pending") {
|
||||||
await auth.api.signOut({ headers: await headers() });
|
await auth.api.signOut({ headers: await headers() });
|
||||||
return NextResponse.redirect(
|
return NextResponse.redirect(
|
||||||
new URL(`/login?error=pending?redirect=${redirectUrl}`, request.url),
|
new URL(`/login?error=pending?redirect=${redirectUrl}`, request.url),
|
||||||
@@ -38,6 +42,33 @@ export async function proxy(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (url.pathname.startsWith("/api")) {
|
if (url.pathname.startsWith("/api")) {
|
||||||
|
if (url.pathname.startsWith("/api/v1")) {
|
||||||
|
const apiEnabled = String(env.API_ENABLED) === "true";
|
||||||
|
if (!apiEnabled) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
message: "This API route does not exist.",
|
||||||
|
status: 404,
|
||||||
|
}),
|
||||||
|
{ status: 404, headers: { "Content-Type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const openapiEnabled = String(env.OPENAPI_ENABLED) === "true";
|
||||||
|
if (
|
||||||
|
!openapiEnabled &&
|
||||||
|
(url.pathname.startsWith("/api/v1/docs") ||
|
||||||
|
url.pathname.startsWith("/api/v1/openapi"))
|
||||||
|
) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
message: "This API route does not exist.",
|
||||||
|
status: 404,
|
||||||
|
}),
|
||||||
|
{ status: 404, headers: { "Content-Type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const routeExists = checkRouteExists(url.pathname);
|
const routeExists = checkRouteExists(url.pathname);
|
||||||
if (!routeExists) {
|
if (!routeExists) {
|
||||||
return new NextResponse(
|
return new NextResponse(
|
||||||
@@ -71,7 +102,20 @@ 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\/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));
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
+1
-1
@@ -44,7 +44,7 @@ export function BreadCrumbsWrapper() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const FORBIDDEN_LINKS = ["organization", "dashboard", "database", "admin", "settings", "notifications", "storages"];
|
const FORBIDDEN_LINKS = ["organization", "dashboard", "database", "admin", "notifications", "storages"];
|
||||||
|
|
||||||
|
|
||||||
export function BreadCrumbs({}: BreadCrumbsProps) {
|
export function BreadCrumbs({}: BreadCrumbsProps) {
|
||||||
+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);
|
||||||
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import {cn} from "@/lib/utils";
|
||||||
|
import {CircleSlash2, Plus} from "lucide-react";
|
||||||
|
import {forwardRef, HTMLAttributes} from "react";
|
||||||
|
|
||||||
|
type EmptyStatePlaceholderProps = {
|
||||||
|
url?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
text: string;
|
||||||
|
className?: string;
|
||||||
|
state?: string
|
||||||
|
} & HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export const EmptyStatePlaceholder = forwardRef<HTMLDivElement, EmptyStatePlaceholderProps>(({
|
||||||
|
url,
|
||||||
|
onClick,
|
||||||
|
text,
|
||||||
|
state,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}, ref) => {
|
||||||
|
const Container = (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-full flex-col items-center justify-center w-full rounded-2xl border border-dashed border-muted p-6 lg:p-10",
|
||||||
|
"transition-colors text-muted-foreground text-center space-y-4",
|
||||||
|
state != "empty" && "hover:bg-muted/50 hover:text-primary",
|
||||||
|
(onClick || url) && "cursor-pointer"
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{state == "empty" ?
|
||||||
|
<CircleSlash2 className="w-5 h-5 lg:w-6 lg:h-6"/>
|
||||||
|
:
|
||||||
|
<Plus className="w-5 h-5 lg:w-6 lg:h-6"/>
|
||||||
|
}
|
||||||
|
<p className="text-sm">{text}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
return (
|
||||||
|
<div className={cn(className)}>
|
||||||
|
<Link href={url}>{Container}</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={cn(className)} ref={ref} {...props}>{Container}</div>;
|
||||||
|
});
|
||||||
|
|
||||||
|
EmptyStatePlaceholder.displayName = "EmptyStatePlaceholder";
|
||||||
+42
-10
@@ -1,5 +1,3 @@
|
|||||||
//https://github.com/sersavan/shadcn-multi-select-component
|
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { CheckIcon, XCircle, ChevronDown, XIcon, WandSparkles } from "lucide-react";
|
import { CheckIcon, XCircle, ChevronDown, XIcon, WandSparkles } from "lucide-react";
|
||||||
@@ -233,53 +231,87 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto p-0" align="start" onEscapeKeyDown={() => setIsPopoverOpen(false)}>
|
<PopoverContent
|
||||||
|
className="w-auto p-0"
|
||||||
|
align="start"
|
||||||
|
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput placeholder="Search..." onKeyDown={handleInputKeyDown} />
|
<CommandInput placeholder="Search..." onKeyDown={handleInputKeyDown} />
|
||||||
<CommandList>
|
<CommandList className="max-h-none overflow-visible">
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
|
<div
|
||||||
|
className="max-h-[18rem] overflow-y-auto overflow-x-hidden overscroll-contain touch-pan-y pr-1"
|
||||||
|
style={{ WebkitOverflowScrolling: "touch" }}
|
||||||
|
onWheelCapture={(event) => event.stopPropagation()}
|
||||||
|
onTouchMoveCapture={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
<CommandItem key="all" onSelect={toggleAll} className="cursor-pointer">
|
<CommandItem key="all" onSelect={toggleAll} className="cursor-pointer">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||||
selectedValues.length === options.length ? "bg-primary text-primary-foreground" : "opacity-50 [&_svg]:invisible"
|
selectedValues.length === options.length
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "opacity-50 [&_svg]:invisible"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CheckIcon className="h-4 w-4" />
|
<CheckIcon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<span>(Select All)</span>
|
<span>(Select All)</span>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
|
|
||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
const isSelected = selectedValues.includes(option.value);
|
const isSelected = selectedValues.includes(option.value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandItem key={option.value} onSelect={() => toggleOption(option.value)} className="cursor-pointer">
|
<CommandItem
|
||||||
|
key={option.value}
|
||||||
|
onSelect={() => toggleOption(option.value)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||||
isSelected ? "bg-primary text-primary-foreground" : "opacity-50 [&_svg]:invisible"
|
isSelected
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "opacity-50 [&_svg]:invisible"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CheckIcon className="h-4 w-4" />
|
<CheckIcon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
{option.icon && <option.icon className="mr-2 h-4 w-4 text-muted-foreground" />}
|
|
||||||
|
{option.icon && (
|
||||||
|
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
|
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{selectedValues.length > 0 && (
|
{selectedValues.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<CommandItem onSelect={handleClear} className="flex-1 justify-center cursor-pointer">
|
<CommandItem
|
||||||
|
onSelect={handleClear}
|
||||||
|
className="flex-1 justify-center cursor-pointer"
|
||||||
|
>
|
||||||
Clear
|
Clear
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<Separator orientation="vertical" className="flex min-h-6 h-full" />
|
<Separator orientation="vertical" className="flex min-h-6 h-full" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<CommandItem onSelect={() => setIsPopoverOpen(false)} className="flex-1 justify-center cursor-pointer max-w-full">
|
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => setIsPopoverOpen(false)}
|
||||||
|
className="flex-1 justify-center cursor-pointer max-w-full"
|
||||||
|
>
|
||||||
Close
|
Close
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</div>
|
</div>
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
|
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
|
||||||
import { PaginationIndexes } from "@/components/wrappers/common/pagination/pagination-indexes";
|
import { PaginationIndexes } from "@/components/common/pagination-indexes";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export type paginationNavigationProps = {
|
export type paginationNavigationProps = {
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { PaginationNavigation } from "@/components/wrappers/common/pagination/pagination-navigation";
|
import { PaginationNavigation } from "@/components/common/pagination-navigation";
|
||||||
|
|
||||||
export type paginationNavigationProps = {
|
export type paginationNavigationProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {TablePaginationNavigation} from "@/components/wrappers/common/table/table-pagination-navigation";
|
import {TablePaginationNavigation} from "@/components/common/table-pagination-navigation";
|
||||||
import {TablePaginationSize} from "@/components/wrappers/common/table/table-pagination-size";
|
import {TablePaginationSize} from "@/components/common/table-pagination-size";
|
||||||
import {cn} from "@/lib/utils";
|
import {cn} from "@/lib/utils";
|
||||||
|
|
||||||
interface tablePaginationProps {
|
interface tablePaginationProps {
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user