mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
64
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9bfae9b9d | ||
|
|
2fca928cf0 | ||
|
|
8f3a362a3a | ||
|
|
62a6189788 | ||
|
|
2a6d631719 | ||
|
|
4942591d7f | ||
|
|
1760c1b852 | ||
|
|
0e7ebb79db | ||
|
|
727d9eb57c | ||
|
|
228ff75468 | ||
|
|
80018cac08 | ||
|
|
b2c19e7ed0 | ||
|
|
a64a80910d | ||
|
|
4eeeef3427 | ||
|
|
657586ddff | ||
|
|
797b287899 | ||
|
|
db2d95f2c5 | ||
|
|
16a8f259ab | ||
|
|
ee1cd8d08d | ||
|
|
e53046324c | ||
|
|
dd71977317 | ||
|
|
bee0da1454 | ||
|
|
b60feaa447 | ||
|
|
743e54614a | ||
|
|
bd96713af4 | ||
|
|
8f7f9b457a | ||
|
|
4c0d68eab8 | ||
|
|
8211407cf2 | ||
|
|
5fb6c7d36a | ||
|
|
30b1cf4493 | ||
|
|
f4789ac5fe | ||
|
|
5fd826ae71 | ||
|
|
22ce08f06c | ||
|
|
696c88f03f | ||
|
|
9d5d87e47b | ||
|
|
e32539e02c | ||
|
|
46ba931a6c | ||
|
|
4bac5143d6 | ||
|
|
bcb1674491 | ||
|
|
3cbd9677c2 | ||
|
|
b216ee1a80 | ||
|
|
32ec482449 | ||
|
|
5074748055 | ||
|
|
6fa11f834b | ||
|
|
25c7702127 | ||
|
|
b546556d08 | ||
|
|
379a85f6f6 | ||
|
|
92d2c14d6a | ||
|
|
a6c1f7d3f2 | ||
|
|
6fa6187cc4 | ||
|
|
848e7a1609 | ||
|
|
3e8c0a2896 | ||
|
|
11900b1500 | ||
|
|
5668a1411f | ||
|
|
6db60f111d | ||
|
|
5c682addc9 | ||
|
|
24810cc818 | ||
|
|
8caeb11da5 | ||
|
|
5e07b446fc | ||
|
|
760388307c | ||
|
|
eebd8500ee | ||
|
|
12112afa37 | ||
|
|
cddb0a7e05 | ||
|
|
21b317e4f1 |
+8
-15
@@ -1,37 +1,30 @@
|
||||
# Environnement
|
||||
NODE_ENV=production
|
||||
NODE_ENV=development
|
||||
|
||||
# Base de données
|
||||
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
|
||||
DATABASE_URL=postgresql://devuser:changeme@localhost:5433/devdb?schema=public
|
||||
|
||||
# Projet
|
||||
PROJECT_NAME="Portabase"
|
||||
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
||||
PROJECT_URL=http://app.portabase.io
|
||||
PROJECT_URL=http://localhost:8887
|
||||
PROJECT_SECRET=
|
||||
|
||||
# SMTP (email)
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_PORT=
|
||||
SMTP_USER=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM=
|
||||
#SMTP_SECURE="true"
|
||||
#Port 587 -> secure: false
|
||||
#Port 465 -> secure: true
|
||||
#By default : true
|
||||
|
||||
# Google
|
||||
AUTH_GOOGLE_ID=
|
||||
AUTH_GOOGLE_SECRET=
|
||||
AUTH_GOOGLE_METHOD=
|
||||
|
||||
# S3
|
||||
S3_ENDPOINT=http://app.s3.portabase.io
|
||||
S3_ACCESS_KEY=
|
||||
S3_SECRET_KEY=
|
||||
S3_BUCKET_NAME=portabase
|
||||
S3_PORT=9000
|
||||
S3_USE_SSL=true
|
||||
|
||||
# Storage Type (s3, local)
|
||||
STORAGE_TYPE=local
|
||||
|
||||
# Retention
|
||||
RETENTION_CRON="* * * * *"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: portabase
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -53,11 +53,16 @@ jobs:
|
||||
TAGS2="${{ inputs.image_name2 }}:$REF_NAME"
|
||||
if [[ "${{ inputs.add_latest }}" == "true" ]]; then
|
||||
TAGS="$TAGS,${{ inputs.image_name }}:latest"
|
||||
TAGS2="$TAGS2,${{ inputs.image_name }}:latest"
|
||||
TAGS2="$TAGS2,${{ inputs.image_name2 }}:latest"
|
||||
fi
|
||||
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
||||
echo "tags2=$TAGS2" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU (enables multi-arch emulation)
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub account 1
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||
@@ -70,6 +75,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: ${{ inputs.dockerfile }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.set-tags.outputs.tags }}
|
||||
target: ${{ inputs.target }}
|
||||
@@ -85,6 +91,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: ${{ inputs.dockerfile }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.set-tags.outputs.tags2 }}
|
||||
target: ${{ inputs.target }}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
.idea
|
||||
.vscode
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
|
||||
Generated
-8
@@ -1,8 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
Generated
-8
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/portabase.iml" filepath="$PROJECT_DIR$/.idea/portabase.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
-12
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/backend" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
-6
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"WillLuke.nextjs.hasPrompted": true
|
||||
}
|
||||
+2
-2
@@ -26,5 +26,5 @@ keywords:
|
||||
- web-ui
|
||||
- agent
|
||||
license: Apache-2.0
|
||||
version: 1.2.0-rc.1
|
||||
date-released: "2026-01-18"
|
||||
version: 1.2.4-rc.5
|
||||
date-released: "2026-01-28"
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
[](LICENSE)
|
||||
[](https://hub.docker.com/r/solucetechnologies/portabase)
|
||||
[](https://github.com/Portabase/portabase)
|
||||
[](https://www.buymeacoffee.com/portabase)
|
||||
|
||||
[](https://www.postgresql.org/)
|
||||
[](https://www.mysql.com/)
|
||||
[](https://mariadb.org/)
|
||||
[](https://www.mongodb.com/)
|
||||
[](https://github.com/Portabase/portabase)
|
||||
[](https://github.com/Portabase/portabase)
|
||||
|
||||
@@ -61,6 +63,7 @@ You have 4 ways to install Portabase:
|
||||
|
||||
[](https://github.com/Portabase/portabase/graphs/contributors)
|
||||
|
||||
[](https://www.buymeacoffee.com/portabase)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { PageParams } from "@/types/next";
|
||||
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
||||
import { AgentForm } from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
|
||||
import { notFound } from "next/navigation";
|
||||
import { db } from "@/db";
|
||||
|
||||
export default async function RoutePage(
|
||||
props: PageParams<{
|
||||
agentId: string;
|
||||
}>
|
||||
) {
|
||||
const { agentId } = await props.params;
|
||||
|
||||
const agent = await db.query.agent.findFirst({
|
||||
where: (fields, { eq }) => eq(fields.id, agentId),
|
||||
});
|
||||
|
||||
if (!agent) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>Edit {agent.name}</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<AgentForm defaultValues={agent} agentId={agent.id} />
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import {capitalizeFirstLetter} from "@/utils/text";
|
||||
import {generateEdgeKey} from "@/utils/edge_key";
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
import {AgentContentPage} from "@/components/wrappers/dashboard/agent/agent-content";
|
||||
import {AgentDialog} from "@/features/agents/components/agent.dialog";
|
||||
import {AgentType} from "@/features/agents/agents.schema";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{ agentId: string }>) {
|
||||
|
||||
@@ -39,10 +41,11 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
||||
</div>
|
||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link className={buttonVariants({variant: "outline"})}
|
||||
href={`/dashboard/agents/${agent.id}/edit`}>
|
||||
<GearIcon className="w-7 h-7"/>
|
||||
</Link>
|
||||
<AgentDialog agent={agent as AgentType & { id: string }}>
|
||||
<div className={buttonVariants({variant: "outline", className: "cursor-pointer"})}>
|
||||
<GearIcon className="w-7 h-7"/>
|
||||
</div>
|
||||
</AgentDialog>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {AgentForm} from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
|
||||
import {Metadata} from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Agent",
|
||||
};
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>Create new agent</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<AgentForm/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import {PageParams} from "@/types/next";
|
||||
import {AgentCard} from "@/components/wrappers/dashboard/agent/agent-card/agent-card";
|
||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {notFound} from "next/navigation";
|
||||
import {db} from "@/db";
|
||||
@@ -10,6 +9,7 @@ import * as drizzleDb from "@/db";
|
||||
import {desc, eq, not} from "drizzle-orm";
|
||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||
import {Metadata} from "next";
|
||||
import {AgentDialog} from "@/features/agents/components/agent.dialog";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Agents",
|
||||
@@ -36,9 +36,9 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
<PageTitle>Agents</PageTitle>
|
||||
{agents.length > 0 && (
|
||||
<PageActions>
|
||||
<Link href={"/dashboard/agents/new"}>
|
||||
<AgentDialog>
|
||||
<Button>+ Create Agent</Button>
|
||||
</Link>
|
||||
</AgentDialog>
|
||||
</PageActions>
|
||||
)}
|
||||
</PageHeader>
|
||||
@@ -46,12 +46,13 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
{agents.length > 0 ? (
|
||||
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1}/>
|
||||
) : (
|
||||
<EmptyStatePlaceholder
|
||||
url={"/dashboard/agents/new"}
|
||||
text={"Create new Agent"}
|
||||
/>
|
||||
<AgentDialog>
|
||||
<EmptyStatePlaceholder
|
||||
text={"Create new Agent"}
|
||||
/>
|
||||
</AgentDialog>
|
||||
)}
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import {notFound} from "next/navigation";
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
|
||||
import * as drizzleDb from "@/db";
|
||||
|
||||
import {db} from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{ projectId: string }>) {
|
||||
const {projectId} = await props.params;
|
||||
|
||||
const proj = await db.query.project.findFirst({
|
||||
where: eq(drizzleDb.schemas.project.id, projectId),
|
||||
with: {
|
||||
databases: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!proj) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
|
||||
const org = await db.query.organization.findFirst({
|
||||
where: eq(drizzleDb.schemas.organization.slug, "default"),
|
||||
});
|
||||
|
||||
if (!org) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const availableDatabases = (
|
||||
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)],
|
||||
})
|
||||
)
|
||||
// .filter((db) => db.project !== null) as DatabaseWith[];
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>Edit {proj.name}</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<ProjectForm
|
||||
organization={org}
|
||||
databases={availableDatabases as DatabaseWith[]}
|
||||
defaultValues={{...proj, databases: proj.databases.map((db) => db.id)}}
|
||||
projectId={proj.id}
|
||||
/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
||||
import {Page, PageContent, PageTitle} from "@/features/layout/page";
|
||||
import {buttonVariants} from "@/components/ui/button";
|
||||
import {GearIcon} from "@radix-ui/react-icons";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ButtonDeleteProject
|
||||
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
|
||||
@@ -15,6 +14,9 @@ import {eq} from "drizzle-orm";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
import {ProjectDialog} from "@/features/projects/components/project.dialog";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
import {ProjectWith} from "@/db/schema/06_project";
|
||||
|
||||
|
||||
export default async function RoutePage(props: PageParams<{
|
||||
@@ -51,6 +53,19 @@ export default async function RoutePage(props: PageParams<{
|
||||
redirect("/dashboard/projects");
|
||||
}
|
||||
|
||||
const availableDatabases = (
|
||||
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";
|
||||
|
||||
return (
|
||||
@@ -63,10 +78,15 @@ export default async function RoutePage(props: PageParams<{
|
||||
{!isMember && (
|
||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link className={buttonVariants({variant: "outline"})}
|
||||
href={`/dashboard/projects/${proj.id}/edit`}>
|
||||
<GearIcon className="w-7 h-7"/>
|
||||
</Link>
|
||||
<ProjectDialog
|
||||
databases={availableDatabases}
|
||||
organization={org}
|
||||
project={proj as ProjectWith}
|
||||
>
|
||||
<div className={buttonVariants({variant: "outline", className: "cursor-pointer"})}>
|
||||
<GearIcon className="w-7 h-7"/>
|
||||
</div>
|
||||
</ProjectDialog>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
||||
@@ -95,4 +115,4 @@ export default async function RoutePage(props: PageParams<{
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
|
||||
import {notFound} from "next/navigation";
|
||||
import {db} from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {getOrganization} from "@/lib/auth/auth";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
const organization = await getOrganization({});
|
||||
|
||||
if (!organization) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const availableDatabases = (
|
||||
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[];
|
||||
|
||||
const org = await db.query.organization.findFirst({
|
||||
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
|
||||
});
|
||||
|
||||
if (!org) notFound();
|
||||
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>Create new project</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<ProjectForm databases={availableDatabases} organization={org}/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import {PageParams} from "@/types/next";
|
||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||
import {Button} from "@/components/ui/button";
|
||||
@@ -10,6 +8,8 @@ import {notFound} from "next/navigation";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||
import {Metadata} from "next";
|
||||
import {ProjectDialog} from "@/features/projects/components/project.dialog";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Projects",
|
||||
@@ -36,6 +36,19 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
});
|
||||
const isMember = activeMember?.role === "member";
|
||||
|
||||
const availableDatabases = (
|
||||
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 (
|
||||
<Page>
|
||||
@@ -43,9 +56,9 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
<PageTitle>Projects</PageTitle>
|
||||
{(projects.length > 0 && !isMember) && (
|
||||
<PageActions>
|
||||
<Link href={`/dashboard/projects/new`}>
|
||||
<ProjectDialog databases={availableDatabases} organization={organization}>
|
||||
<Button>+ Create Project</Button>
|
||||
</Link>
|
||||
</ProjectDialog>
|
||||
</PageActions>
|
||||
)}
|
||||
</PageHeader>
|
||||
@@ -62,9 +75,11 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
) : isMember ? (
|
||||
<EmptyStatePlaceholder text="No project available"/>
|
||||
) : (
|
||||
<EmptyStatePlaceholder url="/dashboard/projects/new" text="Create new Project"/>
|
||||
<ProjectDialog databases={availableDatabases} organization={organization}>
|
||||
<EmptyStatePlaceholder text="Create new Project"/>
|
||||
</ProjectDialog>
|
||||
)}
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {notFound} from "next/navigation";
|
||||
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/organization-form/organization-form";
|
||||
import {getOrganization} from "@/lib/auth/auth";
|
||||
import {db} from "@/db";
|
||||
import {isNull} from "drizzle-orm";
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{
|
||||
|
||||
}>) {
|
||||
const user = await currentUser();
|
||||
|
||||
const organization = await getOrganization({});
|
||||
|
||||
const users = await db.query.user.findMany({
|
||||
where: (fields) => isNull(fields.deletedAt)
|
||||
});
|
||||
|
||||
if (!user || !users || !organization || organization.slug == "default") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return(
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>
|
||||
Edit {organization.name}
|
||||
</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<OrganizationForm currentUser={user} users={users} defaultValues={organization}/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -9,9 +9,13 @@ import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
||||
import {DeleteOrganizationButton} from "@/components/wrappers/dashboard/organization/delete-organization-button";
|
||||
import {
|
||||
EditButtonSettings
|
||||
} from "@/components/wrappers/dashboard/organization/settings/edit-button-settings/edit-button-settings";
|
||||
import {EditOrganizationDialog} from "@/features/organization/components/edit-organization.dialog";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {GearIcon} from "@radix-ui/react-icons";
|
||||
import {db} from "@/db";
|
||||
import {isNull} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Settings",
|
||||
@@ -22,7 +26,7 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||
const user = await currentUser();
|
||||
const activeMember = await getActiveMember()
|
||||
|
||||
if (!organization || !activeMember) {
|
||||
if (!organization || !activeMember || !user) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
@@ -30,6 +34,30 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||
const storageChannels = await getOrganizationStorageChannels(organization.id)
|
||||
const permissions = computeOrganizationPermissions(activeMember);
|
||||
|
||||
// Fetch users for the form
|
||||
const users = await db.query.user.findMany({
|
||||
where: (fields) => isNull(fields.deletedAt)
|
||||
});
|
||||
|
||||
// Re-fetch organization with members to match type expectations if needed,
|
||||
// although getOrganization returns OrganizationWithMembers usually, let's verify or cast.
|
||||
// getOrganization returns Organization type from database schema, which includes relations if defined in auth lib.
|
||||
// However, OrganizationForm expects OrganizationWithMembers.
|
||||
// Let's ensure we have members.
|
||||
|
||||
const organizationWithMembers = await db.query.organization.findFirst({
|
||||
where: eq(drizzleDb.schemas.organization.id, organization.id),
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
user: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!organizationWithMembers) notFound();
|
||||
|
||||
|
||||
return (
|
||||
<Page>
|
||||
@@ -41,7 +69,15 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||
<div className="flex items-center gap-2">
|
||||
{permissions.canManageSettings && organization.slug !== "default" && (
|
||||
<EditButtonSettings/>
|
||||
<EditOrganizationDialog
|
||||
organization={organizationWithMembers}
|
||||
users={users}
|
||||
currentUser={user}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<GearIcon className="w-7 h-7"/>
|
||||
</Button>
|
||||
</EditOrganizationDialog>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -62,4 +98,4 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||
</PageContent>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ export async function POST(
|
||||
const generatedId = formData.get("generatedId") as string | null;
|
||||
const method = formData.get("method") as string | null;
|
||||
|
||||
|
||||
|
||||
|
||||
if (!generatedId || !isUuidv4(generatedId)) {
|
||||
return NextResponse.json(
|
||||
{error: "generatedId is not a valid UUID"},
|
||||
@@ -113,6 +116,7 @@ export async function POST(
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{error: "File is required for successful backup"},
|
||||
@@ -127,7 +131,11 @@ export async function POST(
|
||||
const fileName = `${uuid}${fileExtension}`;
|
||||
const buffer = Buffer.from(await decryptedFile.arrayBuffer());
|
||||
|
||||
await storeBackupFiles(backup, database, buffer, fileName)
|
||||
|
||||
console.log(extension)
|
||||
|
||||
|
||||
const storageResults = await storeBackupFiles(backup, database, buffer, fileName)
|
||||
eventEmitter.emit('modification', {update: true});
|
||||
|
||||
await sendNotificationsBackupRestore(database, "success_backup");
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import {Body} from "./route";
|
||||
import {isUuidv4} from "@/utils/verify-uuid";
|
||||
import {getFileUrlPresignedLocal, getFileUrlPreSignedS3Action} from "@/features/upload/private/upload.action";
|
||||
import {Agent} from "@/db/schema/08_agent";
|
||||
import {Database} from "@/db/schema/07_database";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {db as dbClient} from "@/db";
|
||||
import {and, eq, inArray} from "drizzle-orm";
|
||||
import {EDbmsSchema} from "@/db/schema/types";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {SafeActionResult} from "next-safe-action";
|
||||
import {ZodString} from "zod";
|
||||
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import type {StorageInput} from "@/features/storages/types";
|
||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||
@@ -50,6 +46,12 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
{status: 500}
|
||||
);
|
||||
}
|
||||
|
||||
if (!dbmsEnumSchema.safeParse(db.dbms).success) {
|
||||
console.log(`Database type not available: ${db.dbms}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const [databaseCreated] = await dbClient
|
||||
.insert(drizzleDb.schemas.database)
|
||||
.values({
|
||||
@@ -77,10 +79,6 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
.returning();
|
||||
|
||||
|
||||
// const backup = await dbClient.query.backup.findFirst({
|
||||
// where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status, "waiting"))
|
||||
// })
|
||||
|
||||
const activeBackup = await dbClient.query.backup.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
|
||||
@@ -110,25 +108,6 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
restoreAction = true
|
||||
|
||||
|
||||
// const backupToRestore = await dbClient.query.backup.findFirst({
|
||||
// where: eq(drizzleDb.schemas.backup.id, restoration.backupId),
|
||||
// with: {
|
||||
// database: {
|
||||
// with: {
|
||||
// project: true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
|
||||
// const [settings] = await dbClient.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||
// if (!settings) {
|
||||
// return NextResponse.json(
|
||||
// {error: "Unable to find settings"},
|
||||
// {status: 500}
|
||||
// );
|
||||
// }
|
||||
|
||||
if (!restoration.backupStorage || restoration.backupStorage.status != "success" || !restoration.backupStorage.path) {
|
||||
restoreAction = false
|
||||
continue;
|
||||
@@ -141,6 +120,10 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
path: restoration.backupStorage.path,
|
||||
signedUrl: true,
|
||||
},
|
||||
metadata: {
|
||||
storageId: restoration.backupStorage.storageChannelId,
|
||||
fileKind: "backups"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import path from "path";
|
||||
import {db} from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import type {StorageInput} from "@/features/storages/types";
|
||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||
import {Readable} from "node:stream";
|
||||
@@ -15,17 +12,10 @@ export async function GET(
|
||||
const token = searchParams.get('token');
|
||||
const expires = searchParams.get('expires');
|
||||
const pathFromUrl = searchParams.get('path');
|
||||
const storageId = searchParams.get('storageId');
|
||||
|
||||
if (!pathFromUrl) {
|
||||
return NextResponse.json({error: "Missing file path in search params"}, {status: 404})
|
||||
}
|
||||
|
||||
const localStorageChannel = await db.query.storageChannel.findFirst({
|
||||
where: eq(drizzleDb.schemas.storageChannel.provider, "local"),
|
||||
})
|
||||
|
||||
if (!localStorageChannel) {
|
||||
return NextResponse.json({error: "No local storage channel found"})
|
||||
if (!pathFromUrl || !storageId) {
|
||||
return NextResponse.json({error: "Missing search params"}, {status: 404})
|
||||
}
|
||||
|
||||
const input: StorageInput = {
|
||||
@@ -34,14 +24,18 @@ export async function GET(
|
||||
path: pathFromUrl,
|
||||
signedUrl: true,
|
||||
},
|
||||
metadata: {
|
||||
storageId: storageId,
|
||||
fileKind: "backups",
|
||||
}
|
||||
};
|
||||
|
||||
console.debug(input);
|
||||
|
||||
const result = await dispatchStorage(input, undefined, localStorageChannel.id);
|
||||
const result = await dispatchStorage(input, undefined, storageId);
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json({error: "Enable to get file from local storage channel, an error occurred !"})
|
||||
return NextResponse.json({error: "Enable to get file from privided storage channel, an error occurred !"})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import {auth} from "@/lib/auth/auth";
|
||||
import {headers} from "next/headers";
|
||||
import {checkFileExistsInBucket, getObjectFromClient} from "@/utils/s3-file-management";
|
||||
import {env} from "@/env.mjs";
|
||||
import * as stream from "node:stream";
|
||||
import path from "path";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
@@ -16,28 +12,34 @@ export async function GET(
|
||||
req: Request,
|
||||
{params}: { params: Promise<{ fileName: string }> }
|
||||
) {
|
||||
|
||||
|
||||
const {searchParams} = new URL(req.url);
|
||||
const fileName = (await params).fileName;
|
||||
|
||||
const storageId = searchParams.get('storageId');
|
||||
|
||||
if (!fileName) return NextResponse.json({error: "Missing file parameter"}, {status: 400});
|
||||
|
||||
const session = await auth.api.getSession({headers: await headers()});
|
||||
if (!session) return NextResponse.json({error: "Unauthorized"}, {status: 403});
|
||||
|
||||
const settings = await db.query.setting.findFirst({
|
||||
where: eq(drizzleDb.schemas.setting.name, "system"),
|
||||
with: {
|
||||
storageChannel: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!settings || !settings.storageChannel) {
|
||||
return NextResponse.json({error: "Unable to get settings or no default storage channel"});
|
||||
if (!storageId) {
|
||||
return NextResponse.json({error: "Missing storageId in search params"}, {status: 404})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// const settings = await db.query.setting.findFirst({
|
||||
// where: eq(drizzleDb.schemas.setting.name, "system"),
|
||||
// with: {
|
||||
// storageChannel: true
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// if (!settings || !settings.storageChannel) {
|
||||
// return NextResponse.json({error: "Unable to get settings or no default storage channel"});
|
||||
// }
|
||||
|
||||
|
||||
const ext = fileName.split(".").pop()?.toLowerCase();
|
||||
const contentType =
|
||||
ext === "png"
|
||||
@@ -58,12 +60,17 @@ export async function GET(
|
||||
action: "get",
|
||||
data: {
|
||||
path: path,
|
||||
},
|
||||
metadata: {
|
||||
storageId: storageId,
|
||||
fileKind: "images"
|
||||
}
|
||||
}
|
||||
|
||||
const result = await dispatchStorage(input, undefined, settings.storageChannel.id);
|
||||
const result = await dispatchStorage(input, undefined, storageId);
|
||||
|
||||
if (!result.file || !Buffer.isBuffer(result.file)) {
|
||||
console.error(`An error occurred while getting file :`, result);
|
||||
return NextResponse.json(
|
||||
{error: "Invalid file payload"},
|
||||
{status: 500}
|
||||
@@ -0,0 +1,23 @@
|
||||
export async function GET(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
const code = url.searchParams.get("code");
|
||||
|
||||
const success = Boolean(code);
|
||||
|
||||
return new Response(
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>${success ? "Success" : "Failed"}</h1>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
{
|
||||
status: success ? 200 : 400,
|
||||
headers: {
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
# context: .
|
||||
# dockerfile: docker/dockerfile/Dockerfile
|
||||
# target: prod
|
||||
image: solucetechnologies/portabase:1.1.4-rc.2
|
||||
image: solucetechnologies/portabase:latest
|
||||
ports:
|
||||
- '8887:80'
|
||||
environment:
|
||||
|
||||
+5
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "portabase",
|
||||
"version": "1.2.0-rc.1",
|
||||
"version": "1.2.4-rc.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 8887",
|
||||
@@ -62,6 +62,7 @@
|
||||
"drizzle-orm": "^0.43.1",
|
||||
"drizzle-zod": "^0.7.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"googleapis": "^170.1.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.553.0",
|
||||
"minio": "^8.0.5",
|
||||
@@ -74,6 +75,7 @@
|
||||
"nodemailer": "^7.0.3",
|
||||
"npm-check-updates": "^18.0.1",
|
||||
"pg": "^8.16.0",
|
||||
"prettier": "^3.8.0",
|
||||
"react": "^19.2.0",
|
||||
"react-day-picker": "9.7.0",
|
||||
"react-dom": "^19.2.0",
|
||||
@@ -85,6 +87,7 @@
|
||||
"react-twc": "^1.4.2",
|
||||
"react-use-measure": "^2.1.7",
|
||||
"recharts": "^2.15.3",
|
||||
"sharp": "^0.34.5",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"sonner": "^2.0.3",
|
||||
@@ -121,5 +124,5 @@
|
||||
"typescript": "^5.8.3",
|
||||
"zenstack": "2.14.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.27.0"
|
||||
"packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316"
|
||||
}
|
||||
|
||||
Generated
+287
-23
@@ -124,7 +124,7 @@ importers:
|
||||
version: 6.0.0
|
||||
better-auth:
|
||||
specifier: 1.4.5
|
||||
version: 1.4.5(next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
version: 1.4.5(next@16.0.10(@babel/core@7.26.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@@ -152,6 +152,9 @@ importers:
|
||||
embla-carousel-react:
|
||||
specifier: ^8.6.0
|
||||
version: 8.6.0(react@19.2.3)
|
||||
googleapis:
|
||||
specifier: ^170.1.0
|
||||
version: 170.1.0
|
||||
input-otp:
|
||||
specifier: ^1.4.2
|
||||
version: 1.4.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
@@ -166,10 +169,10 @@ importers:
|
||||
version: 12.24.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
next:
|
||||
specifier: 16.0.10
|
||||
version: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
version: 16.0.10(@babel/core@7.26.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
next-safe-action:
|
||||
specifier: ^7.10.8
|
||||
version: 7.10.8(next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76)
|
||||
version: 7.10.8(next@16.0.10(@babel/core@7.26.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76)
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
@@ -188,6 +191,9 @@ importers:
|
||||
pg:
|
||||
specifier: ^8.16.0
|
||||
version: 8.16.3
|
||||
prettier:
|
||||
specifier: ^3.8.0
|
||||
version: 3.8.0
|
||||
react:
|
||||
specifier: ^19.2.0
|
||||
version: 19.2.3
|
||||
@@ -221,6 +227,9 @@ importers:
|
||||
recharts:
|
||||
specifier: ^2.15.3
|
||||
version: 2.15.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
sharp:
|
||||
specifier: ^0.34.5
|
||||
version: 0.34.5
|
||||
socket.io:
|
||||
specifier: ^4.8.1
|
||||
version: 4.8.3
|
||||
@@ -1732,6 +1741,10 @@ packages:
|
||||
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@prisma/client@6.7.0':
|
||||
resolution: {integrity: sha512-+k61zZn1XHjbZul8q6TdQLpuI/cvyfil87zqK2zpreNIXyXtpUv3+H/oM69hcsFcZXaokHJIzPAt5Z8C8eK2QA==}
|
||||
engines: {node: '>=18.18'}
|
||||
@@ -3427,6 +3440,10 @@ packages:
|
||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ajv-formats@2.1.1:
|
||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||
peerDependencies:
|
||||
@@ -3627,6 +3644,9 @@ packages:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
bignumber.js@9.3.1:
|
||||
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
|
||||
|
||||
binary-extensions@2.3.0:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3665,6 +3685,9 @@ packages:
|
||||
resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
@@ -3921,6 +3944,10 @@ packages:
|
||||
damerau-levenshtein@1.0.8:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
|
||||
data-uri-to-buffer@4.0.1:
|
||||
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
data-view-buffer@1.0.2:
|
||||
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4158,6 +4185,9 @@ packages:
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
|
||||
effect@3.18.4:
|
||||
resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==}
|
||||
|
||||
@@ -4426,6 +4456,9 @@ packages:
|
||||
exsolve@1.0.8:
|
||||
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
|
||||
|
||||
extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
|
||||
fast-check@3.23.2:
|
||||
resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
@@ -4480,6 +4513,10 @@ packages:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
||||
engines: {node: ^12.20 || >= 14.13}
|
||||
|
||||
file-entry-cache@8.0.0:
|
||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@@ -4519,6 +4556,10 @@ packages:
|
||||
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||
engines: {node: '>=0.4.x'}
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
fraction.js@4.3.7:
|
||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||
|
||||
@@ -4572,6 +4613,14 @@ packages:
|
||||
functions-have-names@1.2.3:
|
||||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||
|
||||
gaxios@7.1.3:
|
||||
resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
gcp-metadata@8.1.2:
|
||||
resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
generator-function@2.0.1:
|
||||
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4622,6 +4671,10 @@ packages:
|
||||
glob-to-regexp@0.4.1:
|
||||
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
||||
|
||||
glob@10.5.0:
|
||||
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||
hasBin: true
|
||||
|
||||
glob@11.1.0:
|
||||
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -4643,6 +4696,22 @@ packages:
|
||||
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
google-auth-library@10.5.0:
|
||||
resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
google-logging-utils@1.1.3:
|
||||
resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
googleapis-common@8.0.1:
|
||||
resolution: {integrity: sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
googleapis@170.1.0:
|
||||
resolution: {integrity: sha512-RLbc7yG6qzZqvAmGcgjvNIoZ7wpcCFxtc+HN+46etxDrlO4a8l5Cb7NxNQGhV91oRmL7mt56VoRoypAtEQEIKg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4650,6 +4719,10 @@ packages:
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
gtoken@8.0.0:
|
||||
resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
has-bigints@1.1.0:
|
||||
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4704,6 +4777,10 @@ packages:
|
||||
resolution: {integrity: sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
@@ -4897,6 +4974,9 @@ packages:
|
||||
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
jackspeak@4.1.1:
|
||||
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -4932,6 +5012,9 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
json-bigint@1.0.0:
|
||||
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
|
||||
|
||||
json-buffer@3.0.1:
|
||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||
|
||||
@@ -4963,6 +5046,12 @@ packages:
|
||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
jwa@2.0.1:
|
||||
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
|
||||
|
||||
jws@4.0.1:
|
||||
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
@@ -5121,6 +5210,9 @@ packages:
|
||||
lowlight@1.17.0:
|
||||
resolution: {integrity: sha512-vmtBgYKD+QVNy7tIa7ulz5d//Il9R4MooOVh4nkOf9R9Cb/Dk5TXMSTieg/vDulkBkIWj59/BIlyFQxT9X1oAQ==}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lru-cache@11.2.4:
|
||||
resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -5361,6 +5453,11 @@ packages:
|
||||
resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
|
||||
node-fetch-native@1.6.7:
|
||||
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
|
||||
|
||||
@@ -5373,6 +5470,10 @@ packages:
|
||||
encoding:
|
||||
optional: true
|
||||
|
||||
node-fetch@3.3.2:
|
||||
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
node-forge@1.3.3:
|
||||
resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==}
|
||||
engines: {node: '>= 6.13.0'}
|
||||
@@ -5527,6 +5628,10 @@ packages:
|
||||
path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
path-scurry@2.0.1:
|
||||
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -5669,8 +5774,8 @@ packages:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
prettier@3.7.4:
|
||||
resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
|
||||
prettier@3.8.0:
|
||||
resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
@@ -5725,6 +5830,10 @@ packages:
|
||||
qr.js@0.0.0:
|
||||
resolution: {integrity: sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==}
|
||||
|
||||
qs@6.14.1:
|
||||
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
query-string@7.1.3:
|
||||
resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -5925,6 +6034,10 @@ packages:
|
||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
|
||||
rimraf@5.0.10:
|
||||
resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
|
||||
hasBin: true
|
||||
|
||||
rou3@0.7.12:
|
||||
resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==}
|
||||
|
||||
@@ -6437,6 +6550,9 @@ packages:
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
url-template@2.0.8:
|
||||
resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==}
|
||||
|
||||
use-callback-ref@1.3.3:
|
||||
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -6554,6 +6670,10 @@ packages:
|
||||
web-encoding@1.1.5:
|
||||
resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==}
|
||||
|
||||
web-streams-polyfill@3.3.3:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
@@ -8028,6 +8148,9 @@ snapshots:
|
||||
|
||||
'@phc/format@1.0.0': {}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3)':
|
||||
optionalDependencies:
|
||||
prisma: 6.7.0(typescript@5.9.3)
|
||||
@@ -9473,7 +9596,7 @@ snapshots:
|
||||
'@react-email/render@1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
html-to-text: 9.0.5
|
||||
prettier: 3.7.4
|
||||
prettier: 3.8.0
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
react-promise-suspense: 0.3.4
|
||||
@@ -9481,7 +9604,7 @@ snapshots:
|
||||
'@react-email/render@2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
html-to-text: 9.0.5
|
||||
prettier: 3.7.4
|
||||
prettier: 3.8.0
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
|
||||
@@ -10013,7 +10136,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 22.19.3
|
||||
tapable: 2.3.0
|
||||
webpack: 5.104.1(esbuild@0.27.2)
|
||||
webpack: 5.104.1(esbuild@0.25.10)
|
||||
transitivePeerDependencies:
|
||||
- '@swc/core'
|
||||
- esbuild
|
||||
@@ -10430,6 +10553,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
ajv-formats@2.1.1(ajv@8.17.1):
|
||||
optionalDependencies:
|
||||
ajv: 8.17.1
|
||||
@@ -10608,7 +10733,7 @@ snapshots:
|
||||
|
||||
bcryptjs@2.4.3: {}
|
||||
|
||||
better-auth@1.4.5(next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
better-auth@1.4.5(next@16.0.10(@babel/core@7.26.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
dependencies:
|
||||
'@better-auth/core': 1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)
|
||||
'@better-auth/telemetry': 1.4.5(@better-auth/core@1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@3.25.76))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0))
|
||||
@@ -10624,7 +10749,7 @@ snapshots:
|
||||
nanostores: 1.1.0
|
||||
zod: 4.3.5
|
||||
optionalDependencies:
|
||||
next: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
next: 16.0.10(@babel/core@7.26.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
|
||||
@@ -10637,6 +10762,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
zod: 4.3.5
|
||||
|
||||
bignumber.js@9.3.1: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
bl@4.1.0:
|
||||
@@ -10678,6 +10805,8 @@ snapshots:
|
||||
|
||||
buffer-crc32@1.0.0: {}
|
||||
|
||||
buffer-equal-constant-time@1.0.1: {}
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
buffer@5.7.1:
|
||||
@@ -10957,6 +11086,8 @@ snapshots:
|
||||
|
||||
damerau-levenshtein@1.0.8: {}
|
||||
|
||||
data-uri-to-buffer@4.0.1: {}
|
||||
|
||||
data-view-buffer@1.0.2:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -11116,6 +11247,10 @@ snapshots:
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
effect@3.18.4:
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
@@ -11647,6 +11782,8 @@ snapshots:
|
||||
|
||||
exsolve@1.0.8: {}
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
fast-check@3.23.2:
|
||||
dependencies:
|
||||
pure-rand: 6.1.0
|
||||
@@ -11699,6 +11836,11 @@ snapshots:
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 3.3.3
|
||||
|
||||
file-entry-cache@8.0.0:
|
||||
dependencies:
|
||||
flat-cache: 4.0.1
|
||||
@@ -11736,6 +11878,10 @@ snapshots:
|
||||
|
||||
format@0.2.2: {}
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
dependencies:
|
||||
fetch-blob: 3.2.0
|
||||
|
||||
fraction.js@4.3.7: {}
|
||||
|
||||
framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
@@ -11780,6 +11926,23 @@ snapshots:
|
||||
|
||||
functions-have-names@1.2.3: {}
|
||||
|
||||
gaxios@7.1.3:
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
node-fetch: 3.3.2
|
||||
rimraf: 5.0.10
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
gcp-metadata@8.1.2:
|
||||
dependencies:
|
||||
gaxios: 7.1.3
|
||||
google-logging-utils: 1.1.3
|
||||
json-bigint: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
generator-function@2.0.1: {}
|
||||
|
||||
gensync@1.0.0-beta.2: {}
|
||||
@@ -11837,6 +12000,15 @@ snapshots:
|
||||
|
||||
glob-to-regexp@0.4.1: {}
|
||||
|
||||
glob@10.5.0:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.5
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
glob@11.1.0:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
@@ -11857,10 +12029,48 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
gopd: 1.2.0
|
||||
|
||||
google-auth-library@10.5.0:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
gaxios: 7.1.3
|
||||
gcp-metadata: 8.1.2
|
||||
google-logging-utils: 1.1.3
|
||||
gtoken: 8.0.0
|
||||
jws: 4.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
google-logging-utils@1.1.3: {}
|
||||
|
||||
googleapis-common@8.0.1:
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
gaxios: 7.1.3
|
||||
google-auth-library: 10.5.0
|
||||
qs: 6.14.1
|
||||
url-template: 2.0.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
googleapis@170.1.0:
|
||||
dependencies:
|
||||
google-auth-library: 10.5.0
|
||||
googleapis-common: 8.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
gtoken@8.0.0:
|
||||
dependencies:
|
||||
gaxios: 7.1.3
|
||||
jws: 4.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
has-bigints@1.1.0: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
@@ -11920,6 +12130,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
ignore@5.3.2: {}
|
||||
@@ -12100,6 +12317,12 @@ snapshots:
|
||||
has-symbols: 1.1.0
|
||||
set-function-name: 2.0.2
|
||||
|
||||
jackspeak@3.4.3:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jackspeak@4.1.1:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
@@ -12126,6 +12349,10 @@ snapshots:
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
|
||||
json-bigint@1.0.0:
|
||||
dependencies:
|
||||
bignumber.js: 9.3.1
|
||||
|
||||
json-buffer@3.0.1: {}
|
||||
|
||||
json-parse-even-better-errors@2.3.1: {}
|
||||
@@ -12155,6 +12382,17 @@ snapshots:
|
||||
object.assign: 4.1.7
|
||||
object.values: 1.2.1
|
||||
|
||||
jwa@2.0.1:
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
jws@4.0.1:
|
||||
dependencies:
|
||||
jwa: 2.0.1
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
@@ -12289,6 +12527,8 @@ snapshots:
|
||||
fault: 1.0.4
|
||||
highlight.js: 10.4.1
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.2.4: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
@@ -12423,9 +12663,9 @@ snapshots:
|
||||
|
||||
neo-async@2.6.2: {}
|
||||
|
||||
next-safe-action@7.10.8(next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76):
|
||||
next-safe-action@7.10.8(next@16.0.10(@babel/core@7.26.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76):
|
||||
dependencies:
|
||||
next: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
next: 16.0.10(@babel/core@7.26.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
optionalDependencies:
|
||||
@@ -12454,12 +12694,12 @@ snapshots:
|
||||
'@next/swc-linux-x64-musl': 15.5.2
|
||||
'@next/swc-win32-arm64-msvc': 15.5.2
|
||||
'@next/swc-win32-x64-msvc': 15.5.2
|
||||
sharp: 0.34.4
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
next@16.0.10(@babel/core@7.26.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
dependencies:
|
||||
'@next/env': 16.0.10
|
||||
'@swc/helpers': 0.5.15
|
||||
@@ -12467,7 +12707,7 @@ snapshots:
|
||||
postcss: 8.4.31
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3)
|
||||
styled-jsx: 5.1.6(@babel/core@7.26.10)(react@19.2.3)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.0.10
|
||||
'@next/swc-darwin-x64': 16.0.10
|
||||
@@ -12491,12 +12731,20 @@ snapshots:
|
||||
|
||||
node-cron@4.2.1: {}
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch-native@1.6.7: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
node-fetch@3.3.2:
|
||||
dependencies:
|
||||
data-uri-to-buffer: 4.0.1
|
||||
fetch-blob: 3.2.0
|
||||
formdata-polyfill: 4.0.10
|
||||
|
||||
node-forge@1.3.3: {}
|
||||
|
||||
node-gyp-build@4.8.4: {}
|
||||
@@ -12677,6 +12925,11 @@ snapshots:
|
||||
|
||||
path-parse@1.0.7: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.2
|
||||
|
||||
path-scurry@2.0.1:
|
||||
dependencies:
|
||||
lru-cache: 11.2.4
|
||||
@@ -12798,7 +13051,7 @@ snapshots:
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@3.7.4: {}
|
||||
prettier@3.8.0: {}
|
||||
|
||||
pretty-bytes@6.1.1: {}
|
||||
|
||||
@@ -12864,6 +13117,10 @@ snapshots:
|
||||
|
||||
qr.js@0.0.0: {}
|
||||
|
||||
qs@6.14.1:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
||||
query-string@7.1.3:
|
||||
dependencies:
|
||||
decode-uri-component: 0.2.2
|
||||
@@ -13128,6 +13385,10 @@ snapshots:
|
||||
|
||||
reusify@1.1.0: {}
|
||||
|
||||
rimraf@5.0.10:
|
||||
dependencies:
|
||||
glob: 10.5.0
|
||||
|
||||
rou3@0.7.12: {}
|
||||
|
||||
run-parallel@1.2.0:
|
||||
@@ -13273,7 +13534,6 @@ snapshots:
|
||||
'@img/sharp-win32-arm64': 0.34.5
|
||||
'@img/sharp-win32-ia32': 0.34.5
|
||||
'@img/sharp-win32-x64': 0.34.5
|
||||
optional: true
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
@@ -13527,12 +13787,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.26.10
|
||||
|
||||
styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.3):
|
||||
styled-jsx@5.1.6(@babel/core@7.26.10)(react@19.2.3):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 19.2.3
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/core': 7.26.10
|
||||
|
||||
sucrase@3.35.1:
|
||||
dependencies:
|
||||
@@ -13620,14 +13880,14 @@ snapshots:
|
||||
ansi-escapes: 4.3.2
|
||||
supports-hyperlinks: 2.3.0
|
||||
|
||||
terser-webpack-plugin@5.3.16(esbuild@0.25.10)(webpack@5.104.1(esbuild@0.25.10)):
|
||||
terser-webpack-plugin@5.3.16(esbuild@0.25.10)(webpack@5.104.1(esbuild@0.27.2)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 4.3.3
|
||||
serialize-javascript: 6.0.2
|
||||
terser: 5.44.1
|
||||
webpack: 5.104.1(esbuild@0.27.2)
|
||||
webpack: 5.104.1(esbuild@0.25.10)
|
||||
optionalDependencies:
|
||||
esbuild: 0.25.10
|
||||
|
||||
@@ -13837,6 +14097,8 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
url-template@2.0.8: {}
|
||||
|
||||
use-callback-ref@1.3.3(@types/react@19.0.10)(react@19.0.0):
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
@@ -13974,11 +14236,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@zxing/text-encoding': 0.9.0
|
||||
|
||||
web-streams-polyfill@3.3.3: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
webpack-sources@3.3.3: {}
|
||||
|
||||
webpack@5.104.1(esbuild@0.27.2):
|
||||
webpack@5.104.1(esbuild@0.25.10):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
'@types/estree': 1.0.8
|
||||
@@ -14002,7 +14266,7 @@ snapshots:
|
||||
neo-async: 2.6.2
|
||||
schema-utils: 4.3.3
|
||||
tapable: 2.3.0
|
||||
terser-webpack-plugin: 5.3.16(esbuild@0.25.10)(webpack@5.104.1(esbuild@0.25.10))
|
||||
terser-webpack-plugin: 5.3.16(esbuild@0.25.10)(webpack@5.104.1(esbuild@0.27.2))
|
||||
watchpack: 2.5.0
|
||||
webpack-sources: 3.3.3
|
||||
transitivePeerDependencies:
|
||||
|
||||
@@ -1,2 +1,13 @@
|
||||
onlyBuiltDependencies:
|
||||
- '@prisma/client'
|
||||
- '@prisma/engines'
|
||||
- argon2
|
||||
- bcrypt
|
||||
- cpu-features
|
||||
- esbuild
|
||||
- prisma
|
||||
- protobufjs
|
||||
- sharp
|
||||
- ssh2
|
||||
- unrs-resolver
|
||||
- zenstack
|
||||
|
||||
@@ -54,11 +54,12 @@ function checkRouteExists(pathname: string) {
|
||||
/^\/api\/agent\/[^/]+\/status\/?$/,
|
||||
/^\/api\/agent\/[^/]+\/backup\/?$/,
|
||||
/^\/api\/agent\/[^/]+\/restore\/?$/,
|
||||
/^\/api\/files\/?$/,
|
||||
/^\/api\/images\/[^/]+\/?$/,
|
||||
/^\/api\/files\/images\/[^/]+\/?$/,
|
||||
/^\/api\/files\/backups\/?$/,
|
||||
/^\/api\/events\/?$/,
|
||||
/^\/api\/init\/?$/,
|
||||
/^\/api\/config\/?$/,
|
||||
/^\/api\/google\/drive\/callback\/?$/,
|
||||
];
|
||||
return routePatterns.some((pattern) => pattern.test(pathname));
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -58,7 +58,7 @@ function DialogContent({
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
"max-h-[90vh] overflow-y-auto",
|
||||
"max-h-[90vh] overflow-y-auto overflow-x-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import {cn} from "@/lib/utils";
|
||||
|
||||
export type ConnectionCircleProps = {
|
||||
date?: Date | null;
|
||||
};
|
||||
|
||||
export const ConnectionCircle = ({date}: ConnectionCircleProps) => {
|
||||
let style = "bg-gray-300 border-gray-400";
|
||||
|
||||
if (date instanceof Date && !isNaN(date.getTime())) {
|
||||
const now = Date.now();
|
||||
const timestamp = date.getTime();
|
||||
const interval_seconds = (now - timestamp) / 1000;
|
||||
|
||||
if (interval_seconds < 55) {
|
||||
style = "bg-green-400 border-green-600";
|
||||
} else if (interval_seconds <= 60) {
|
||||
style = "bg-orange-400 border-orange-600";
|
||||
} else {
|
||||
style = "bg-red-400 border-red-600";
|
||||
}
|
||||
} else {
|
||||
console.warn("Invalid date passed to ConnectionCircle:", date);
|
||||
}
|
||||
|
||||
return <div className={cn("w-5 h-5 rounded-full border-4", style)}/>;
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import {cn} from "@/lib/utils";
|
||||
|
||||
export type ConnectionIndicatorProps = {
|
||||
date?: Date | null;
|
||||
};
|
||||
|
||||
export const ConnectionIndicator = ({date}: ConnectionIndicatorProps) => {
|
||||
let style = "bg-gray-300";
|
||||
|
||||
if (date instanceof Date && !isNaN(date.getTime())) {
|
||||
const intervalSeconds = (Date.now() - date.getTime()) / 1000;
|
||||
|
||||
if (intervalSeconds < 55) {
|
||||
style = "bg-green-500";
|
||||
} else if (intervalSeconds <= 60) {
|
||||
style = "bg-orange-400";
|
||||
} else {
|
||||
style = "bg-red-500";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-3 h-3">
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
"absolute -inset-0.25 rounded-full opacity-60 animate-ping",
|
||||
style
|
||||
)}
|
||||
style={{
|
||||
animationDuration: "2s",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-3 h-3 rounded-full shadow-sm animate-pulse",
|
||||
style
|
||||
)}
|
||||
style={{
|
||||
animationDuration: "2s",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +1,22 @@
|
||||
import Link from "next/link";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {Plus} from "lucide-react";
|
||||
import {forwardRef} from "react";
|
||||
|
||||
type EmptyStatePlaceholderProps = {
|
||||
url?: string;
|
||||
onClick?: () => void;
|
||||
text: string;
|
||||
className?: string;
|
||||
};
|
||||
} & React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const EmptyStatePlaceholder = ({
|
||||
export const EmptyStatePlaceholder = forwardRef<HTMLDivElement, EmptyStatePlaceholderProps>(({
|
||||
url,
|
||||
onClick,
|
||||
text,
|
||||
className,
|
||||
}: EmptyStatePlaceholderProps) => {
|
||||
...props
|
||||
}, ref) => {
|
||||
const Container = (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -37,5 +39,7 @@ export const EmptyStatePlaceholder = ({
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={cn(className)}>{Container}</div>;
|
||||
};
|
||||
return <div className={cn(className)} ref={ref} {...props}>{Container}</div>;
|
||||
});
|
||||
|
||||
EmptyStatePlaceholder.displayName = "EmptyStatePlaceholder";
|
||||
@@ -15,7 +15,7 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
|
||||
* Variants for the multi-select component to handle different styles.
|
||||
* Uses class-variance-authority (cva) to define different styles based on "variant" prop.
|
||||
*/
|
||||
const multiSelectVariants = cva("m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300", {
|
||||
const multiSelectVariants = cva("m-1 transition ease-in-out duration-300", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-foreground/10 text-foreground bg-card hover:bg-card/80",
|
||||
@@ -177,13 +177,13 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
||||
return (
|
||||
<Badge
|
||||
key={value}
|
||||
className={cn(isAnimating ? "animate-bounce" : "", multiSelectVariants({ variant }))}
|
||||
className={cn(isAnimating ? "animate-bounce" : "", "max-w-full", multiSelectVariants({ variant }))}
|
||||
style={{ animationDuration: `${animation}s` }}
|
||||
>
|
||||
{IconComponent && <IconComponent className="h-4 w-4 mr-2" />}
|
||||
{option?.label}
|
||||
<span className="truncate">{option?.label}</span>
|
||||
<XCircle
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
className="ml-2 h-4 w-4 cursor-pointer shrink-0"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
toggleOption(value);
|
||||
@@ -196,6 +196,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
||||
<Badge
|
||||
className={cn(
|
||||
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
|
||||
"max-w-full",
|
||||
isAnimating ? "animate-bounce" : "",
|
||||
multiSelectVariants({ variant })
|
||||
)}
|
||||
@@ -203,7 +204,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
||||
>
|
||||
{`+ ${selectedValues.length - maxCount} more`}
|
||||
<XCircle
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
className="ml-2 h-4 w-4 cursor-pointer shrink-0"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
clearExtraOptions();
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ export const NotificationChannelFormSchema = ChannelFormSchema.extend({
|
||||
});
|
||||
|
||||
export const StorageChannelFormSchema = ChannelFormSchema.extend({
|
||||
provider: z.enum(["local", "s3"], {required_error: "Provider is required"}),
|
||||
provider: z.enum(["local", "s3", "google-drive"], {required_error: "Provider is required"}),
|
||||
});
|
||||
|
||||
|
||||
|
||||
+46
-8
@@ -25,8 +25,8 @@ import {
|
||||
ChannelKind,
|
||||
renderChannelForm
|
||||
} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||
import {storageTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/storage";
|
||||
import {notificationTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/notification";
|
||||
import {storageProviders} from "@/components/wrappers/dashboard/admin/channels/helpers/storage";
|
||||
import {notificationProviders} from "@/components/wrappers/dashboard/admin/channels/helpers/notification";
|
||||
import {
|
||||
ChannelTestButton
|
||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-test-button";
|
||||
@@ -94,7 +94,7 @@ export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}
|
||||
});
|
||||
|
||||
const provider = form.watch("provider");
|
||||
const channelTypes = kind == "notification" ? notificationTypes : storageTypes
|
||||
const channelTypes = kind == "notification" ? notificationProviders : storageProviders
|
||||
const selectedProviderDetails = channelTypes.find(t => t.value === provider);
|
||||
|
||||
if (isCreate && !provider) {
|
||||
@@ -106,18 +106,55 @@ export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}
|
||||
<Card
|
||||
key={type.value}
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-center gap-3 p-4 cursor-pointer hover:bg-accent/50 hover:border-primary/50 transition-all",
|
||||
"relative flex flex-col items-center justify-center gap-3 p-4 transition-all border",
|
||||
type.preview
|
||||
? "cursor-not-allowed bg-muted/60 border-border text-muted-foreground"
|
||||
: "cursor-pointer bg-background hover:bg-accent/40 hover:border-primary/50"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (type.preview) return;
|
||||
form.setValue("provider", type.value as any);
|
||||
form.setValue("config", {});
|
||||
}}
|
||||
>
|
||||
<div className="h-10 w-10 bg-secondary rounded-full flex items-center justify-center">
|
||||
<Icon className="h-6 w-6 text-foreground"/>
|
||||
|
||||
{type.preview && (
|
||||
<div
|
||||
className="absolute bottom-0 right-0 overflow-hidden w-20 h-20 pointer-events-none">
|
||||
<div
|
||||
className="absolute bottom-0 right-0 w-38 h-5 flex items-center justify-center
|
||||
transform -rotate-45 translate-x-14 -translate-y-4"
|
||||
style={{backgroundColor: "#FE6702"}}
|
||||
>
|
||||
<span className="text-white text-[7px] pr-3 font-medium text-center w-full">COMING SOON</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full flex items-center justify-center transition-colors",
|
||||
type.preview
|
||||
? "bg-muted"
|
||||
: "bg-accent text-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={cn(
|
||||
"h-6 w-6",
|
||||
type.preview ? "text-muted-foreground" : "text-foreground"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-medium text-sm">{type.label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium text-sm text-center leading-tight",
|
||||
type.preview ? "text-muted-foreground" : "text-foreground"
|
||||
)}
|
||||
>
|
||||
{type.label}
|
||||
</span>
|
||||
</Card>
|
||||
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -213,4 +250,5 @@ export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+1
@@ -46,6 +46,7 @@ export const ChannelTestButton = ({channel, organizationId, kind}: NotifierTestC
|
||||
if (result.success) {
|
||||
toast.success("Successfully connected to storage channel");
|
||||
} else {
|
||||
console.error(result);
|
||||
toast.error("An error occurred while testing the storage channel, check your configuration");
|
||||
}
|
||||
} else {
|
||||
|
||||
+33
-1
@@ -3,7 +3,6 @@ import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/compon
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
|
||||
|
||||
type NotifierNtfyFormProps = {
|
||||
form: UseFormReturn<any, any, any>
|
||||
}
|
||||
@@ -12,6 +11,7 @@ export const NotifierNtfyForm = ({form}: NotifierNtfyFormProps) => {
|
||||
return (
|
||||
<>
|
||||
<Separator className="my-1"/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.ntfyTopic"
|
||||
@@ -25,6 +25,7 @@ export const NotifierNtfyForm = ({form}: NotifierNtfyFormProps) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.ntfyServerUrl"
|
||||
@@ -39,6 +40,7 @@ export const NotifierNtfyForm = ({form}: NotifierNtfyFormProps) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.ntfyToken"
|
||||
@@ -53,6 +55,36 @@ export const NotifierNtfyForm = ({form}: NotifierNtfyFormProps) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Separator className="my-1"/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.ntfyUsername"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Basic Auth Username (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="username"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.ntfyPassword"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Basic Auth Password (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="password" placeholder="password"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
|
||||
import {UseFormReturn} from "react-hook-form";
|
||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {PasswordInput} from "@/components/ui/password-input";
|
||||
import {
|
||||
googleDriveRefreshTokenAction
|
||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/google-drive/helpers";
|
||||
import {toast} from "sonner";
|
||||
|
||||
type StorageGoogleDriveFormProps = {
|
||||
form: UseFormReturn<any>;
|
||||
};
|
||||
|
||||
export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => {
|
||||
const refreshToken = form.watch("config.refreshToken");
|
||||
const isConnected = Boolean(refreshToken);
|
||||
|
||||
const handleConnect = () => {
|
||||
const clientId = form.getValues("config.clientId");
|
||||
const clientSecret = form.getValues("config.clientSecret");
|
||||
const redirectUri = `${window.location.origin}/api/google/drive/callback`;
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
form.setError("config.clientId", {message: "Client ID and Secret are required"});
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = encodeURIComponent("https://www.googleapis.com/auth/drive.file");
|
||||
const oauthUrl =
|
||||
`https://accounts.google.com/o/oauth2/v2/auth` +
|
||||
`?client_id=${clientId}` +
|
||||
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
||||
`&response_type=code` +
|
||||
`&scope=${scope}` +
|
||||
`&access_type=offline` +
|
||||
`&prompt=consent`;
|
||||
|
||||
const oauthWindow = window.open(oauthUrl, "_blank", "width=500,height=600");
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
if (!oauthWindow || oauthWindow.closed) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
if (oauthWindow.location.href.startsWith(redirectUri)) {
|
||||
const code = new URL(oauthWindow.location.href).searchParams.get("code");
|
||||
if (!code) return;
|
||||
|
||||
const result = await googleDriveRefreshTokenAction({
|
||||
code,
|
||||
clientId,
|
||||
clientSecret,
|
||||
redirectUri,
|
||||
});
|
||||
const inner = result?.data;
|
||||
|
||||
if (inner?.success) {
|
||||
toast.success(inner.actionSuccess?.message);
|
||||
|
||||
if (!inner.value) {
|
||||
form.setError("config", {message: "OAuth succeeded but no refresh token returned"});
|
||||
return;
|
||||
}
|
||||
|
||||
form.setValue("config.refreshToken", inner.value, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
} else {
|
||||
toast.error(inner?.actionError?.message);
|
||||
}
|
||||
|
||||
|
||||
oauthWindow.close();
|
||||
clearInterval(interval);
|
||||
}
|
||||
} catch {
|
||||
// Ignore cross-origin errors until redirect
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Separator className="my-1"/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.clientId"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="xxxx.apps.googleusercontent.com"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.clientSecret"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client Secret</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.folderId"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Folder ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<input type="hidden" {...form.register("config.refreshToken")} />
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button type="button" onClick={handleConnect}>
|
||||
{isConnected ? "Reconnect Google Drive" : "Connect Google Drive"}
|
||||
</Button>
|
||||
|
||||
{isConnected && (
|
||||
<span className="text-sm text-green-600 font-medium">
|
||||
Google Drive connected
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
"use server"
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
|
||||
|
||||
export const googleDriveRefreshTokenAction = userAction.schema(
|
||||
z.object({
|
||||
code: z.string(),
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
redirectUri: z.string(),
|
||||
})).action(async ({parsedInput}): Promise<ServerActionResult<string>> => {
|
||||
const {code, clientId, clientSecret, redirectUri} = parsedInput;
|
||||
|
||||
try {
|
||||
|
||||
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
code: code,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: redirectUri,
|
||||
}),
|
||||
});
|
||||
|
||||
const tokens = await tokenRes.json();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: tokens.refresh_token,
|
||||
actionSuccess: {
|
||||
message: "Refresh token successfully fetched",
|
||||
messageParams: {code: code},
|
||||
},
|
||||
};
|
||||
}catch {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "An error occurred",
|
||||
status: 404,
|
||||
messageParams: {code: code},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
@@ -20,16 +20,18 @@ import {
|
||||
import {
|
||||
NotifierWebhookForm
|
||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/webhook.form";
|
||||
import {notificationTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/notification";
|
||||
import {storageTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/storage";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||
import {
|
||||
notificationProviders,
|
||||
} from "@/components/wrappers/dashboard/admin/channels/helpers/notification";
|
||||
import {storageProviders} from "@/components/wrappers/dashboard/admin/channels/helpers/storage";
|
||||
import {ForwardRefExoticComponent, JSX, RefAttributes, SVGProps} from "react";
|
||||
import {LucideProps} from "lucide-react";
|
||||
import {
|
||||
StorageS3Form
|
||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/s3.form";
|
||||
import {
|
||||
StorageGoogleDriveForm
|
||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/google-drive.form";
|
||||
|
||||
export type ChannelKind = "notification" | "storage";
|
||||
|
||||
@@ -45,19 +47,21 @@ export function getChannelTextBasedOnKind(kind: ChannelKind) {
|
||||
}
|
||||
|
||||
|
||||
type ProviderIconTypes = {
|
||||
export type ProviderIconTypes = {
|
||||
value: string
|
||||
label: string
|
||||
icon: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>
|
||||
preview?: boolean
|
||||
} | {
|
||||
value: string
|
||||
label: string
|
||||
icon: (props: SVGProps<SVGSVGElement>) => JSX.Element
|
||||
preview?: boolean
|
||||
}
|
||||
|
||||
const providerIcons: ProviderIconTypes[] = [
|
||||
...notificationTypes,
|
||||
...storageTypes,
|
||||
export const providerIcons: ProviderIconTypes[] = [
|
||||
...notificationProviders,
|
||||
...storageProviders,
|
||||
];
|
||||
|
||||
|
||||
@@ -85,6 +89,8 @@ export const renderChannelForm = (provider: string | undefined, form: UseFormRet
|
||||
return <NotifierWebhookForm form={form}/>;
|
||||
case "s3":
|
||||
return <StorageS3Form form={form}/>
|
||||
case "google-drive":
|
||||
return <StorageGoogleDriveForm form={form}/>
|
||||
case "local":
|
||||
return <></>
|
||||
default:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {Mail} from "lucide-react";
|
||||
import type { SVGProps } from 'react';
|
||||
import {ProviderIconTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||
|
||||
|
||||
|
||||
export const notificationTypes = [
|
||||
|
||||
export const notificationProviders: ProviderIconTypes[] = [
|
||||
{value: "smtp", label: "Email", icon: Mail},
|
||||
{value: "slack", label: "Slack", icon: SlackIcon},
|
||||
{value: "discord", label: "Discord", icon: DiscordIcon},
|
||||
@@ -11,6 +13,7 @@ export const notificationTypes = [
|
||||
{value: "gotify", label: "Gotify", icon: GotifyIcon},
|
||||
{value: "ntfy", label: "ntfy.sh", icon: NtfyIcon},
|
||||
{value: "webhook", label: "Webhook", icon: WebhookIcon},
|
||||
{value: "microsoft-teams", label: "Microsoft Teams", icon: MSTeamsIcon, preview: true}
|
||||
]
|
||||
|
||||
|
||||
@@ -481,3 +484,49 @@ export function WebhookIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function MSTeamsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (<svg xmlns="http://www.w3.org/2000/svg" width={256} height={256} viewBox="0 0 256 239" {...props}>
|
||||
<defs>
|
||||
<path id="logosMicrosoftTeams0"
|
||||
d="M136.93 64.476v12.8a32.674 32.674 0 0 1-5.953-.952a38.698 38.698 0 0 1-26.79-22.742h21.848c6.008.022 10.872 4.887 10.895 10.894Z"
|
||||
opacity=".2"/>
|
||||
</defs>
|
||||
<defs>
|
||||
<linearGradient id="logosMicrosoftTeams1" x1="17.372%" x2="82.628%" y1="-6.51%" y2="106.51%">
|
||||
<stop offset="0%" stop-color="#5A62C3"/>
|
||||
<stop offset="50%" stop-color="#4D55BD"/>
|
||||
<stop offset="100%" stop-color="#3940AB"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="#5059C9"
|
||||
d="M178.563 89.302h66.125c6.248 0 11.312 5.065 11.312 11.312v60.231c0 22.96-18.613 41.574-41.573 41.574h-.197c-22.96.003-41.576-18.607-41.579-41.568V95.215a5.912 5.912 0 0 1 5.912-5.913Z"/>
|
||||
<circle cx="223.256" cy="50.605" r="26.791" fill="#5059C9"/>
|
||||
<circle cx="139.907" cy="38.698" r="38.698" fill="#7B83EB"/>
|
||||
<path fill="#7B83EB"
|
||||
d="M191.506 89.302H82.355c-6.173.153-11.056 5.276-10.913 11.449v68.697c-.862 37.044 28.445 67.785 65.488 68.692c37.043-.907 66.35-31.648 65.489-68.692v-68.697c.143-6.173-4.74-11.296-10.913-11.449Z"/>
|
||||
<path
|
||||
d="M142.884 89.302v96.268a10.96 10.96 0 0 1-6.787 10.062c-1.3.55-2.697.833-4.108.833H76.68c-.774-1.965-1.488-3.93-2.084-5.953a72.509 72.509 0 0 1-3.155-21.076v-68.703c-.143-6.163 4.732-11.278 10.895-11.43h60.547Z"
|
||||
opacity=".1"/>
|
||||
<path
|
||||
d="M136.93 89.302v102.222c0 1.411-.283 2.808-.833 4.108a10.96 10.96 0 0 1-10.062 6.787H79.48c-1.012-1.965-1.965-3.93-2.798-5.954a59.049 59.049 0 0 1-2.084-5.953a72.508 72.508 0 0 1-3.155-21.076v-68.703c-.143-6.163 4.732-11.278 10.895-11.43h54.593Z"
|
||||
opacity=".2"/>
|
||||
<path
|
||||
d="M136.93 89.302v90.315c-.045 5.998-4.896 10.85-10.895 10.895H74.597a72.508 72.508 0 0 1-3.155-21.076v-68.703c-.143-6.163 4.732-11.278 10.895-11.43h54.593Z"
|
||||
opacity=".2"/>
|
||||
<path
|
||||
d="M130.977 89.302v90.315c-.046 5.998-4.897 10.85-10.895 10.895H74.597a72.508 72.508 0 0 1-3.155-21.076v-68.703c-.143-6.163 4.732-11.278 10.895-11.43h48.64Z"
|
||||
opacity=".2"/>
|
||||
<path
|
||||
d="M142.884 58.523v18.753c-1.012.06-1.965.12-2.977.12c-1.012 0-1.965-.06-2.977-.12a32.674 32.674 0 0 1-5.953-.952a38.698 38.698 0 0 1-26.791-22.742a33.082 33.082 0 0 1-1.905-5.954h29.708c6.007.023 10.872 4.887 10.895 10.895Z"
|
||||
opacity=".1"/>
|
||||
<use href="#logosMicrosoftTeams0" opacity=".2"/>
|
||||
<use href="#logosMicrosoftTeams0" opacity=".2"/>
|
||||
<path d="M130.977 64.476v11.848a38.698 38.698 0 0 1-26.791-22.743h15.896c6.008.023 10.872 4.888 10.895 10.895Z"
|
||||
opacity=".2"/>
|
||||
<path fill="url(#logosMicrosoftTeams1)"
|
||||
d="M10.913 53.581h109.15c6.028 0 10.914 4.886 10.914 10.913v109.151c0 6.027-4.886 10.913-10.913 10.913H10.913C4.886 184.558 0 179.672 0 173.645V64.495C0 58.466 4.886 53.58 10.913 53.58Z"/>
|
||||
<path fill="#FFF" d="M94.208 95.125h-21.82v59.416H58.487V95.125H36.769V83.599h57.439z"/>
|
||||
</svg>)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import {Server} from "lucide-react";
|
||||
import type {SVGProps} from "react";
|
||||
import {ProviderIconTypes} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||
|
||||
export const storageTypes = [
|
||||
export const storageProviders: ProviderIconTypes[] = [
|
||||
{value: "local", label: "Local", icon: Server},
|
||||
{value: "s3", label: "s3", icon: S3Icon},
|
||||
{value: "s3", label: "S3", icon: S3Icon},
|
||||
{value: "google-drive", label: "Google Drive", icon: GoogleDriveIcon},
|
||||
{value: "blob", label: "Azure Blob Storage", icon: BlobIcon, preview: true},
|
||||
{value: "gcs", label: "Google Cloud Storage", icon: GCSIcon, preview: true},
|
||||
]
|
||||
|
||||
export function S3Icon(props: SVGProps<SVGSVGElement>) {
|
||||
@@ -15,3 +19,58 @@ export function S3Icon(props: SVGProps<SVGSVGElement>) {
|
||||
);
|
||||
}
|
||||
|
||||
export function GoogleDriveIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={256} height={199} {...props} viewBox="0 0 256 229"><path fill="#0066da" d="m19.354 196.034l11.29 19.5c2.346 4.106 5.718 7.332 9.677 9.678q17.009-21.591 23.68-33.137q6.77-11.717 16.641-36.655q-26.604-3.502-40.32-3.502q-13.165 0-40.322 3.502c0 4.545 1.173 9.09 3.519 13.196z"/><path fill="#ea4335" d="M215.681 225.212c3.96-2.346 7.332-5.572 9.677-9.677l4.692-8.064l22.434-38.855a26.57 26.57 0 0 0 3.518-13.196q-27.315-3.502-40.247-3.502q-13.899 0-40.248 3.502q9.754 25.075 16.422 36.655q6.724 11.683 23.752 33.137"/><path fill="#00832d" d="M128.001 73.311q19.68-23.768 27.125-36.655q5.996-10.377 13.196-33.137C164.363 1.173 159.818 0 155.126 0h-54.25C96.184 0 91.64 1.32 87.68 3.519q9.16 26.103 15.544 37.154q7.056 12.213 24.777 32.638"/><path fill="#2684fc" d="M175.36 155.42H80.642l-40.32 69.792c3.958 2.346 8.503 3.519 13.195 3.519h148.968c4.692 0 9.238-1.32 13.196-3.52z"/><path fill="#00ac47" d="M128.001 73.311L87.681 3.52c-3.96 2.346-7.332 5.571-9.678 9.677L3.519 142.224A26.57 26.57 0 0 0 0 155.42h80.642z"/><path fill="#ffba00" d="m215.242 77.71l-37.243-64.514c-2.345-4.106-5.718-7.331-9.677-9.677l-40.32 69.792l47.358 82.109h80.496c0-4.546-1.173-9.09-3.519-13.196z"/></svg>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function BlobIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width={256} height={199} {...props} viewBox="0 0 48 48">
|
||||
<path fill="#2979ff"
|
||||
d="M32.66,7H15.34c-0.715,0-1.375,0.381-1.732,1l-8.66,15c-0.357,0.619-0.357,1.381,0,2l8.66,15c0.357,0.619,1.018,1,1.732,1H32.66c0.715,0,1.375-0.381,1.732-1l8.66-15c0.357-0.619,0.357-1.381,0-2l-8.66-15C34.035,7.381,33.375,7,32.66,7z"></path>
|
||||
<path fill="#fff"
|
||||
d="M32,35H16c-0.552,0-1-0.448-1-1V14c0-0.552,0.448-1,1-1h13l4,4v17C33,34.552,32.552,35,32,35z"></path>
|
||||
<path fill="#b1cfff" d="M29,13v3c0,0.552,0.448,1,1,1h3L29,13z"></path>
|
||||
<path fill="#03a9f4"
|
||||
d="M22.265 23.831H20.93V19.58l-1.312.383v-1.011l2.524-.873h.122V23.831zM28.702 21.48c0 .416-.049.776-.146 1.081-.097.303-.234.556-.411.756-.176.2-.387.349-.632.446-.245.097-.515.146-.81.146-.298 0-.57-.049-.816-.146-.246-.097-.457-.246-.633-.446-.176-.2-.315-.453-.413-.756-.099-.305-.147-.665-.147-1.081v-1.051c0-.413.049-.773.146-1.078.097-.305.234-.558.411-.758.176-.2.387-.349.632-.446C26.126 18.049 26.397 18 26.695 18c.295 0 .565.049.811.146.246.097.458.246.635.446.176.2.313.453.412.758.099.305.149.665.149 1.078V21.48zM27.37 20.26c0-.229-.016-.421-.047-.579-.032-.155-.076-.283-.134-.381-.058-.097-.129-.167-.212-.209-.083-.042-.176-.063-.282-.063-.105 0-.2.021-.283.063-.083.042-.153.112-.209.209-.057.097-.1.225-.13.381-.03.158-.045.35-.045.579v1.383c0 .234.014.431.045.589.03.158.075.286.133.381.058.097.129.166.213.208.084.042.179.063.284.063.103 0 .195-.021.278-.063.083-.042.154-.111.212-.208.058-.095.101-.223.132-.381.03-.158.046-.354.046-.589V20.26zM23.308 29.571c0 .416-.049.776-.146 1.081-.097.303-.234.556-.411.756-.176.2-.387.349-.632.446C21.874 31.951 21.604 32 21.309 32c-.298 0-.57-.049-.816-.146-.246-.097-.457-.246-.633-.446-.176-.2-.315-.453-.413-.756-.099-.305-.147-.665-.147-1.081V28.52c0-.413.049-.773.146-1.078.097-.305.234-.558.411-.758.176-.2.387-.349.632-.446.245-.097.516-.146.814-.146.295 0 .565.049.811.146.246.097.458.246.635.446.176.2.313.453.412.758s.149.665.149 1.078V29.571zM21.977 28.35c0-.229-.016-.421-.047-.579-.032-.155-.076-.283-.134-.381-.058-.097-.129-.167-.212-.209-.083-.042-.176-.063-.282-.063s-.2.021-.283.063c-.083.042-.153.112-.209.209-.057.097-.1.225-.13.381-.03.158-.045.35-.045.579v1.383c0 .234.014.431.045.589.03.158.075.286.133.381.058.097.129.166.213.208s.179.063.284.063c.103 0 .195-.021.278-.063.083-.042.154-.111.212-.208.058-.095.101-.223.132-.381.03-.158.046-.354.046-.589V28.35zM27.659 31.921h-1.335V27.67l-1.312.383v-1.011l2.524-.873h.122V31.921z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function GCSIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
id="standard_product_icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
{...props}
|
||||
>
|
||||
<g id="bounding_box">
|
||||
<rect width="512" height="512" fill="none"/>
|
||||
</g>
|
||||
<g id="art">
|
||||
<path
|
||||
fill="#34a853"
|
||||
d="M442,277.9H70c-8.8,0-16,7.2-16,16v148.1c0,8.8,7.2,16,16,16h107.2c1.5.5,3.1.7,4.8.7s3.3-.3,4.8-.7h255.2c8.8,0,16-7.2,16-16v-148.1c0-8.8-7.2-16-16-16ZM86,309.9h80v116.1h-80v-116.1ZM426,425.9h-228v-116.1h228v116.1Z"
|
||||
/>
|
||||
<path
|
||||
fill="#fbbc04"
|
||||
d="M442,54H70c-8.8,0-16,7.2-16,16v148.8c0,8.8,7.2,16,16,16h372c8.8,0,16-7.2,16-16V70c0-8.8-7.2-16-16-16ZM86,86h80v116.8h-80v-116.8ZM426,202.8h-228v-116.8h228v116.8Z"
|
||||
/>
|
||||
<path
|
||||
fill="#ea4335"
|
||||
d="M442,234.8h-16V86H54v-16c0-8.8,7.2-16,16-16h372c8.8,0,16,7.2,16,16v148.8c0,8.8-7.2,16-16,16Z"
|
||||
/>
|
||||
<path
|
||||
fill="#4285f4"
|
||||
d="M442,457.9h-16v-148.1H54v-16c0-8.8,7.2-16,16-16h372c8.8,0,16,7.2,16,16v148.1c0,8.8-7.2,16-16,16Z"
|
||||
/>
|
||||
<circle fill="#ea4335" cx="349" cy="144.4" r="37"/>
|
||||
<circle fill="#4285f4" cx="349" cy="367.9" r="37"/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@ import {Trash2} from "lucide-react";
|
||||
import {toast} from "sonner";
|
||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {deleteOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import {deleteOrganizationAction} from "@/features/organization/organization.action";
|
||||
|
||||
export type ButtonDeleteFleetProps = {
|
||||
text?: string;
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import {Input} from "@/components/ui/input";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {toast} from "sonner";
|
||||
import {OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
import {updateOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import {updateOrganizationAction} from "@/features/organization/organization.action";
|
||||
|
||||
type UpdateOrganizationFormProps = {
|
||||
onSuccessAction?: () => void;
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from "@/components/wrappers/dashboard/admin/settings/email/email-form/email-form.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {sendEmail} from "@/lib/email/email-helper";
|
||||
import {sendEmail} from "@/lib/email";
|
||||
import {render} from "@react-email/components";
|
||||
import EmailSettingsTest from "@/components/emails/email-settings-test";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
|
||||
@@ -46,14 +46,14 @@ export function usersListColumns(): ColumnDef<User>[] {
|
||||
<TableCell
|
||||
className="text-right text-muted-foreground">{formatLocalizedDate(row.original.createdAt)}</TableCell>
|
||||
</TableRow>
|
||||
{/*{row.original.lastConnectedAt && (*/}
|
||||
{/* <TableRow>*/}
|
||||
{/* <TableCell>Last connected</TableCell>*/}
|
||||
{/* <TableCell className="text-right text-muted-foreground">*/}
|
||||
{/* {formatLocalizedDate(row.original.lastConnectedAt)} ({timeAgo(row.original.lastConnectedAt, locale)})*/}
|
||||
{/* </TableCell>*/}
|
||||
{/* </TableRow>*/}
|
||||
{/*)}*/}
|
||||
{row.original.lastConnectedAt && (
|
||||
<TableRow>
|
||||
<TableCell>Last connected</TableCell>
|
||||
<TableCell className="text-right text-muted-foreground">
|
||||
{formatLocalizedDate(row.original.lastConnectedAt)} ({timeAgo(row.original.lastConnectedAt)})
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{row.original.lastChangedPasswordAt && (
|
||||
<TableRow>
|
||||
<TableCell>Last password change</TableCell>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {
|
||||
addMemberOrganizationAction
|
||||
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/add-member.action";
|
||||
import {sendEmail} from "@/lib/email/email-helper";
|
||||
import {sendEmail} from "@/lib/email";
|
||||
import EmailCreateUser from "@/components/emails/email-create-user";
|
||||
import {SignUpUser} from "@/types/auth";
|
||||
import {createUserDb} from "@/db/services/user";
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import {useState} from "react";
|
||||
import Link from "next/link";
|
||||
import {Card} from "@/components/ui/card";
|
||||
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
|
||||
import {ConnectionIndicator} from "@/components/wrappers/common/connection-indicator";
|
||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||
import {AgentWith} from "@/db/schema/08_agent";
|
||||
import {Activity, ChevronRight, Copy, Check, Fingerprint, Server, Database, ShieldCheck} from "lucide-react";
|
||||
import {Activity, ChevronRight, Copy, Check, Fingerprint, Server, Database} from "lucide-react";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {truncateWords} from "@/utils/text";
|
||||
import {useIsMobile} from "@/hooks/use-mobile";
|
||||
@@ -86,7 +86,7 @@ export const AgentCard = (props: agentCardProps) => {
|
||||
</div>
|
||||
|
||||
<div className="scale-110">
|
||||
<ConnectionCircle date={agent.lastContact}/>
|
||||
<ConnectionIndicator date={agent.lastContact}/>
|
||||
</div>
|
||||
|
||||
<ChevronRight className="w-5 h-5 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all" />
|
||||
|
||||
@@ -26,7 +26,7 @@ type ProfileModalProps = {
|
||||
export const ProfileModal = ({ user, sessions, currentSession, accounts, open, onOpenChange, providers }: ProfileModalProps) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-[95vw] h-[90vh] max-w-md lg:max-w-[1000px] lg:h-[800px] p-0 overflow-hidden flex flex-col outline-none gap-0 rounded-xl bg-background">
|
||||
<DialogContent className="w-[95vw] h-[90vh] max-w-md lg:max-w-[1000px] lg:h-[800px] pb-6 overflow-hidden flex flex-col outline-none gap-0 rounded-xl bg-background">
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>Settings</DialogTitle>
|
||||
<DialogDescription>Manage your account settings</DialogDescription>
|
||||
|
||||
@@ -51,6 +51,10 @@ export const downloadBackupAction = userAction.schema(
|
||||
path: backupStorage.path,
|
||||
signedUrl: true,
|
||||
},
|
||||
metadata: {
|
||||
storageId: backupStorage.storageChannelId,
|
||||
fileKind: "backups"
|
||||
}
|
||||
};
|
||||
|
||||
console.log(input)
|
||||
|
||||
@@ -6,8 +6,8 @@ import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle} from "@/
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema";
|
||||
import {createOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import {CreateOrganizationSchema, CreateOrganizationType} from "@/features/organization/organization.schema";
|
||||
import {createOrganizationAction} from "@/features/organization/organization.action";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useState} from "react";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
@@ -32,12 +32,12 @@ export function CreateOrganizationModal({
|
||||
const router = useRouter();
|
||||
|
||||
const form = useZodForm({
|
||||
schema: OrganizationSchema,
|
||||
schema: CreateOrganizationSchema,
|
||||
});
|
||||
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: OrganizationSchema) => {
|
||||
mutationFn: async (values: CreateOrganizationType) => {
|
||||
|
||||
const result = await createOrganizationAction(values);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import {deleteOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import {deleteOrganizationAction} from "@/features/organization/organization.action";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {toast} from "sonner";
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
"use client"
|
||||
import {ComboBox} from "@/components/wrappers/common/combobox";
|
||||
import {useSidebar} from "@/components/ui/sidebar";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {CreateOrganizationModal} from "@/components/wrappers/dashboard/organization/create-organisation-modal";
|
||||
import {useState} from "react";
|
||||
|
||||
import * as React from "react"
|
||||
import { Building2, Check, ChevronsUpDown, Plus } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { authClient } from "@/lib/auth/auth-client"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {
|
||||
SidebarMenuButton,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
import { CreateOrganizationModal } from "@/components/wrappers/dashboard/organization/create-organisation-modal"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
|
||||
export function OrganizationCombobox() {
|
||||
const router = useRouter();
|
||||
const {state} = useSidebar();
|
||||
const {data: organizations, refetch} = authClient.useListOrganizations();
|
||||
const {data: activeOrganization, refetch: refetchActiveOrga} = authClient.useActiveOrganization();
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const router = useRouter()
|
||||
const { isMobile, state } = useSidebar()
|
||||
const { data: organizations, isPending: isPendingList, refetch } = authClient.useListOrganizations()
|
||||
const { data: activeOrganization, isPending: isPendingActive, refetch: refetchActiveOrga } = authClient.useActiveOrganization()
|
||||
const [openModal, setOpenModal] = React.useState(false)
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
|
||||
// if (!organizations) return null;
|
||||
|
||||
// const values = organizations.map(org => ({ value: org.slug, label: org.name }));
|
||||
const values = organizations?.map(org => ({ value: org.slug, label: org.name })) ?? [];
|
||||
|
||||
|
||||
|
||||
const onValueChange = async (slug: string) => {
|
||||
await authClient.organization.setActive({ organizationSlug: slug });
|
||||
router.refresh();
|
||||
};
|
||||
const handleSelect = async (slug: string) => {
|
||||
if (activeOrganization?.slug === slug) return
|
||||
await authClient.organization.setActive({ organizationSlug: slug })
|
||||
router.refresh()
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
const handleReload = () => {
|
||||
refetch();
|
||||
@@ -31,23 +41,116 @@ export function OrganizationCombobox() {
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateOrganizationModal
|
||||
open={openModal}
|
||||
onSuccess={handleReload}
|
||||
onOpenChange={setOpenModal}
|
||||
/>
|
||||
{state === "expanded" && (
|
||||
<ComboBox
|
||||
sideBar
|
||||
values={values}
|
||||
defaultValue={activeOrganization?.slug}
|
||||
onValueChangeAction={onValueChange}
|
||||
onAddItemAction={() => setOpenModal(true)}
|
||||
addItemLabel="Create new organization"
|
||||
const orgs = organizations || []
|
||||
|
||||
if (isPendingList || isPendingActive) {
|
||||
return (
|
||||
<SidebarMenuButton size="lg" className="pointer-events-none">
|
||||
<Skeleton className="size-8 rounded-lg shrink-0" />
|
||||
<div className="flex flex-col gap-1.5 flex-1 overflow-hidden">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-3 w-16" />
|
||||
</div>
|
||||
<Skeleton className="size-4 shrink-0" />
|
||||
</SidebarMenuButton>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateOrganizationModal
|
||||
open={openModal}
|
||||
onSuccess={handleReload}
|
||||
onOpenChange={setOpenModal}
|
||||
/>
|
||||
)}
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className={cn(
|
||||
"data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground transition-all duration-200 ease-in-out",
|
||||
state === "collapsed" && "justify-center p-0"
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"flex aspect-square size-8 items-center justify-center rounded-lg text-white shadow-sm transition-transform duration-200",
|
||||
activeOrganization?.logo ? "bg-transparent" : "bg-orange-500",
|
||||
isOpen && "scale-105"
|
||||
)}>
|
||||
{activeOrganization?.logo ? (
|
||||
<img src={activeOrganization.logo} alt={activeOrganization.name} className="size-8 rounded-lg object-cover" />
|
||||
) : (
|
||||
<Building2 className="size-5" />
|
||||
)}
|
||||
</div>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
|
||||
<span className="truncate font-semibold tracking-tight">
|
||||
{activeOrganization?.name || "Select Organization"}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4 opacity-50 group-data-[collapsible=icon]:hidden" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-64 rounded-xl border-2 border-border bg-popover shadow-none"
|
||||
align="start"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
sideOffset={4}
|
||||
>
|
||||
{orgs.map((org) => {
|
||||
const isActive = activeOrganization?.id === org.id;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={org.id}
|
||||
onClick={() => handleSelect(org.slug)}
|
||||
className={cn(
|
||||
"group gap-1 p-1 cursor-pointer rounded-lg mb-1 last:mb-0 transition-colors",
|
||||
isActive
|
||||
? "bg-orange-500/10 text-orange-600 dark:text-orange-400 border border-orange-500/20"
|
||||
: "focus:bg-accent hover:bg-accent/50 border border-transparent"
|
||||
)}>
|
||||
<div className={cn(
|
||||
"flex size-9 items-center justify-center rounded-md border shadow-sm transition-all group-hover:shadow-md",
|
||||
org.logo ? "bg-transparent border-transparent" : "",
|
||||
isActive && !org.logo ? "bg-orange-500 text-white border-orange-500/30" : "bg-muted/50 border-border"
|
||||
)}>
|
||||
{org.logo ? (
|
||||
<img src={org.logo} alt={org.name} className="size-9 rounded-md object-cover" />
|
||||
) : (
|
||||
<Building2 className={cn(
|
||||
"size-5",
|
||||
isActive ? "text-white" : "text-muted-foreground"
|
||||
)} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className={cn(
|
||||
"text-sm max-w-42.5 truncate font-medium leading-none",
|
||||
isActive ? "text-orange-600 dark:text-orange-400" : ""
|
||||
)}>{org.name}</span>
|
||||
</div>
|
||||
{isActive && (
|
||||
<div className="ml-auto flex size-5 items-center justify-center rounded-full bg-orange-500 shadow-sm">
|
||||
<Check className="size-3 text-white" strokeWidth={3} />
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
<DropdownMenuSeparator className="my-2 bg-border/50" />
|
||||
<DropdownMenuItem
|
||||
className="gap-3 p-2.5 cursor-pointer text-muted-foreground focus:text-foreground group rounded-lg"
|
||||
onClick={() => setOpenModal(true)}
|
||||
>
|
||||
<div className="flex size-9 items-center justify-center rounded-md border border-dashed border-muted-foreground/30 bg-background transition-colors group-hover:border-primary/50 group-hover:bg-primary/5">
|
||||
<Plus className="size-4" />
|
||||
</div>
|
||||
<div className="font-medium">Create new organization</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import {z} from "zod";
|
||||
|
||||
|
||||
export const OrganizationFormSchema = z.object({
|
||||
name: z.string().min(5, 'Name must be at least 5 characters long').max(40, 'Name must be at most 40 characters long'),
|
||||
slug: z.string()
|
||||
.regex(/^[a-zA-Z0-9_-]*$/, 'Slug can only contain letters, numbers, underscores, and hyphens')
|
||||
.min(5, 'Slug must be at least 5 characters long')
|
||||
.max(20, 'Slug must be at most 20 characters long'),
|
||||
users: z.array(z.string()),
|
||||
|
||||
});
|
||||
|
||||
export type OrganizationFormType = z.infer<typeof OrganizationFormSchema>;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const OrganizationSchema = z.object({
|
||||
name: z.string().min(5, "Name must be at least 5 characters long").max(40, "Name must be at most 40 characters long"),
|
||||
// slug: z
|
||||
// .string()
|
||||
// .regex(/^[a-zA-Z0-9_-]*$/, "Slug can only contain letters, numbers, underscores, and hyphens")
|
||||
// .min(5, "Slug must be at least 5 characters long")
|
||||
// .max(20, "Slug must be at most 20 characters long"),
|
||||
});
|
||||
|
||||
export type OrganizationSchema = z.infer<typeof OrganizationSchema>;
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
"use client"
|
||||
import {buttonVariants} from "@/components/ui/button";
|
||||
import {GearIcon} from "@radix-ui/react-icons";
|
||||
import Link from "next/link";
|
||||
import {usePathname} from "next/navigation";
|
||||
|
||||
export type EditButtonSettings = {}
|
||||
|
||||
export const EditButtonSettings= (props:EditButtonSettings) => {
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
return(
|
||||
<Link className={buttonVariants({variant: "outline"})}
|
||||
href={`${pathname}/edit/`}
|
||||
>
|
||||
<GearIcon className="w-7 h-7"/>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -48,14 +48,25 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
||||
const handleImageUpload = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
if (!file.type.includes("image")) {
|
||||
toast.error("File not an image");
|
||||
|
||||
const allowedFormats = ["image/jpeg", "image/png", "image/webp"];
|
||||
|
||||
if (!allowedFormats.includes(file.type)) {
|
||||
toast.error("Only JPG, PNG or WebP images are allowed.");
|
||||
return;
|
||||
}
|
||||
|
||||
const maxSizeInMB = 5;
|
||||
if (file.size > maxSizeInMB * 1024 * 1024) {
|
||||
toast.error(`Image must be smaller than ${maxSizeInMB}MB.`);
|
||||
return;
|
||||
}
|
||||
|
||||
submitImage.mutate(file);
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="relative ">
|
||||
|
||||
@@ -68,12 +79,12 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
||||
onClick={() => {
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "file";
|
||||
fileInput.accept = "image/*";
|
||||
fileInput.accept = ".jpg,.jpeg,.png,.webp";
|
||||
// @ts-ignore
|
||||
fileInput.onchange = handleImageUpload;
|
||||
fileInput.click();
|
||||
}}
|
||||
className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-100 hover:bg-gray-500 hover:bg-opacity-50 rounded-full w-24 h-24 lg:w-32 lg:h-32"
|
||||
className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-30 hover:bg-gray-500 hover:bg-opacity-50 rounded-full w-24 h-24 lg:w-32 lg:h-32"
|
||||
>
|
||||
<UploadIcon className="w-12 h-12 lg:w-16 lg:h-16 text-primary"/>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import {useState} from "react";
|
||||
import {Card} from "@/components/ui/card";
|
||||
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
|
||||
import {ConnectionIndicator} from "@/components/wrappers/common/connection-indicator";
|
||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||
import {Database} from "@/db/schema/07_database";
|
||||
import {ChevronRight, Activity, Fingerprint, Copy, Check} from "lucide-react";
|
||||
@@ -60,7 +60,7 @@ export const DatabaseCard = (props: databaseCardProps) => {
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-3">
|
||||
<div className="scale-100 origin-right">
|
||||
<ConnectionCircle date={database.lastContact}/>
|
||||
<ConnectionIndicator date={database.lastContact}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "user" ADD COLUMN "lastConnectedAt" timestamp;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "user" ADD COLUMN "lastConnectedAt" timestamp;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TYPE "public"."dbms_status" ADD VALUE 'mongodb';
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "settings" ADD COLUMN "smtp_secure" boolean;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TYPE "public"."provider_storage_kind" ADD VALUE 'google-drive';
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -204,6 +204,34 @@
|
||||
"when": 1768733221276,
|
||||
"tag": "0028_graceful_ben_parker",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 29,
|
||||
"version": "7",
|
||||
"when": 1768743644834,
|
||||
"tag": "0029_lowly_white_tiger",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 30,
|
||||
"version": "7",
|
||||
"when": 1768763883339,
|
||||
"tag": "0030_dizzy_morlocks",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 31,
|
||||
"version": "7",
|
||||
"when": 1768931603846,
|
||||
"tag": "0031_chemical_edwin_jarvis",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 32,
|
||||
"version": "7",
|
||||
"when": 1769203863305,
|
||||
"tag": "0032_sparkling_thunderbolt_ross",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {pgTable, timestamp, uuid, varchar} from "drizzle-orm/pg-core";
|
||||
import {boolean, pgTable, timestamp, uuid, varchar} from "drizzle-orm/pg-core";
|
||||
import {typeStorageEnum} from "./types";
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
import {z} from "zod";
|
||||
@@ -19,6 +19,7 @@ export const setting = pgTable("settings", {
|
||||
smtpHost: varchar("smtp_host", {length: 255}),
|
||||
smtpPort: varchar("smtp_port", {length: 255}),
|
||||
smtpUser: varchar("smtp_user", {length: 255}),
|
||||
smtpSecure: boolean("smtp_secure"),
|
||||
defaultStorageChannelId: uuid('default_storage_channel_id')
|
||||
.references(() => storageChannel.id, {onDelete: "set null"}),
|
||||
...timestamps
|
||||
|
||||
@@ -3,7 +3,7 @@ import {boolean, integer, pgEnum, pgTable, text, timestamp, uuid} from "drizzle-
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
import {z} from "zod";
|
||||
import {project} from "./06_project";
|
||||
import {member, OrganizationMember} from "@/db/schema/04_member";
|
||||
import {member} from "@/db/schema/04_member";
|
||||
import {invitation} from "@/db/schema/05_invitation";
|
||||
import {organization} from "@/db/schema/03_organization";
|
||||
import {Account as BetterAuthAccount} from "better-auth";
|
||||
@@ -23,6 +23,7 @@ export const user = pgTable("user", {
|
||||
banned: boolean("banned"),
|
||||
banReason: text("ban_reason"),
|
||||
banExpires: timestamp("ban_expires"),
|
||||
lastConnectedAt: timestamp(),
|
||||
lastChangedPasswordAt: timestamp(),
|
||||
twoFactorEnabled: boolean("two_factor_enabled").default(false),
|
||||
...timestamps
|
||||
|
||||
@@ -7,7 +7,7 @@ import {z} from "zod";
|
||||
import {database} from "@/db/schema/07_database";
|
||||
|
||||
|
||||
export const providerStorageKindEnum = pgEnum('provider_storage_kind', ['local', 's3']);
|
||||
export const providerStorageKindEnum = pgEnum('provider_storage_kind', ['local', 's3', 'google-drive']);
|
||||
|
||||
export const storageChannel = pgTable('storage_channel', {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { pgEnum } from "drizzle-orm/pg-core";
|
||||
import { createSelectSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import {pgEnum} from "drizzle-orm/pg-core";
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
import {z} from "zod";
|
||||
|
||||
export const dbmsEnum = pgEnum("dbms_status", ["postgresql", "mysql"]);
|
||||
export const dbmsEnum = pgEnum("dbms_status", ["postgresql", "mysql", "mongodb"]);
|
||||
export const statusEnum = pgEnum("status", ["waiting", "ongoing", "failed", "success"]);
|
||||
export const typeStorageEnum = pgEnum("type_storage", ["local", "s3"]);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ export const env = createEnv({
|
||||
SMTP_HOST: z.string().optional(),
|
||||
SMTP_PORT: z.string().optional(),
|
||||
SMTP_USER: z.string().optional(),
|
||||
SMTP_SECURE: z.coerce.boolean().default(true),
|
||||
|
||||
AUTH_GOOGLE_ID: z.string().optional(),
|
||||
AUTH_GOOGLE_SECRET: z.string().optional(),
|
||||
@@ -60,6 +61,7 @@ export const env = createEnv({
|
||||
SMTP_HOST: process.env.SMTP_HOST,
|
||||
SMTP_PORT: process.env.SMTP_PORT,
|
||||
SMTP_USER: process.env.SMTP_USER,
|
||||
SMTP_SECURE: process.env.SMTP_SECURE,
|
||||
|
||||
AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID,
|
||||
AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
import {ActionError, userAction} from "@/lib/safe-actions/actions";
|
||||
import {AgentSchema} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.schema";
|
||||
import {AgentSchema} from "@/features/agents/agents.schema";
|
||||
import {z} from "zod";
|
||||
import {eq, and, ne, count} from "drizzle-orm";
|
||||
import {db} from "@/db";
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const AgentSchema = z.object({
|
||||
name: z.string(),
|
||||
name: z.string().nonempty("Name is required"),
|
||||
description: z.string(),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {AgentForm} from "@/features/agents/components/agent.form";
|
||||
import {useState} from "react";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Plus} from "lucide-react";
|
||||
import {AgentType} from "@/features/agents/agents.schema";
|
||||
|
||||
type AgentDialogProps = {
|
||||
children?: React.ReactNode;
|
||||
agent?: AgentType & { id: string };
|
||||
};
|
||||
|
||||
export const AgentDialog = ({children, agent}: AgentDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const isEdit = !!agent;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{children ? children : <Button><Plus className="mr-2 h-4 w-4"/> Create Agent</Button>}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEdit ? `Edit ${agent.name}` : "Create new agent"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<AgentForm
|
||||
onSuccess={() => setOpen(false)}
|
||||
defaultValues={agent}
|
||||
agentId={agent?.id}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
+9
-4
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardContent} from "@/components/ui/card";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
@@ -16,13 +15,14 @@ import {Button} from "@/components/ui/button";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import {AgentSchema, AgentType} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.schema";
|
||||
import {AgentSchema, AgentType} from "@/features/agents/agents.schema";
|
||||
import {toast} from "sonner";
|
||||
import {createAgentAction, updateAgentAction} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.action";
|
||||
import {createAgentAction, updateAgentAction} from "@/features/agents/agents.action";
|
||||
|
||||
export type agentFormProps = {
|
||||
defaultValues?: AgentType;
|
||||
agentId?: string;
|
||||
onSuccess?: (data: any) => void;
|
||||
};
|
||||
|
||||
export const AgentForm = (props: agentFormProps) => {
|
||||
@@ -52,7 +52,12 @@ export const AgentForm = (props: agentFormProps) => {
|
||||
}
|
||||
toast.success(`Success ${isCreate ? "creating" : "updating"} agent`);
|
||||
router.refresh();
|
||||
router.push(`/dashboard/agents/${data.id}`);
|
||||
|
||||
if (props.onSuccess) {
|
||||
props.onSuccess(data);
|
||||
} else {
|
||||
router.push(`/dashboard/agents/${data.id}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type {EventPayload, DispatchResult} from '../types';
|
||||
|
||||
export async function sendNtfy(
|
||||
config: { ntfyServerUrl?: string; ntfyTopic: string; ntfyToken?: string },
|
||||
config: { ntfyServerUrl?: string; ntfyTopic: string; ntfyToken?: string, ntfyUsername?: string, ntfyPassword?: string },
|
||||
payload: EventPayload
|
||||
): Promise<DispatchResult> {
|
||||
const {ntfyServerUrl, ntfyTopic, ntfyToken} = config;
|
||||
const {ntfyServerUrl, ntfyTopic, ntfyToken, ntfyUsername, ntfyPassword} = config;
|
||||
|
||||
const baseUrl = (ntfyServerUrl || "https://ntfy.sh").replace(/\/$/, "");
|
||||
|
||||
@@ -24,6 +24,11 @@ export async function sendNtfy(
|
||||
headers['Authorization'] = `Bearer ${ntfyToken}`;
|
||||
}
|
||||
|
||||
if (ntfyUsername && ntfyPassword) {
|
||||
const credentials = btoa(`${ntfyUsername}:${ntfyPassword}`);
|
||||
headers['Authorization'] = `Basic ${credentials}`;
|
||||
}
|
||||
|
||||
const res = await fetch(`${baseUrl}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {OrganizationForm} from "@/features/organization/components/organization.form";
|
||||
import {useState} from "react";
|
||||
import {Button, buttonVariants} from "@/components/ui/button";
|
||||
import {GearIcon} from "@radix-ui/react-icons";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
import {User as BetterAuthUser} from "better-auth";
|
||||
|
||||
type EditOrganizationDialogProps = {
|
||||
children?: React.ReactNode;
|
||||
organization: OrganizationWithMembers;
|
||||
users: User[];
|
||||
currentUser: BetterAuthUser;
|
||||
};
|
||||
|
||||
export const EditOrganizationDialog = ({children, organization, users, currentUser}: EditOrganizationDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{children ? children : (
|
||||
<Button variant="outline">
|
||||
<GearIcon className="w-4 h-4 mr-2"/> Edit
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit {organization.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<OrganizationForm
|
||||
onSuccess={() => setOpen(false)}
|
||||
defaultValues={organization}
|
||||
users={users}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
+12
-7
@@ -16,13 +16,13 @@ import {useMutation} from "@tanstack/react-query";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select";
|
||||
import {
|
||||
OrganizationFormSchema,
|
||||
OrganizationFormType
|
||||
} from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema";
|
||||
UpdateOrganizationSchema,
|
||||
UpdateOrganizationType
|
||||
} from "@/features/organization/organization.schema";
|
||||
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {
|
||||
updateOrganizationAction
|
||||
} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
} from "@/features/organization/organization.action";
|
||||
import {toast} from "sonner";
|
||||
import {User as BetterAuthUser} from "better-auth";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
@@ -32,6 +32,7 @@ export type organizationFormProps = {
|
||||
defaultValues?: OrganizationWithMembers;
|
||||
users: User[];
|
||||
currentUser: BetterAuthUser;
|
||||
onSuccess?: (data: any) => void;
|
||||
};
|
||||
|
||||
export const OrganizationForm = (props: organizationFormProps) => {
|
||||
@@ -64,12 +65,12 @@ export const OrganizationForm = (props: organizationFormProps) => {
|
||||
};
|
||||
|
||||
const form = useZodForm({
|
||||
schema: OrganizationFormSchema,
|
||||
schema: UpdateOrganizationSchema,
|
||||
defaultValues: formattedDefaultValues,
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (values: OrganizationFormType) => updateOrganizationAction({
|
||||
mutationFn: (values: UpdateOrganizationType) => updateOrganizationAction({
|
||||
data: values,
|
||||
organizationId: props.defaultValues?.id ?? ""
|
||||
}),
|
||||
@@ -78,7 +79,11 @@ export const OrganizationForm = (props: organizationFormProps) => {
|
||||
toast.success(result.data.actionSuccess?.message || "Organization updated successfully.");
|
||||
refetch()
|
||||
refetchActiveOrga()
|
||||
router.push("/dashboard/settings");
|
||||
if (props.onSuccess) {
|
||||
props.onSuccess(result.data.value);
|
||||
} else {
|
||||
router.push("/dashboard/settings");
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore
|
||||
const errorMsg = result?.data?.actionError?.message || result?.data?.actionError?.messageParams?.message || "Failed to update the organization.";
|
||||
+3
-6
@@ -1,12 +1,9 @@
|
||||
"use server";
|
||||
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema";
|
||||
import {CreateOrganizationSchema, UpdateOrganizationSchema} from "@/features/organization/organization.schema";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {z} from "zod";
|
||||
import {
|
||||
OrganizationFormSchema
|
||||
} from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema";
|
||||
import {db} from "@/db";
|
||||
import {and, eq, inArray, or} from "drizzle-orm";
|
||||
import {auth, checkSlugOrganization, createOrganization} from "@/lib/auth/auth";
|
||||
@@ -14,7 +11,7 @@ import {slugify} from "@/utils/slugify";
|
||||
import {Organization} from "@/db/schema/03_organization";
|
||||
import * as drizzleDb from "@/db";
|
||||
|
||||
export const createOrganizationAction = userAction.schema(OrganizationSchema).action(async ({parsedInput}): Promise<ServerActionResult<Organization>> => {
|
||||
export const createOrganizationAction = userAction.schema(CreateOrganizationSchema).action(async ({parsedInput}): Promise<ServerActionResult<Organization>> => {
|
||||
try {
|
||||
const slug = slugify(parsedInput.name);
|
||||
if (!await checkSlugOrganization(slug)) {
|
||||
@@ -69,7 +66,7 @@ export const createOrganizationAction = userAction.schema(OrganizationSchema).ac
|
||||
export const updateOrganizationAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
data: OrganizationFormSchema,
|
||||
data: UpdateOrganizationSchema,
|
||||
organizationId: z.string(),
|
||||
})
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const CreateOrganizationSchema = z.object({
|
||||
name: z.string().min(5, "Name must be at least 5 characters long").max(40, "Name must be at most 40 characters long"),
|
||||
});
|
||||
|
||||
export const UpdateOrganizationSchema = z.object({
|
||||
name: z.string().min(5, 'Name must be at least 5 characters long').max(40, 'Name must be at most 40 characters long'),
|
||||
slug: z.string()
|
||||
.regex(/^[a-zA-Z0-9_-]*$/, 'Slug can only contain letters, numbers, underscores, and hyphens')
|
||||
.min(5, 'Slug must be at least 5 characters long')
|
||||
.max(20, 'Slug must be at most 20 characters long'),
|
||||
users: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type CreateOrganizationType = z.infer<typeof CreateOrganizationSchema>;
|
||||
export type UpdateOrganizationType = z.infer<typeof UpdateOrganizationSchema>;
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {ProjectForm} from "@/features/projects/components/project.form";
|
||||
import {useState} from "react";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Plus} from "lucide-react";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
import {Organization} from "@/db/schema/03_organization";
|
||||
import {ProjectWith} from "@/db/schema/06_project";
|
||||
|
||||
type ProjectDialogProps = {
|
||||
children?: React.ReactNode;
|
||||
databases: DatabaseWith[];
|
||||
organization: Organization;
|
||||
project?: ProjectWith;
|
||||
};
|
||||
|
||||
export const ProjectDialog = ({children, databases, organization, project}: ProjectDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const isEdit = !!project;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{children ? children : <Button><Plus className="mr-2 h-4 w-4"/> Create Project</Button>}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEdit ? `Edit ${project.name}` : "Create new project"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<ProjectForm
|
||||
onSuccess={() => setOpen(false)}
|
||||
databases={databases}
|
||||
organization={organization}
|
||||
defaultValues={project ? {
|
||||
...project,
|
||||
databases: project.databases.map(db => db.id)
|
||||
} : undefined}
|
||||
projectId={project?.id}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
+15
-5
@@ -1,13 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { ProjectSchema, ProjectType } from "@/components/wrappers/dashboard/projects/project-form/project-form.schema";
|
||||
import { createProjectAction, updateProjectAction } from "@/components/wrappers/dashboard/projects/project-form/project-form.action";
|
||||
import { ProjectSchema, ProjectType } from "@/features/projects/projects.schema";
|
||||
import { createProjectAction, updateProjectAction } from "@/features/projects/projects.action";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { MultiSelect } from "@/components/wrappers/common/multiselect/multi-select";
|
||||
import { toast } from "sonner";
|
||||
@@ -19,15 +18,22 @@ export type projectFormProps = {
|
||||
databases: DatabaseWith[];
|
||||
organization: Organization;
|
||||
projectId?: string;
|
||||
onSuccess?: (data: any) => void;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const ProjectForm = (props: projectFormProps) => {
|
||||
const router = useRouter();
|
||||
const isCreate = !Boolean(props.defaultValues);
|
||||
const formatDatabasesList = (databases: DatabaseWith[]) => {
|
||||
return databases.map((database) => ({
|
||||
value: database.id,
|
||||
label: `${database.name} (${database.agentDatabaseId}) | ${database.agent?.name}`,
|
||||
label: `${database.name} | ${database.agent?.name}`,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -65,7 +71,11 @@ export const ProjectForm = (props: projectFormProps) => {
|
||||
if (project.data.success) {
|
||||
project.data.actionSuccess && toast.success(project.data.actionSuccess.message);
|
||||
router.refresh();
|
||||
router.push(`/dashboard/projects/${project.data.value!.id}`);
|
||||
if (props.onSuccess) {
|
||||
props.onSuccess(project.data.value);
|
||||
} else {
|
||||
router.push(`/dashboard/projects/${project.data.value!.id}`);
|
||||
}
|
||||
} else {
|
||||
project.data.actionError && toast.error(project.data.actionError.message || "Unknown error occurred.");
|
||||
router.refresh();
|
||||
+1
-3
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {ProjectSchema} from "@/components/wrappers/dashboard/projects/project-form/project-form.schema";
|
||||
import {ProjectSchema} from "@/features/projects/projects.schema";
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {db} from "@/db";
|
||||
@@ -119,13 +119,11 @@ export const updateProjectAction = userAction
|
||||
.where(inArray(drizzleDb.schemas.alertPolicy.databaseId, databasesToRemove)).execute();
|
||||
|
||||
}
|
||||
// const slug = slugify(parsedInput.data.name);
|
||||
|
||||
const [updatedProject] = await db
|
||||
.update(drizzleDb.schemas.project)
|
||||
.set({
|
||||
name: parsedInput.data.name,
|
||||
// slug: slug,
|
||||
})
|
||||
.where(eq(drizzleDb.schemas.project.id, parsedInput.projectId))
|
||||
.returning();
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ProjectSchema = z.object({
|
||||
name: z.string(),
|
||||
name: z.string().nonempty("Name is required"),
|
||||
databases: z.array(z.string()),
|
||||
});
|
||||
|
||||
@@ -89,7 +89,8 @@ export async function dispatchStorage(
|
||||
return await dispatchViaProvider(
|
||||
channel.provider as StorageProviderKind,
|
||||
channel.config,
|
||||
input
|
||||
input,
|
||||
|
||||
);
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import {Backup, DatabaseWith} from "@/db/schema/07_database";
|
||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||
import type {StorageInput, StorageResult} from "@/features/storages/types";
|
||||
import type {
|
||||
StorageGetInput,
|
||||
StorageInput,
|
||||
StorageMetaData,
|
||||
StorageResult,
|
||||
StorageUploadInput
|
||||
} from "@/features/storages/types";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {db} from "@/db";
|
||||
import {createHash} from "crypto";
|
||||
import crypto, {createHash} from "crypto";
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
import path from "path";
|
||||
|
||||
function computeChecksum(buffer: Buffer): string {
|
||||
return createHash("sha256").update(buffer).digest("hex");
|
||||
@@ -114,3 +122,34 @@ export async function storeBackupFiles(
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function generateFileUrl(input: { data: StorageGetInput | StorageUploadInput, metadata?: StorageMetaData }): Promise<string | null> {
|
||||
const fileName = path.basename(input.data.path);
|
||||
const baseUrl = getServerUrl();
|
||||
const metadata = input.metadata;
|
||||
|
||||
if (!metadata){
|
||||
return null;
|
||||
}
|
||||
|
||||
let params = new URLSearchParams({
|
||||
storageId: metadata.storageId,
|
||||
});
|
||||
|
||||
if (metadata.fileKind === "backups") {
|
||||
const crypto = require("crypto");
|
||||
const expiresAt = Date.now() + 60 * 1000;
|
||||
const token = crypto.createHash("sha256").update(`${fileName}${expiresAt}`).digest("hex");
|
||||
|
||||
params.set("path", input.data.path);
|
||||
params.set("token", token);
|
||||
params.set("expires", expiresAt.toString());
|
||||
return `${baseUrl}/api/files/${metadata.fileKind}/?${params.toString()}`
|
||||
}else if (metadata.fileKind === "images") {
|
||||
return `${baseUrl}/api/files/${metadata.fileKind}/${fileName}?${params.toString()}`
|
||||
}else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// import {drive_v3, google} from "googleapis";
|
||||
// import Drive = drive_v3.Drive;
|
||||
// import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types";
|
||||
//
|
||||
//
|
||||
// export async function getGoogleDriveClient(config: GoogleDriveConfig): Promise<Drive> {
|
||||
// const auth = new google.auth.JWT({
|
||||
// email: config.clientEmail,
|
||||
// key: config.privateKey?.replace(/\\n/g, "\n"),
|
||||
// scopes: ["https://www.googleapis.com/auth/drive"],
|
||||
// });
|
||||
//
|
||||
// return google.drive({
|
||||
// version: "v3",
|
||||
// auth,
|
||||
// });
|
||||
// }
|
||||
//
|
||||
//
|
||||
// export async function findFileByName(
|
||||
// drive: any,
|
||||
// name: string,
|
||||
// folderId: string
|
||||
// ): Promise<string | null> {
|
||||
// const res = await drive.files.list({
|
||||
// q: `name='${name}' and '${folderId}' in parents and trashed=false`,
|
||||
// fields: "files(id)",
|
||||
// pageSize: 1,
|
||||
// });
|
||||
//
|
||||
// return res.data.files?.[0]?.id ?? null;
|
||||
// }
|
||||
|
||||
import {drive_v3, google} from "googleapis";
|
||||
import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types";
|
||||
import Drive = drive_v3.Drive;
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
|
||||
export async function getGoogleDriveClient(config: GoogleDriveConfig): Promise<Drive> {
|
||||
const baseUrl = getServerUrl();
|
||||
|
||||
const oauth2Client = new google.auth.OAuth2(
|
||||
config.clientId,
|
||||
config.clientSecret,
|
||||
// config.redirectUri
|
||||
baseUrl
|
||||
);
|
||||
|
||||
oauth2Client.setCredentials({
|
||||
refresh_token: config.refreshToken
|
||||
});
|
||||
|
||||
return google.drive({
|
||||
version: "v3",
|
||||
auth: oauth2Client
|
||||
});
|
||||
}
|
||||
|
||||
export async function findFileByName(
|
||||
drive: drive_v3.Drive,
|
||||
name: string,
|
||||
folderId: string
|
||||
): Promise<string | null> {
|
||||
const res = await drive.files.list({
|
||||
q: `name='${name}' and '${folderId}' in parents and trashed=false`,
|
||||
fields: "files(id)",
|
||||
pageSize: 1,
|
||||
supportsAllDrives: true,
|
||||
includeItemsFromAllDrives: true,
|
||||
});
|
||||
|
||||
return res.data.files?.[0]?.id ?? null;
|
||||
}
|
||||
|
||||
|
||||
export async function ensureFolderPath(client: any, path: string, rootFolderId: string): Promise<string> {
|
||||
const parts = path.split("/").filter(Boolean); // ["backups", "project-1"]
|
||||
let parentId = rootFolderId;
|
||||
|
||||
for (const part of parts) {
|
||||
const res = await client.files.list({
|
||||
q: `'${parentId}' in parents and name='${part}' and mimeType='application/vnd.google-apps.folder' and trashed=false`,
|
||||
fields: "files(id, name)",
|
||||
supportsAllDrives: true,
|
||||
includeItemsFromAllDrives: true,
|
||||
});
|
||||
|
||||
if (res.data.files && res.data.files.length > 0) {
|
||||
parentId = res.data.files[0].id!;
|
||||
} else {
|
||||
const folder = await client.files.create({
|
||||
requestBody: {
|
||||
name: part,
|
||||
mimeType: "application/vnd.google-apps.folder",
|
||||
parents: [parentId],
|
||||
},
|
||||
fields: "id",
|
||||
supportsAllDrives: true,
|
||||
});
|
||||
parentId = folder.data.id!;
|
||||
}
|
||||
}
|
||||
|
||||
return parentId;
|
||||
}
|
||||
|
||||
|
||||
export async function resolveFilePath(client: any, fullPath: string, rootFolderId: string): Promise<string | null> {
|
||||
const parts = fullPath.split("/").filter(Boolean);
|
||||
const fileName = parts.pop()!;
|
||||
let parentId = rootFolderId;
|
||||
|
||||
for (const part of parts) {
|
||||
const res = await client.files.list({
|
||||
q: `'${parentId}' in parents and name='${part}' and mimeType='application/vnd.google-apps.folder' and trashed=false`,
|
||||
fields: "files(id, name)",
|
||||
supportsAllDrives: true,
|
||||
includeItemsFromAllDrives: true,
|
||||
});
|
||||
if (res.data.files && res.data.files.length > 0) {
|
||||
parentId = res.data.files[0].id!;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const fileRes = await client.files.list({
|
||||
q: `'${parentId}' in parents and name='${fileName}' and trashed=false`,
|
||||
fields: "files(id, name)",
|
||||
supportsAllDrives: true,
|
||||
includeItemsFromAllDrives: true,
|
||||
});
|
||||
|
||||
return fileRes.data.files?.[0]?.id || null;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
"use server"
|
||||
import {StorageDeleteInput, StorageGetInput, StorageMetaData, StorageResult, StorageUploadInput} from '../../types';
|
||||
import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types";
|
||||
import {
|
||||
ensureFolderPath,
|
||||
findFileByName,
|
||||
getGoogleDriveClient, resolveFilePath
|
||||
} from "@/features/storages/providers/google-drive/helpers";
|
||||
import {Readable} from "node:stream";
|
||||
import {generateFileUrl} from "@/features/storages/helpers";
|
||||
|
||||
export async function uploadGoogleDrive(
|
||||
config: GoogleDriveConfig,
|
||||
input: { data: StorageUploadInput, metadata?: StorageMetaData },
|
||||
): Promise<StorageResult> {
|
||||
const client = await getGoogleDriveClient(config);
|
||||
|
||||
const fullPath = input.data.path;
|
||||
const pathParts = fullPath.split("/").filter(Boolean);
|
||||
const fileName = pathParts.pop()!;
|
||||
const folderPath = pathParts.join("/");
|
||||
|
||||
const folderId = folderPath
|
||||
? await ensureFolderPath(client, folderPath, config.folderId)
|
||||
: config.folderId;
|
||||
|
||||
|
||||
const existing = await findFileByName(client, fileName, folderId);
|
||||
if (existing) return {success: false, provider: "google-drive", error: "File already exists"};
|
||||
|
||||
|
||||
await client.files.create({
|
||||
requestBody: {name: fileName, parents: [folderId]},
|
||||
media: {body: Readable.from(input.data.file as Buffer)},
|
||||
fields: "id",
|
||||
supportsAllDrives: true,
|
||||
});
|
||||
|
||||
if (input.data.url) {
|
||||
const url = await generateFileUrl(input);
|
||||
if (!url) {
|
||||
return {
|
||||
success: false,
|
||||
provider: "google-drive",
|
||||
response: "Unable to get url file"
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
provider: 'google-drive',
|
||||
url: url
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
provider: 'google-drive',
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
export async function getGoogleDrive(
|
||||
config: GoogleDriveConfig,
|
||||
input: { data: StorageGetInput, metadata: StorageMetaData },
|
||||
): Promise<StorageResult> {
|
||||
const client = await getGoogleDriveClient(config);
|
||||
|
||||
const fileId = await resolveFilePath(client, input.data.path, config.folderId);
|
||||
if (!fileId) return {success: false, provider: "google-drive", error: "File not found"};
|
||||
|
||||
const res = await client.files.get(
|
||||
{fileId, alt: "media", supportsAllDrives: true},
|
||||
{responseType: "arraybuffer"}
|
||||
);
|
||||
|
||||
|
||||
if (input.data.signedUrl) {
|
||||
const url = await generateFileUrl(input);
|
||||
|
||||
if (!url) {
|
||||
return {
|
||||
success: false,
|
||||
provider: "google-drive",
|
||||
response: "Unable to get url"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: "google-drive",
|
||||
file: Buffer.from(res.data as ArrayBuffer),
|
||||
url: url,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: "google-drive",
|
||||
file: Buffer.from(res.data as ArrayBuffer),
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteGoogleDrive(
|
||||
config: GoogleDriveConfig,
|
||||
input: { data: StorageDeleteInput, metadata?: StorageMetaData },
|
||||
): Promise<StorageResult> {
|
||||
const client = await getGoogleDriveClient(config);
|
||||
const fileId = await resolveFilePath(client, input.data.path, config.folderId);
|
||||
if (!fileId) return {success: false, provider: "google-drive", error: "File not found"};
|
||||
|
||||
await client.files.delete({fileId, supportsAllDrives: true});
|
||||
|
||||
return {success: true, provider: "google-drive"};
|
||||
}
|
||||
|
||||
export async function pingGoogleDrive(config: GoogleDriveConfig): Promise<StorageResult> {
|
||||
try {
|
||||
const drive = await getGoogleDriveClient(config);
|
||||
const name = `ping-${Date.now()}.txt`;
|
||||
const buffer = Buffer.from("ping");
|
||||
|
||||
const file = await drive.files.create({
|
||||
requestBody: {name, parents: [config.folderId]},
|
||||
media: {mimeType: "text/plain", body: Readable.from(buffer)},
|
||||
fields: "id",
|
||||
supportsAllDrives: true,
|
||||
});
|
||||
|
||||
await drive.files.get({fileId: file.data.id!, supportsAllDrives: true});
|
||||
await drive.files.delete({fileId: file.data.id!, supportsAllDrives: true});
|
||||
|
||||
return {success: true, provider: "google-drive", response: "Google Drive storage OK"};
|
||||
} catch (err: any) {
|
||||
return {success: false, provider: "google-drive", response: err.message};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// export type GoogleDriveConfig = {
|
||||
// clientEmail: string;
|
||||
// privateKey: string;
|
||||
// folderId: string;
|
||||
// };
|
||||
|
||||
|
||||
export type GoogleDriveConfig = {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
refreshToken: string;
|
||||
folderId: string;
|
||||
};
|
||||
@@ -1,11 +1,17 @@
|
||||
import type {
|
||||
import {
|
||||
StorageProviderKind,
|
||||
StorageInput,
|
||||
StorageResult,
|
||||
StorageResult, StorageMetaData,
|
||||
} from '../types';
|
||||
|
||||
import {uploadLocal, getLocal, deleteLocal, pingLocal} from './local';
|
||||
import {deleteS3, getS3, pingS3, uploadS3} from "@/features/storages/providers/s3";
|
||||
import {
|
||||
deleteGoogleDrive,
|
||||
getGoogleDrive,
|
||||
pingGoogleDrive,
|
||||
uploadGoogleDrive
|
||||
} from "@/features/storages/providers/google-drive";
|
||||
|
||||
type ProviderHandler = {
|
||||
upload: (config: any, input: StorageInput & { action: 'upload' }) => Promise<StorageResult>;
|
||||
@@ -27,6 +33,12 @@ const handlers: Record<StorageProviderKind, ProviderHandler> = {
|
||||
delete: deleteS3,
|
||||
ping: pingS3,
|
||||
},
|
||||
"google-drive": {
|
||||
upload: uploadGoogleDrive,
|
||||
get: getGoogleDrive,
|
||||
delete: deleteGoogleDrive,
|
||||
ping: pingGoogleDrive,
|
||||
}
|
||||
// gcs: null as any,
|
||||
// azure: null as any,
|
||||
};
|
||||
@@ -34,7 +46,7 @@ const handlers: Record<StorageProviderKind, ProviderHandler> = {
|
||||
export async function dispatchViaProvider(
|
||||
kind: StorageProviderKind,
|
||||
config: any,
|
||||
input: StorageInput
|
||||
input: StorageInput,
|
||||
): Promise<StorageResult> {
|
||||
const provider = handlers[kind];
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"use server"
|
||||
import {mkdir, writeFile, unlink, readFile} from 'fs/promises';
|
||||
import path from 'path';
|
||||
import {StorageDeleteInput, StorageGetInput, StorageResult, StorageUploadInput} from '../types';
|
||||
import {StorageDeleteInput, StorageGetInput, StorageMetaData, StorageResult, StorageUploadInput} from '../types';
|
||||
import fs from "node:fs";
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
import {generateFileUrl} from "@/features/storages/helpers";
|
||||
|
||||
// const BASE_DIR = "/private/uploads/files/";
|
||||
const BASE_DIR = "/private/uploads/";
|
||||
|
||||
export async function uploadLocal(
|
||||
config: { baseDir?: string },
|
||||
input: { data: StorageUploadInput }
|
||||
input: { data: StorageUploadInput, metadata?: StorageMetaData }
|
||||
): Promise<StorageResult> {
|
||||
const base = config.baseDir || BASE_DIR;
|
||||
const fullPath = path.join(process.cwd(), base, input.data.path);
|
||||
@@ -19,18 +19,35 @@ export async function uploadLocal(
|
||||
|
||||
await mkdir(dir, {recursive: true});
|
||||
await writeFile(fullPath, input.data.file);
|
||||
const baseUrl = getServerUrl();
|
||||
|
||||
|
||||
if (input.data.url) {
|
||||
const url = await generateFileUrl(input);
|
||||
if (!url) {
|
||||
return {
|
||||
success: false,
|
||||
provider: "local",
|
||||
response: "Unable to get url file"
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
provider: 'local',
|
||||
url: url
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
provider: 'local',
|
||||
url: `${baseUrl}/api/${input.data.path}`,
|
||||
};
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
export async function getLocal(
|
||||
config: { baseDir?: string },
|
||||
input: { data: StorageGetInput }
|
||||
input: { data: StorageGetInput, metadata: StorageMetaData }
|
||||
): Promise<StorageResult> {
|
||||
const base = config.baseDir || BASE_DIR;
|
||||
const filePath = path.join(process.cwd(), base, input.data.path)
|
||||
@@ -47,37 +64,35 @@ export async function getLocal(
|
||||
}
|
||||
|
||||
if (input.data.signedUrl) {
|
||||
const crypto = require("crypto");
|
||||
const baseUrl = getServerUrl();
|
||||
const url = await generateFileUrl(input);
|
||||
|
||||
const expiresAt = Date.now() + 60 * 1000;
|
||||
const token = crypto.createHash("sha256").update(`${fileName}${expiresAt}`).digest("hex");
|
||||
|
||||
const params = new URLSearchParams({
|
||||
path: input.data.path,
|
||||
token,
|
||||
expires: expiresAt.toString(),
|
||||
});
|
||||
if (!url) {
|
||||
return {
|
||||
success: false,
|
||||
provider: "local",
|
||||
response: "Unable to get url file"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: 'local',
|
||||
file: file,
|
||||
url: `${baseUrl}/api/files/?${params.toString()}`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
provider: 'local',
|
||||
provider: "local",
|
||||
file: file,
|
||||
url: url,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: "local",
|
||||
file: file,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export async function deleteLocal(
|
||||
config: { baseDir?: string },
|
||||
input: { data: StorageDeleteInput }
|
||||
input: { data: StorageDeleteInput, metadata?: StorageMetaData }
|
||||
): Promise<StorageResult> {
|
||||
const base = config.baseDir || BASE_DIR;
|
||||
const fullPath = path.join(process.cwd(), base, input.data.path);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as Minio from "minio";
|
||||
import {StorageDeleteInput, StorageGetInput, StorageResult, StorageUploadInput} from "../types";
|
||||
import {StorageDeleteInput, StorageGetInput, StorageMetaData, StorageResult, StorageUploadInput} from "../types";
|
||||
import {generateFileUrl} from "@/features/storages/helpers";
|
||||
|
||||
type S3Config = {
|
||||
endPointUrl: string;
|
||||
@@ -20,7 +21,6 @@ async function getS3Client(config: S3Config) {
|
||||
});
|
||||
}
|
||||
|
||||
// Keep it like that
|
||||
const BASE_DIR = "";
|
||||
|
||||
|
||||
@@ -30,7 +30,10 @@ async function ensureBucket(config: S3Config) {
|
||||
if (!exists) await client.makeBucket(config.bucketName);
|
||||
}
|
||||
|
||||
export async function uploadS3(config: S3Config, input: { data: StorageUploadInput }): Promise<StorageResult> {
|
||||
export async function uploadS3(
|
||||
config: S3Config,
|
||||
input: { data: StorageUploadInput, metadata?: StorageMetaData }
|
||||
): Promise<StorageResult> {
|
||||
const client = await getS3Client(config);
|
||||
await ensureBucket(config);
|
||||
|
||||
@@ -45,18 +48,34 @@ export async function uploadS3(config: S3Config, input: { data: StorageUploadInp
|
||||
|
||||
await client.putObject(config.bucketName, key, input.data.file as Buffer);
|
||||
|
||||
|
||||
if (input.data.url) {
|
||||
const url = await generateFileUrl(input);
|
||||
if (!url) {
|
||||
return {
|
||||
success: false,
|
||||
provider: "s3",
|
||||
response: "Unable to get url file"
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
provider: 's3',
|
||||
url: url
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
provider: "s3",
|
||||
provider: 's3',
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
export async function getS3(config: S3Config, input: { data: StorageGetInput }): Promise<StorageResult> {
|
||||
export async function getS3(config: S3Config, input: { data: StorageGetInput, metadata: StorageMetaData }): Promise<StorageResult> {
|
||||
const client = await getS3Client(config);
|
||||
|
||||
// const key = input.data.path;
|
||||
const key = `${BASE_DIR}${input.data.path}`;
|
||||
|
||||
try {
|
||||
await client.statObject(config.bucketName, key);
|
||||
} catch {
|
||||
@@ -78,9 +97,8 @@ export async function getS3(config: S3Config, input: { data: StorageGetInput }):
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteS3(config: S3Config, input: { data: StorageDeleteInput }): Promise<StorageResult> {
|
||||
export async function deleteS3(config: S3Config, input: { data: StorageDeleteInput, metadata?: StorageMetaData }): Promise<StorageResult> {
|
||||
const client = await getS3Client(config);
|
||||
// const key = input.data.path;
|
||||
const key = `${BASE_DIR}${input.data.path}`;
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type StorageProviderKind =
|
||||
| 'local'
|
||||
| 's3'
|
||||
| 'google-drive'
|
||||
;
|
||||
|
||||
export type StorageAction =
|
||||
@@ -8,9 +9,19 @@ export type StorageAction =
|
||||
| 'get'
|
||||
| 'delete';
|
||||
|
||||
export type StorageFileKind =
|
||||
| 'backups'
|
||||
| 'images'
|
||||
|
||||
export type StorageMetaData = {
|
||||
storageId: string,
|
||||
fileKind: StorageFileKind
|
||||
}
|
||||
|
||||
export interface StorageUploadInput {
|
||||
path: string;
|
||||
file: Buffer | Uint8Array;
|
||||
url?: boolean;
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
@@ -25,9 +36,9 @@ export interface StorageDeleteInput {
|
||||
}
|
||||
|
||||
export type StorageInput =
|
||||
| { action: 'upload'; data: StorageUploadInput }
|
||||
| { action: 'get'; data: StorageGetInput }
|
||||
| { action: 'delete'; data: StorageDeleteInput }
|
||||
| { action: 'upload'; data: StorageUploadInput, metadata?: StorageMetaData }
|
||||
| { action: 'get'; data: StorageGetInput, metadata: StorageMetaData}
|
||||
| { action: 'delete'; data: StorageDeleteInput, metadata?: StorageMetaData }
|
||||
| { action: 'ping'; };
|
||||
|
||||
export interface StorageResult {
|
||||
|
||||
@@ -2,21 +2,13 @@
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {z} from "zod";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import {mkdir, writeFile} from "fs/promises";
|
||||
import path from "path";
|
||||
import {env} from "@/env.mjs";
|
||||
import {checkMinioAlive, saveFileInBucket} from "@/utils/s3-file-management";
|
||||
//@ts-ignore
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
import {db} from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||
import {StorageInput} from "@/features/storages/types";
|
||||
|
||||
// const imageDir = "images/";
|
||||
import sharp from "sharp";
|
||||
|
||||
|
||||
export const uploadUserImageAction = userAction.schema(
|
||||
@@ -31,6 +23,19 @@ export const uploadUserImageAction = userAction.schema(
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
|
||||
const isPng = fileFormat === "png";
|
||||
const isWebp = fileFormat === "webp";
|
||||
|
||||
const compressedBuffer = await sharp(buffer)
|
||||
.resize({ width: 1024 })
|
||||
.toFormat(isPng ? "png" : isWebp ? "webp" : "jpeg", {
|
||||
quality: 80,
|
||||
compressionLevel: isPng ? 9 : undefined
|
||||
})
|
||||
.toBuffer();
|
||||
|
||||
|
||||
|
||||
const settings = await db.query.setting.findFirst({
|
||||
where: eq(drizzleDb.schemas.setting.name, "system"),
|
||||
with: {
|
||||
@@ -54,12 +59,17 @@ export const uploadUserImageAction = userAction.schema(
|
||||
action: "upload",
|
||||
data: {
|
||||
path: path,
|
||||
file: buffer
|
||||
file: compressedBuffer,
|
||||
url: true
|
||||
},
|
||||
metadata: {
|
||||
storageId: settings.storageChannel.id,
|
||||
fileKind: "images"
|
||||
}
|
||||
}
|
||||
|
||||
const result = await dispatchStorage(input, undefined, settings.storageChannel.id);
|
||||
|
||||
console.log(result);
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -90,74 +100,5 @@ export const uploadUserImageAction = userAction.schema(
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// export const uploadImageAction = userAction
|
||||
// .schema(z.instanceof(FormData))
|
||||
// .action(async ({parsedInput: formData, ctx}) => {
|
||||
// const file = formData.get("file") as File;
|
||||
// const uuid = uuidv4();
|
||||
// const fileFormat = file.name.split(".").pop();
|
||||
// const arrayBuffer = await file.arrayBuffer();
|
||||
// const buffer = Buffer.from(arrayBuffer);
|
||||
//
|
||||
//
|
||||
// const settings = await db.query.setting.findFirst({
|
||||
// where: eq(drizzleDb.schemas.setting.name, "system"),
|
||||
// with: {
|
||||
// storageChannel: true
|
||||
// }
|
||||
// });
|
||||
//
|
||||
//
|
||||
// if (!settings) throw new Error("System settings not found.");
|
||||
//
|
||||
// let fileName: string | null = null;
|
||||
// let result: void | UploadedObjectInfo;
|
||||
//
|
||||
// if (settings.storage === "local") {
|
||||
// fileName = `${imageDir}${uuid}.${fileFormat}`;
|
||||
// result = await uploadLocal(fileName, buffer);
|
||||
// } else if (settings.storage === "s3") {
|
||||
// fileName = `${imageDir}${uuid}.${fileFormat}`;
|
||||
// result = await uploadS3Compatible(env.S3_BUCKET_NAME ?? "", fileName, buffer);
|
||||
// } else {
|
||||
// throw new Error(`Unsupported storage type: ${settings.storage}`);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// const url = `${getServerUrl()}/api/${fileName}`
|
||||
// return {data: {result, url}};
|
||||
// });
|
||||
//
|
||||
|
||||
async function uploadLocal(fileName: string, buffer: any) {
|
||||
const localDir = "private/uploads/";
|
||||
try {
|
||||
await mkdir(path.join(process.cwd(), localDir), {recursive: true});
|
||||
return await writeFile(path.join(process.cwd(), localDir + fileName), buffer);
|
||||
} catch (error) {
|
||||
throw new Error("An error occured while importing image");
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadS3Compatible(bucketName: string, fileName: string, buffer: any) {
|
||||
return await saveFileInBucket({
|
||||
bucketName,
|
||||
fileName,
|
||||
file: buffer,
|
||||
});
|
||||
}
|
||||
|
||||
function getUrl(fileName: string, settings: Setting, bucketName: string): string {
|
||||
if (settings.storage === "s3") {
|
||||
return `https://${env.S3_ENDPOINT}/${bucketName}/${fileName}`;
|
||||
} else if (settings.storage === "local") {
|
||||
const url = getServerUrl();
|
||||
return `${url}/api/images/${fileName}`;
|
||||
}
|
||||
throw new Error("Invalid storage configuration");
|
||||
}
|
||||
|
||||
export async function checkConnexionToS3() {
|
||||
return await checkMinioAlive();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user