Working on some bugs.

This commit is contained in:
charlesgauthereau
2025-07-27 21:26:31 +02:00
parent f9dc15ba73
commit 85047e7509
107 changed files with 971 additions and 2203 deletions
+23 -2
View File
@@ -1,4 +1,25 @@
# Ignore Docker-related configs
.dockerignore
docker-compose.yml
README.md
docker-compose*.yml
# Development/Editor config
.idea
.next
.vscode
# Node-related
node_modules
npm-debug.log
yarn-debug.log
yarn-error.log
pnpm-debug.log
# Next.js build output
.next
out
# Docs and markdowns
README.md
*.md
Binary file not shown.
+4 -4
View File
@@ -32,13 +32,13 @@ export default function Layout({children}: { children: React.ReactNode }) {
<div className="flex min-h-full flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8 ">
<div className="mx-auto w-full max-w-md">
<div className="sm:mx-auto sm:w-full sm:max-w-md flex items-center justify-center space-x-2">
<Image
<img
className="p-12 text-black dark:text-white"
src={imageTheme}
loading="eager"
// loading="eager"
alt="Logo"
width={1024}
height={1024}
// width={1024}
// height={1024}
/>
<span
className="text-sm text-muted-foreground -ml-12 -mb-12">
+1 -1
View File
@@ -3,7 +3,7 @@
import {useEffect, useRef, useState} from "react";
import {useSearchParams} from "next/navigation";
import {LoginForm} from "@/components/wrappers/auth/login/loginForm/LoginForm";
import {LoginForm} from "@/components/wrappers/auth/login/login-form/login-form";
import {toast} from "sonner";
export default function SignInPage(props: {
@@ -1,6 +1,6 @@
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { AgentForm } from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
import { AgentForm } from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
import { notFound } from "next/navigation";
import { db } from "@/db";
@@ -1,17 +1,21 @@
import Link from "next/link";
import {GearIcon} from "@radix-ui/react-icons";
import {PageParams} from "@/types/next";
import {Page, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {formatDateLastContact} from "@/utils/date-formatting";
import {buttonVariants} from "@/components/ui/button";
import {Card, CardContent, CardHeader} from "@/components/ui/card";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
import {AgentCardKey} from "@/components/wrappers/dashboard/agent/AgentCardKey/AgentCardKey";
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
import {AgentCardKey} from "@/components/wrappers/dashboard/agent/agent-card-key/agent-card-key";
import { db } from "@/db";
import * as drizzleDb from "@/db";
import {and, eq} from "drizzle-orm";
import {notFound} from "next/navigation";
import {
ButtonDeleteProject
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
import {ButtonDeleteAgent} from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
export default async function RoutePage(props: PageParams<{ agentId: string }>) {
@@ -26,6 +30,7 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
})
if (!agent) {
notFound()
}
@@ -74,7 +79,9 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
<GearIcon className="w-7 h-7"/>
</Link>
</PageTitle>
<PageActions className="justify-between">
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"} />
</PageActions>
</div>
<PageDescription className="mt-5 sm:mt-0">{agent.description}</PageDescription>
<PageContent className="flex flex-col w-full h-full">
@@ -1,6 +1,6 @@
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { AgentForm } from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
import { AgentForm } from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
export default async function RoutePage(props: PageParams<{}>) {
return (
@@ -1,15 +1,22 @@
import { PageParams } from "@/types/next";
import { AgentCard } from "@/components/wrappers/dashboard/agent/AgentCard/AgentCard";
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";
import * as drizzleDb from "@/db";
import {and, eq, not} from "drizzle-orm";
export const dynamic = "force-dynamic";
export default async function RoutePage(props: PageParams<{}>) {
const agents = await db.query.agent.findMany();
const agents = await db.query.agent.findMany({
where: not(eq(drizzleDb.schemas.agent.isArchived, true))
});
if (!agents) {
notFound();
@@ -1,7 +1,7 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation";
import {RestoreForm} from "@/components/wrappers/dashboard/database/RestoreForm";
import {RestoreForm} from "@/components/wrappers/dashboard/database/restore-form";
import {currentUser} from "@/lib/auth/current-user";
import {db} from "@/db";
@@ -1,7 +1,7 @@
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { notFound } from "next/navigation";
import { DatabaseForm } from "@/components/wrappers/dashboard/database/DatabaseForm/DatabaseForm";
import { DatabaseForm } from "@/components/wrappers/dashboard/database/database-form/database-form";
import { db } from "@/db";
import * as drizzleDb from "@/db";
@@ -2,10 +2,10 @@ import {PageParams} from "@/types/next";
import {notFound} from "next/navigation";
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/Database/DatabaseTabs";
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/Database/DatabaseKpi";
import {EditButton} from "@/components/wrappers/dashboard/database/EditButton/EditButton";
import {CronButton} from "@/components/wrappers/dashboard/database/CronButton/CronButton";
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/database/database-tabs";
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/database/database-kpi";
import {EditButton} from "@/components/wrappers/dashboard/database/edit-button/edit-button";
import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button";
import {db} from "@/db";
import {eq, and} from "drizzle-orm";
@@ -1,7 +1,7 @@
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/ProjectsForm/ProjectForm";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
import * as drizzleDb from "@/db";
import {db} from "@/db";
@@ -3,9 +3,9 @@ import { Page, PageActions, PageContent, PageDescription, PageTitle } from "@/fe
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/ButtonDeleteProject/ButtonDeleteProject";
import { ButtonDeleteProject } from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
import { ProjectDatabaseCard } from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
import { ProjectDatabaseCard } from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
import { notFound } from "next/navigation";
import { db } from "@/db";
@@ -1,6 +1,6 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
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";
@@ -4,7 +4,7 @@ import {PageParams} from "@/types/next";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {Button} from "@/components/ui/button";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectCard";
import {ProjectCard} from "@/components/wrappers/dashboard/projects/project-card/project-card";
import {db} from "@/db";
import {notFound} from "next/navigation";
import {getOrganization} from "@/lib/auth/auth";
@@ -1,15 +1,25 @@
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/OrganizationForm/OrganizationForm";
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({});
if (!organization || organization.slug == "default") {
const users = await db.query.user.findMany({
where: (fields) => isNull(fields.deletedAt)
});
if (!user || !users || !organization || organization.slug == "default") {
notFound();
}
@@ -21,7 +31,7 @@ export default async function RoutePage(props: PageParams<{
</PageTitle>
</PageHeader>
<PageContent>
<OrganizationForm members={organization.members} defaultValues={organization}/>
<OrganizationForm currentUser={user} users={users} defaultValues={organization}/>
</PageContent>
</Page>
)
@@ -5,8 +5,8 @@ import {getOrganization} from "@/lib/auth/auth";
import {notFound} from "next/navigation";
import {
DeleteOrganizationButton
} from "@/components/wrappers/dashboard/organization/DeleteOrganization/DeleteOrganizationButton";
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/EditButtonSettings/EditButtonSettings";
} from "@/components/wrappers/dashboard/organization/delete-organization/delete-organization-button";
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/edit-button-settings/edit-button-settings";
import {
SettingsOrganizationMembersTable
} from "@/components/wrappers/dashboard/settings/settings-organization-members-table";
@@ -1,15 +1,16 @@
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { EvolutionLineChart } from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
import { PercentageLineChart } from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
import { notFound } from "next/navigation";
import { db } from "@/db";
import { asc, count, eq, inArray } from "drizzle-orm";
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
import {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
import {notFound} from "next/navigation";
import {db} from "@/db";
import {asc, count, eq, inArray} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {getOrganization} from "@/lib/auth/auth";
import {DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
export default async function RoutePage(props: PageParams<{ }>) {
export default async function RoutePage(props: PageParams<{}>) {
const organization = await getOrganization({});
if (!organization) {
@@ -26,7 +27,13 @@ export default async function RoutePage(props: PageParams<{ }>) {
where: eq(drizzleDb.schemas.project.organizationId, org.id),
});
const projectsCount = projects.length;
const projectIds = projects.map(project => project.id);
const databasesOfAllProjects = await db.query.database.findMany({
where: inArray(drizzleDb.schemas.database.projectId, projectIds),
})
const databaseIds = databasesOfAllProjects.map((database) => database.id);
const backupsEvolution = await db.query.backup.findMany({
columns: {
@@ -34,9 +41,11 @@ export default async function RoutePage(props: PageParams<{ }>) {
createdAt: true,
},
orderBy: [asc(drizzleDb.schemas.backup.id)],
where: inArray(drizzleDb.schemas.backup.databaseId, databaseIds),
});
const backupsRate = await db
.select({
createdAt: drizzleDb.schemas.backup.createdAt,
@@ -48,7 +57,20 @@ export default async function RoutePage(props: PageParams<{ }>) {
.groupBy(drizzleDb.schemas.backup.createdAt, drizzleDb.schemas.backup.status)
.orderBy(drizzleDb.schemas.backup.createdAt);
console.log(backupsRate);
const restorationsCountResult = await db
.select({
count: count(),
})
.from(drizzleDb.schemas.restoration)
.where(inArray(drizzleDb.schemas.restoration.databaseId, databaseIds));
const restorationsCount = restorationsCountResult[0]?.count ?? 0;
const projectsCount = projects.length;
const backupsEvolutionCount = backupsEvolution.length;
return (
@@ -58,23 +80,26 @@ export default async function RoutePage(props: PageParams<{ }>) {
</PageHeader>
<PageContent className="flex flex-col gap-y-4">
<div className="flex flex-col md:flex-row gap-4">
<Card className="w-full">
<CardHeader>
<Card className="w-full flex-1">
<CardHeader className="flex items-center gap-2">
<Folder className="w-5 h-5 text-muted-foreground" />
<CardTitle>Projects</CardTitle>
</CardHeader>
<CardContent>{projectsCount}</CardContent>
<CardContent className="text-3xl font-bold">{projectsCount}</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>KPI 2</CardTitle>
<Card className="w-full flex-1">
<CardHeader className="flex items-center gap-2">
<DatabaseBackup className="w-5 h-5 text-muted-foreground" />
<CardTitle>Backups</CardTitle>
</CardHeader>
<CardContent></CardContent>
<CardContent className="text-3xl font-bold">{backupsEvolutionCount}</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>KPI 3</CardTitle>
<Card className="w-full flex-1">
<CardHeader className="flex items-center gap-2">
<RefreshCcw className="w-5 h-5 text-muted-foreground" />
<CardTitle>Restorations</CardTitle>
</CardHeader>
<CardContent></CardContent>
<CardContent className="text-3xl font-bold">{restorationsCount}</CardContent>
</Card>
</div>
<div className="flex flex-col md:flex-row gap-4">
@@ -83,7 +108,7 @@ export default async function RoutePage(props: PageParams<{ }>) {
<CardTitle>Evolution of the number of backups</CardTitle>
</CardHeader>
<CardContent>
<EvolutionLineChart data={backupsEvolution} />
<EvolutionLineChart data={backupsEvolution}/>
</CardContent>
</Card>
<Card className="w-full">
@@ -91,7 +116,7 @@ export default async function RoutePage(props: PageParams<{ }>) {
<CardTitle>Success rate of backups</CardTitle>
</CardHeader>
<CardContent>
<PercentageLineChart data={backupsRate} />
<PercentageLineChart data={backupsRate}/>
</CardContent>
</Card>
</div>
+1 -1
View File
@@ -1,4 +1,4 @@
import { LoadingSpinner } from "@/components/wrappers/common/loading/LoadingSpinner";
import { LoadingSpinner } from "@/components/wrappers/common/loading/loading-spinner";
export default function Loading() {
return (
+3 -3
View File
@@ -1,10 +1,10 @@
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation";
import {UserForm} from "@/components/wrappers/dashboard/profile/UserForm/UserForm";
import {UserForm} from "@/components/wrappers/dashboard/profile/user-form/user-form";
import {Badge} from "@/components/ui/badge";
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/ButtonDeleteAccount";
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/Avatar/AvatarWithUpload";
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/button-delete-account/button-delete-account";
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/avatar/avatar-with-upload";
import {currentUser} from "@/lib/auth/current-user";
import {getAccounts, getSessions} from "@/lib/auth/auth";
//import { getAccounts, getSessions } from "@/lib/auth/auth";
+35 -42
View File
@@ -1,36 +1,29 @@
name: portabase-dev
services:
app:
build:
context: .
dockerfile: docker/dockerfile/Dockerfile
target: dev
ports:
- "8887:80"
environment:
- TIME_ZONE="Europe/Paris"
- NODE_ENV=development
depends_on:
db:
condition: service_healthy
volumes:
- .:/app
- /app/node_modules
container_name: portabase-app
develop:
watch:
- action: sync
path: .
target: /app
- action: rebuild
path: package.json
# app:
# build:
# context: .
# dockerfile: docker/dockerfile/Dockerfile
# target: dev
# ports:
# - "8887:8887"
# environment:
# - TIME_ZONE="Europe/Paris"
# - NODE_ENV=development
# depends_on:
# db:
# condition: service_healthy
# volumes:
# - .:/app
# - /app/node_modules
# container_name: portabase-app
#
db:
image: postgres:16-alpine
ports:
- "5433:5432"
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
@@ -43,21 +36,21 @@ services:
timeout: 5s
retries: 5
db-pgadmin:
image: dpage/pgadmin4
environment:
PGADMIN_DEFAULT_EMAIL: "devuser@devuser.devuser"
PGADMIN_DEFAULT_PASSWORD: "changeme"
PGADMIN_CONFIG_SERVER_MODE: "False"
POSTGRES_USER: "devuser"
POSTGRES_PASSWORD: "changeme"
volumes:
- pgadmin-data:/var/lib/pgadmin
ports:
- "8080:80"
restart: unless-stopped
depends_on:
- db
# db-pgadmin:
# image: dpage/pgadmin4
# environment:
# PGADMIN_DEFAULT_EMAIL: "devuser@devuser.devuser"
# PGADMIN_DEFAULT_PASSWORD: "changeme"
# PGADMIN_CONFIG_SERVER_MODE: "False"
# POSTGRES_USER: "devuser"
# POSTGRES_PASSWORD: "changeme"
# volumes:
# - pgadmin-data:/var/lib/pgadmin
# ports:
# - "8080:80"
# restart: unless-stopped
# depends_on:
# - db
s3:
image: docker.io/bitnami/minio:latest
@@ -74,4 +67,4 @@ services:
volumes:
postgres-data:
minio_data:
pgadmin-data:
# pgadmin-data:
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:23-alpine AS base
FROM node:22-alpine AS base
ENV YARN_VERSION=4.9.1
+13 -6
View File
@@ -1,11 +1,18 @@
#!/bin/bash
#
#npx drizzle-kit generate
#npx drizzle-kit migrate
#
#npm run dev
#
#exec "$@"
set -euo pipefail
npm install -g npm@11.0.0
npm -v
echo "▶ Running Drizzle codegen..."
npx drizzle-kit generate
echo "▶ Applying migrations..."
npx drizzle-kit migrate
npm run dev
exec "$@"
echo "▶ Starting Next.js dev server..."
exec npm run dev
-20
View File
@@ -1,20 +0,0 @@
// next-auth.d.ts
import NextAuth from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
email: string;
name: string;
authMethod: string; // Add authMethod to the session user type
};
}
interface User {
id: string;
email: string;
name: string;
authMethod: string; // Add authMethod to the user type
}
}
+3 -3
View File
@@ -3,7 +3,7 @@
"version": "1.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack -p 80",
"dev": "next dev --turbopack -p 8887",
"build": "next build --experimental-build-mode compile",
"start": "next start",
"lint": "next lint",
@@ -52,7 +52,7 @@
"@zenstackhq/runtime": "2.14.2",
"argon2": "^0.43.0",
"bcrypt": "^6.0.0",
"better-auth": "^1.2.8",
"better-auth": "1.3.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -65,7 +65,7 @@
"input-otp": "^1.4.2",
"lucide-react": "^0.510.0",
"minio": "^8.0.5",
"next": "^15.4.2-canary.5",
"next": "15.4.2-canary.10",
"next-safe-action": "^7.10.8",
"next-themes": "^0.4.6",
"nodemailer": "^7.0.3",
@@ -9,9 +9,9 @@ import { toast } from "sonner";
import { useMutation } from "@tanstack/react-query";
import { TooltipProvider } from "@/components/ui/tooltip";
import Link from "next/link";
import { PasswordInput } from "@/components/wrappers/auth/PaswordInput/password-input";
import { LoginSchema, LoginType } from "@/components/wrappers/auth/login/loginForm/login-form.schema";
import { SocialAuthButton, SocialProviderType } from "@/components/wrappers/auth/login/buttonAuth/SocialAuthButton";
import { PasswordInput } from "@/components/wrappers/auth/password-input/password-input";
import { LoginSchema, LoginType } from "@/components/wrappers/auth/login/login-form/login-form.schema";
import { SocialAuthButton, SocialProviderType } from "@/components/wrappers/auth/login/button-auth/social-auth-button";
import { signIn } from "@/lib/auth/auth-client";
import { useRouter } from "next/navigation";
import { Icon } from "@iconify/react";
@@ -12,7 +12,7 @@ import { Form } from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { TooltipProvider, TooltipTrigger, Tooltip, TooltipContent } from "@/components/ui/tooltip";
import { RegisterSchema, RegisterType } from "@/components/wrappers/auth/register/register-form/register-form.schema";
import { PasswordInput } from "@/components/wrappers/auth/PaswordInput/password-input";
import { PasswordInput } from "@/components/wrappers/auth/password-input/password-input";
import {signUp} from "@/lib/auth/auth-client";
export type registerFormProps = {
@@ -1,7 +1,7 @@
"use server";
import { userAction } from "@/safe-actions";
import { z } from "zod";
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.schema";
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema";
import { eq } from "drizzle-orm";
import { db } from "@/db";
import * as drizzleDb from "@/db";
@@ -8,9 +8,9 @@ import { Button } from "@/components/ui/button";
import { useMutation } from "@tanstack/react-query";
import { TooltipProvider } from "@/components/ui/tooltip";
import { EmailFormSchema, EmailFormType } from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.schema";
import { PasswordInput } from "@/components/wrappers/auth/PaswordInput/password-input";
import { updateEmailSettingsAction } from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.action";
import { EmailFormSchema, EmailFormType } from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema";
import { PasswordInput } from "@/components/wrappers/auth/password-input/password-input";
import { updateEmailSettingsAction } from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.action";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
@@ -1,4 +1,4 @@
import { EmailForm } from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/EmailForm";
import { EmailForm } from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form";
import { Send } from "lucide-react";
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
import { useMutation } from "@tanstack/react-query";
@@ -2,14 +2,14 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Info, ShieldCheck } from "lucide-react";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { StorageS3Form } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/StorageS3Form";
import { StorageS3Form } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/storage-s3-form";
import { useState } from "react";
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
import { useMutation } from "@tanstack/react-query";
import { checkConnexionToS3 } from "@/features/upload/public/upload.action";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import { updateStorageSettingsAction } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.action";
import { updateStorageSettingsAction } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.action";
import {Setting} from "@/db/schema/00_setting";
export type SettingsStorageTabProps = {
@@ -3,7 +3,7 @@
import { userAction } from "@/safe-actions";
import { z } from "zod";
import { db } from "@/db";
import { S3FormSchema, StorageSwitchSchema } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.schema";
import { S3FormSchema, StorageSwitchSchema } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.schema";
import { eq } from "drizzle-orm";
import * as drizzleDb from "@/db";
@@ -8,10 +8,10 @@ import { Button } from "@/components/ui/button";
import { useMutation } from "@tanstack/react-query";
import { TooltipProvider } from "@/components/ui/tooltip";
import { S3FormSchema, S3FormType } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.schema";
import { S3FormSchema, S3FormType } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.schema";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { updateS3SettingsAction } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.action";
import { updateS3SettingsAction } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.action";
export type S3FormProps = {
defaultValues?: S3FormType;
@@ -1,8 +1,8 @@
"use client";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { SettingsEmailTab } from "@/components/wrappers/dashboard/admin/AdminEmailTab/SettingsEmailTab";
import { SettingsStorageTab } from "@/components/wrappers/dashboard/admin/AdminStorageTab/SettingsStorageTab";
import { SettingsEmailTab } from "@/components/wrappers/dashboard/admin/admin-email-tab/settings-email-tab";
import { SettingsStorageTab } from "@/components/wrappers/dashboard/admin/admin-storage-tab/settings-storage-tab";
import { AdminUsersTable } from "@/components/wrappers/dashboard/admin/admin-user-table";
import { User } from "@/db/schema/01_user";
import { Setting } from "@/db/schema/00_setting";
@@ -1,13 +1,13 @@
"use client"
import {ColumnDef} from "@tanstack/react-table";
import {Badge} from "@/components/ui/badge";
import {updateUserAction} from "@/components/wrappers/dashboard/profile/UserForm/user-form.action";
import {updateUserAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
import {useMutation} from "@tanstack/react-query";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {useState} from "react";
import {Trash2} from "lucide-react";
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/delete-account.action";
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-delete-account/delete-account.action";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {User} from "@/db/schema/01_user";
import {useSession} from "@/lib/auth/auth-client";
@@ -1,7 +1,7 @@
"use client";
import {generateEdgeKey} from "@/utils/edge_key";
import {getServerUrl} from "@/utils/get-server-url";
import {PasswordInput} from "@/components/wrappers/auth/PaswordInput/password-input";
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
import {useState} from "react";
import {CopyButton} from "@/components/wrappers/common/button/copy-button";
import {Agent} from "@/db/schema/07_agent";
@@ -16,7 +16,7 @@ export const AgentCard = (props: agentCardProps) => {
return (
<Link href={`/dashboard/agents/${agent.id}`} className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md">
<Card className="flex flex-row justify-between">
<div>
<div className="flex-1 text-left">
<CardHeader className="text-2xl font-bold">{agent.name}</CardHeader>
<CardContent>Last contact: {formatDateLastContact(agent.lastContact)}</CardContent>
</div>
@@ -1,6 +1,6 @@
"use server";
import {ActionError, userAction} from "@/safe-actions";
import {AgentSchema} from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.schema";
import {AgentSchema} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.schema";
import {z} from "zod";
import {eq, and, ne, count} from "drizzle-orm";
import {db} from "@/db";
@@ -16,9 +16,9 @@ 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/AgentForm/agent-form.schema";
import {AgentSchema, AgentType} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.schema";
import {toast} from "sonner";
import {createAgentAction, updateAgentAction} from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.action";
import {createAgentAction, updateAgentAction} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.action";
export type agentFormProps = {
defaultValues?: AgentType;
@@ -7,7 +7,7 @@ import { generateEdgeKey } from "@/utils/edge_key";
import { PropsWithChildren } from "react";
import { CopyButton } from "@/components/wrappers/common/button/copy-button";
import { getServerUrl } from "@/utils/get-server-url";
import { CodeSnippet } from "@/components/wrappers/code-snippet/CodeSnippet";
import { CodeSnippet } from "@/components/wrappers/code-snippet/code-snippet";
import {Agent} from "@/db/schema/07_agent";
export type agentRegistrationDialogProps = PropsWithChildren<{
@@ -0,0 +1,42 @@
"use client";
import { Trash2 } from "lucide-react";
import { ButtonWithConfirm } from "@/components/wrappers/common/button/button-with-confirm";
import { useMutation } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import {deleteAgentAction} from "@/components/wrappers/dashboard/agent/button-delete-agent/delete-agent.action";
export type ButtonDeleteAgentProps = {
text?: string;
agentId: string;
};
export const ButtonDeleteAgent = (props: ButtonDeleteAgentProps) => {
const router = useRouter();
const mutation = useMutation({
mutationFn: () => deleteAgentAction(props.agentId),
onSuccess: async (result: any) => {
if (result.data?.success) {
toast.success(result.data.actionSuccess.message);
router.push("/dashboard/agents");
} else {
toast.error(result.data.actionError.message || "Unknown error occurred.");
}
},
});
return (
<ButtonWithConfirm
text={props.text ? props.text : ""}
onClick={() => {
mutation.mutate();
}}
variant={"destructive"}
isPending={mutation.isPending}
className="gap-2"
icon={<Trash2 />}
/>
);
};
@@ -0,0 +1,53 @@
"use server";
import {userAction} from "@/safe-actions";
import {z} from "zod";
import {v4 as uuidv4} from "uuid";
import {ServerActionResult} from "@/types/action-type";
import {eq} from "drizzle-orm";
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {Agent} from "@/db/schema/07_agent";
export const deleteAgentAction = userAction.schema(z.string()).action(async ({parsedInput}): Promise<ServerActionResult<Agent>> => {
try {
// const deletedAgent: Agent[] = await db.delete(drizzleDb.schemas.agent).where(eq(drizzleDb.schemas.agent.id, parsedInput)).returning();
const uuid = uuidv4();
const updatedAgent = await db
.update(drizzleDb.schemas.agent)
.set({
isArchived: true,
slug: uuid,
})
.where(eq(drizzleDb.schemas.agent.id, parsedInput))
.returning();
if (!updatedAgent[0]) {
throw new Error("Agent not found or update failed");
}
return {
success: true,
value: updatedAgent[0],
actionSuccess: {
message: "Agent has been successfully deleted.",
messageParams: {projectId: parsedInput},
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "Failed to delete agent.",
status: 500,
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: {projectId: parsedInput},
},
};
}
});
@@ -1,8 +1,8 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { SidebarMenuButton } from "@/components/ui/sidebar";
import { ChevronUp } from "lucide-react";
import { LoggedInDropdown } from "@/components/wrappers/dashboard/loggedInDropdown/LoggedInDropdown";
import { currentUser } from "@/lib/auth/current-user";
import {LoggedInDropdown} from "@/components/wrappers/dashboard/common/logged-in/logged-in-dropdown";
export const LoggedInButton = async () => {
const user = await currentUser();
@@ -8,8 +8,8 @@ import {
import {SidebarLogo} from "@/components/wrappers/dashboard/common/sidebar/logo-sidebar";
import {SidebarMenuCustomMain} from "@/components/wrappers/dashboard/common/sidebar/menu-sidebar-main";
import {SideBarFooterCredit} from "@/components/wrappers/dashboard/common/sidebar/side-bar-footer-credit";
import {LoggedInButton} from "@/components/wrappers/dashboard/loggedInButton/LoggedInButton";
import {OrganizationCombobox} from "@/components/wrappers/dashboard/organization/organization-combobox";
import {LoggedInButton} from "@/components/wrappers/dashboard/common/logged-in/logged-in-button";
export function AppSidebar() {
@@ -15,6 +15,7 @@ export const SidebarMenuCustomMain = () => {
const member = authClient.useActiveMember();
if (isPending) return null;
if (error || !session) {
@@ -2,7 +2,7 @@
import { Clock9 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { CronInput } from "@/components/wrappers/dashboard/database/CronButton/CronInput";
import { CronInput } from "@/components/wrappers/dashboard/database/cron-button/cron-input";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
@@ -10,7 +10,7 @@ import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/CronButton/cron.action";
import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/cron-button/cron.action";
import {Database} from "@/db/schema/06_database";
export type CronButtonProps = {
@@ -1,5 +1,5 @@
import { AdvancedCronSelect } from "./AdvancedCronSelect";
import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/CronButton/cron.action";
import { AdvancedCronSelect } from "./advanced-cron-select";
import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/cron-button/cron.action";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { useRouter } from "next/navigation";
@@ -8,8 +8,8 @@ import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
import { useMutation } from "@tanstack/react-query";
import { TooltipProvider } from "@/components/ui/tooltip";
import { DatabaseSchema, DatabaseType } from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.schema";
import { updateDatabaseAction } from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.action";
import { DatabaseSchema, DatabaseType } from "@/components/wrappers/dashboard/database/database-form/form-database.schema";
import { updateDatabaseAction } from "@/components/wrappers/dashboard/database/database-form/form-database.action";
import { toast } from "sonner";
export type DatabaseFormProps = {
@@ -4,7 +4,7 @@ import { z } from "zod";
import { userAction } from "@/safe-actions";
import { db } from "@/db";
import { eq } from "drizzle-orm";
import { DatabaseSchema } from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.schema";
import { DatabaseSchema } from "@/components/wrappers/dashboard/database/database-form/form-database.schema";
import * as drizzleDb from "@/db";
export const updateDatabaseAction = userAction
@@ -4,6 +4,8 @@ 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 {auth} from "@/lib/auth/auth";
export function OrganizationCombobox() {
const router = useRouter();
@@ -24,11 +26,15 @@ export function OrganizationCombobox() {
};
});
const onValueChange = async (slug: string) => {
await authClient.organization.setActive({
organizationSlug: slug,
});
router.refresh();
};
const handleReset = () => {
@@ -7,7 +7,7 @@ export const OrganizationFormSchema = z.object({
.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'),
members: z.array(z.string()),
users: z.array(z.string()),
});
@@ -1,19 +1,38 @@
"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 { useRouter } from "next/navigation";
import { MultiSelect } from "@/components/wrappers/common/multiselect/multi-select";
import { OrganizationFormSchema, OrganizationFormType } from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema";
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 {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";
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/02_organization";
import {
deleteOrganizationAction,
updateOrganizationAction
} from "@/components/wrappers/dashboard/organization/organization.action";
import {toast} from "sonner";
import {User as BetterAuthUser} from "better-auth";
import {User} from "@/db/schema/01_user";
export type organizationFormProps = {
defaultValues?: OrganizationWithMembers;
members: MemberWithUser[];
users: User[];
currentUser: BetterAuthUser;
};
export const OrganizationForm = (props: organizationFormProps) => {
@@ -21,17 +40,20 @@ export const OrganizationForm = (props: organizationFormProps) => {
const router = useRouter();
const isCreate = !Boolean(props.defaultValues);
const formatUsersList = (members: MemberWithUser[]) => {
const formatUsersList = (users: User[]) => {
return members.map((member) => ({
value: member.user.id,
label: `${member.user.name} | ${member.user.email}`,
}));
return users
.filter((user) => user.id !== props.currentUser.id)
.map((user) => ({
value: user.id,
label: `${user.name} | ${user.email}`,
}));
};
const formatDefaultUsers = (members: MemberWithUser[]): string[] => {
console.log("ici",members);
return members.map((member) => member.userId);
return members
.filter((member) => member.userId !== props.currentUser.id)
.map((member) => member.userId);
};
const formattedDefaultValues = {
@@ -46,21 +68,28 @@ export const OrganizationForm = (props: organizationFormProps) => {
});
const mutation = useMutation({
mutationFn: async (values: OrganizationFormType) => {
console.log(values);
// const organization = await updateOrganizationAction({ data: values, organizationId: props.defaultValues.id });
// console.log(organization);
// if (organization.data.success) {
// // toast.success(organization.data.actionSuccess.message);
// // router.push(`/dashboard/${organization.data.value.slug}/settings`);
// // router.refresh();
// } else {
// // toast.success(organization.data.actionError.message);
// }
mutationFn: (values: OrganizationFormType) => updateOrganizationAction({
data: values,
organizationId: props.defaultValues?.id ?? ""
}),
onSuccess: async (result) => {
if (result?.data?.success) {
toast.success(result.data.actionSuccess?.message || "Organization updated successfully.");
router.push("/dashboard/settings");
} else {
// @ts-ignore
const errorMsg = result?.data?.actionError?.message || result?.data?.actionError?.messageParams?.message || "Failed to update the organization.";
toast.error(errorMsg);
}
},
onError: (error: any) => {
console.error("Mutation network error:", error);
toast.error(error?.message || "A network error occurred.");
},
});
console.log(formatUsersList(props.members))
return (
<Card>
@@ -77,13 +106,13 @@ export const OrganizationForm = (props: organizationFormProps) => {
control={form.control}
name="name"
defaultValue=""
render={({ field }) => (
render={({field}) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Organization 1" {...field} />
</FormControl>
<FormMessage />
<FormMessage/>
</FormItem>
)}
/>
@@ -91,7 +120,7 @@ export const OrganizationForm = (props: organizationFormProps) => {
control={form.control}
name="slug"
defaultValue=""
render={({ field }) => (
render={({field}) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormControl>
@@ -104,19 +133,19 @@ export const OrganizationForm = (props: organizationFormProps) => {
}}
/>
</FormControl>
<FormMessage />
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="users"
render={({ field }) => (
render={({field}) => (
<FormItem>
<FormLabel>Users</FormLabel>
<FormControl>
<MultiSelect
options={formatUsersList(props.members)}
options={formatUsersList(props.users)}
onValueChange={field.onChange}
defaultValue={field.value ?? []}
placeholder="Select users"
@@ -126,7 +155,7 @@ export const OrganizationForm = (props: organizationFormProps) => {
/>
</FormControl>
<FormDescription>Select users you want to add to this organization</FormDescription>
<FormMessage />
<FormMessage/>
</FormItem>
)}
/>
@@ -4,14 +4,14 @@ import { userAction } from "@/safe-actions";
import { OrganizationSchema } from "@/components/wrappers/dashboard/organization/organization.schema";
import { ServerActionResult } from "@/types/action-type";
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { OrganizationFormSchema } from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema";
import { OrganizationFormSchema } from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema";
import { db } from "@/db";
import { eq } from "drizzle-orm";
import {checkSlugOrganization, createOrganization, deleteOrganization} from "@/lib/auth/auth";
import {and, eq, inArray} from "drizzle-orm";
import {auth, checkSlugOrganization, createOrganization, deleteOrganization} from "@/lib/auth/auth";
import {slugify} from "@/utils/slugify";
import {Organization} from "@/db/schema/02_organization";
import * as drizzleDb from "@/db";
import {headers} from "next/headers";
export const createOrganizationAction = userAction.schema(OrganizationSchema).action(async ({ parsedInput }): Promise<ServerActionResult<Organization>> => {
try {
@@ -72,53 +72,117 @@ export const updateOrganizationAction = userAction
organizationId: z.string(),
})
)
.action(async ({ parsedInput }): Promise<ServerActionResult<Organization>> => {
.action(async ({ parsedInput, ctx }): Promise<ServerActionResult<Organization>> => {
try {
const newUserList = parsedInput.data.users;
console.log(parsedInput);
const organization = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.id, parsedInput.organizationId),
with: {
members: true,
}
});
const organization = await db.select().from(drizzleDb.schemas.organization).where(eq(drizzleDb.schemas.organization.id, parsedInput.organizationId)).execute();
if (organization.length === 0) {
throw new Error("Organization not found.");
if (!organization) {
return {
success: false,
actionError: {
message: "Organization not found.",
status: 404,
cause: "not_found",
},
};
}
const existingItemIds = organization[0].users.map((user) => user.userId);
const existingItemIds = organization.members
.filter((member) => member.userId !== ctx.user.id)
.map((member) => member.userId);
const usersToAdd = newUserList.filter((id) => !existingItemIds.includes(id));
const usersToRemove = existingItemIds.filter((id) => !newUserList.includes(id));
if (usersToAdd.length > 0) {
await db
.insert(drizzleDb.schemas.organization)
.values(
usersToAdd.map((userId) => ({
userId,
organizationId: organization[0].id,
for (const userToAdd of usersToAdd) {
await auth.api.addMember({
body: {
userId: userToAdd,
role: "member",
}))
)
.execute();
organizationId: organization.id,
},
});
}
//
// await db
// .insert(drizzleDb.schemas.member)
// .values(
// usersToAdd.map((userId) => ({
// userId,
// organizationId: organization.id,
// role: "member",
// }))
// )
// .execute();
}
if (usersToRemove.length > 0) {
await db.delete().from(userOrganization).where(inArray(userOrganization.userId, usersToRemove)).execute();
await db.delete(drizzleDb.schemas.member).where(inArray(drizzleDb.schemas.member.userId, usersToRemove)).execute();
// TODO : Do not delete, go permission error with better auth
// for (const userToRemove of usersToRemove) {
//
// const memberToRemove = await db.query.member.findFirst({
// where: and(eq(drizzleDb.schemas.member.userId, userToRemove), eq(drizzleDb.schemas.member.organizationId, organization.id)),
// with: {
// user: true
// }
// })
// console.log(memberToRemove)
//
// if (memberToRemove) {
// console.log("ici")
// await auth.api.removeMember({
// body: {
// memberIdOrEmail: memberToRemove.user.email,
// organizationId: organization.id,
// },
// headers: await headers()
// });
// }
//
// }
}
// const updatedOrganization = await auth.api.updateOrganization({
// body: {
// data: {
// name: parsedInput.data.name,
// slug: parsedInput.data.slug,
// },
// organizationId: organization.id,
// },
// headers: await headers(),
// });
const updatedOrganization = await db
.update(organization)
.update(drizzleDb.schemas.organization)
.set({
name: parsedInput.data.name,
slug: parsedInput.data.slug,
})
.where(eq(organization.id, parsedInput.organizationId))
.where(eq(drizzleDb.schemas.organization.id, parsedInput.organizationId))
.returning()
.execute();
return {
success: true,
value: updatedOrganization[0],
value: updatedOrganization as unknown as Organization,
actionSuccess: {
message: "Organization has been successfully updated.",
messageParams: { organizationId: updatedOrganization[0].id },
messageParams: { organizationId: organization.id},
},
};
} catch (error) {
@@ -128,7 +192,7 @@ export const updateOrganizationAction = userAction
actionError: {
message: "Failed to update organization.",
status: 500,
cause: error.message ?? "Unknown error",
cause: "server_error",
messageParams: { message: "Error updating the organization" },
},
};
@@ -4,7 +4,7 @@ import { UploadIcon } from "lucide-react";
import { toast } from "sonner";
import { uploadImageAction } from "@/features/upload/public/upload.action";
import { useMutation } from "@tanstack/react-query";
import { updateImageUserAction } from "@/components/wrappers/dashboard/profile/Avatar/avatar.action";
import { updateImageUserAction } from "@/components/wrappers/dashboard/profile/avatar/avatar.action";
import { useRouter } from "next/navigation";
import { User } from "@/db/schema/01_user";
@@ -1,7 +1,7 @@
"use server";
import { userAction } from "@/safe-actions";
import { z } from "zod";
import { UserSchema } from "@/components/wrappers/dashboard/profile/UserForm/user-form.schema";
import { UserSchema } from "@/components/wrappers/dashboard/profile/user-form/user-form.schema";
import { db } from "@/db";
import { eq } from "drizzle-orm";
import * as drizzleDb from "@/db";
@@ -8,9 +8,9 @@ import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
import { useMutation } from "@tanstack/react-query";
import { TooltipProvider } from "@/components/ui/tooltip";
import { UserSchema, UserType } from "@/components/wrappers/dashboard/profile/UserForm/user-form.schema";
import { UserSchema, UserType } from "@/components/wrappers/dashboard/profile/user-form/user-form.schema";
import { toast } from "sonner";
import { updateUserAction } from "@/components/wrappers/dashboard/profile/UserForm/user-form.action";
import { updateUserAction } from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
export type userFormProps = {
defaultValues?: UserType;
@@ -3,7 +3,7 @@
import { Trash2 } from "lucide-react";
import { ButtonWithConfirm } from "@/components/wrappers/common/button/button-with-confirm";
import { useMutation } from "@tanstack/react-query";
import { deleteProjectAction } from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/delete-project.action";
import { deleteProjectAction } from "@/components/wrappers/dashboard/projects/button-delete-project/delete-project.action";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
@@ -1,7 +1,7 @@
"use server";
import { userAction } from "@/safe-actions";
import { ProjectSchema } from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.schema";
import { ProjectSchema } from "@/components/wrappers/dashboard/projects/project-form/project-form.schema";
import { z } from "zod";
import { ServerActionResult } from "@/types/action-type";
import { db } from "@/db";
@@ -6,8 +6,8 @@ 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/ProjectsForm/ProjectForm.schema";
import { createProjectAction, updateProjectAction } from "@/components/wrappers/dashboard/projects/ProjectsForm/project-form.action";
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 { useRouter } from "next/navigation";
import { MultiSelect } from "@/components/wrappers/common/multiselect/multi-select";
import { toast } from "sonner";
@@ -5,7 +5,7 @@ import {MemberWithUser} from "@/db/schema/02_organization";
import {useState} from "react";
import {useSession} from "@/lib/auth/auth-client";
import {useMutation} from "@tanstack/react-query";
import {updateUserAction} from "@/components/wrappers/dashboard/profile/UserForm/user-form.action";
import {updateUserAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
import {toast} from "sonner";
import {Badge} from "@/components/ui/badge";
@@ -1,19 +0,0 @@
"use client";
import { useSidebar } from "@/components/ui/sidebar";
import { env } from "@/env.mjs";
export type SideBarFooterCreditProps = {};
export const SideBarFooterCredit = (props: SideBarFooterCreditProps) => {
const { state } = useSidebar();
return (
<>
{state === "expanded" && (
<div className="text-center">
<h1 className="text-[10px]">Portabase Community Edition v{env.NEXT_PUBLIC_PROJECT_VERSION}</h1>
</div>
)}
</>
);
};
@@ -1,15 +0,0 @@
"use client"
import {useSidebar} from "@/components/ui/sidebar";
export type SideBarLogoProps = {}
export const SideBarLogo = (props: SideBarLogoProps) => {
const { state, isMobile } = useSidebar();
return (
<div className="flex justify-center items-center ">
<h1 className="font-bold text-xl">
{state === 'collapsed' && !isMobile ? "P" : "Portabase"}
</h1>
</div>
)
}
@@ -1,162 +0,0 @@
"use client";
import { JSX, useEffect, useState } from "react";
import Link from "next/link";
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu as SM,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
import { ChevronDown, MoreHorizontal } from "lucide-react";
import { usePathname } from "next/navigation";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
export type SidebarMenuProps = {
baseUrl: string;
items: SidebarItem[];
};
type SidebarItemContent = {
title: string;
url: string;
icon: JSX.Element;
dropdown?: {
title: string;
url: string;
}[];
};
type SidebarListItem = {
type: "list";
content: SidebarItemContent;
};
type SidebarCollapseItem = {
type: "collapse";
title: string;
content: SidebarItemContent[];
};
export type SidebarItem = SidebarListItem | SidebarCollapseItem;
export const SidebarMenu = (props: SidebarMenuProps) => {
const pathname = usePathname();
useEffect(() => {
const currentUrl = pathname;
const currentItem = props.items.find((item) => {
if (item.type === "list") {
return `${props.baseUrl}/${item.content.url}` === currentUrl;
} else if (item.type === "collapse") {
return item.content.some((content) => `${props.baseUrl}/${content.url}` === currentUrl);
}
return false;
});
if (currentItem) {
if (currentItem.type === "list") {
setActiveItem(currentItem.content.title);
} else if (currentItem.type === "collapse") {
setActiveItem(currentItem.title!);
}
} else {
setActiveItem("");
}
}, [pathname]);
const [activeItem, setActiveItem] = useState("");
const handleItemClick = (title: string) => {
setActiveItem(title);
};
return (
<SM>
{props.items.map((item, index) =>
item.type === "collapse" ? (
<Collapsible defaultOpen key={index} className={`group/collapsible`}>
<SidebarGroup>
<SidebarGroupLabel asChild>
<CollapsibleTrigger>
{item.title ?? "Please define a title"}
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
</CollapsibleTrigger>
</SidebarGroupLabel>
<CollapsibleContent>
<SidebarGroupContent>
{item.content.map((content) => (
<SidebarMenuItem key={content.title}>
<SidebarMenuButton asChild>
<Link
className={cn(
buttonVariants({
size: "lg",
variant: activeItem === content.title ? "secondary" : "ghost",
}),
"justify-start p-0"
)}
href={`${props.baseUrl}/${content.url}`}
onClick={() => handleItemClick(content.title)}
>
{content.icon}
<span>{content.title}</span>
</Link>
</SidebarMenuButton>
{content.dropdown && (
<SidebarMenuAction>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction>
<MoreHorizontal />
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent side="right" align="start">
{content.dropdown.map((dropdown) => (
<Link href={`${props.baseUrl}/${dropdown.url}`} className="justify-start p-0">
<DropdownMenuItem>
<span>{dropdown.title ?? "Please define a title"}</span>
</DropdownMenuItem>
</Link>
))}
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuAction>
)}
</SidebarMenuItem>
))}
</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
) : (
<SidebarMenuItem key={index}>
<SidebarMenuButton asChild>
<Link
className={cn(
buttonVariants({
size: "lg",
variant: activeItem === item.content.title ? "secondary" : "ghost",
}),
"justify-start p-0"
)}
href={`${props.baseUrl}/${item.content.url}`}
onClick={() => handleItemClick(item.content.title)}
>
{item.content.icon}
<span>{item.content.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
)}
</SM>
);
};
@@ -1,54 +0,0 @@
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu as SM,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import {OrganizationCombobox} from "@/components/wrappers/dashboard/organization/organization-combobox";
import {SideBarLogo} from "@/components/wrappers/dashboard/sideBar/SideBarLogo/SideBarLogo";
import {SideBarFooterCredit} from "@/components/wrappers/dashboard/sideBar/SideBarFooterCredit/SideBarFooterCredit";
import {LoggedInButton} from "@/components/wrappers/dashboard/loggedInButton/LoggedInButton";
import {SidebarContentA} from "./sidebar-content";
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import {notFound} from "next/navigation";
export async function AppSidebar() {
const member = await getActiveMember();
console.log("member", member);
if (!member) {
return notFound();
}
// const organization = await getOrganization({organizationId: member.organizationId});
// console.log("organization", organization);
return (
<Sidebar collapsible="icon">
<SidebarHeader>
<SideBarLogo/>
<SM>
<SidebarMenuItem>
<OrganizationCombobox/>
</SidebarMenuItem>
</SM>
</SidebarHeader>
<SidebarContent>
<SidebarContentA/>
</SidebarContent>
<SidebarFooter>
<SM>
<SidebarMenuItem>
<LoggedInButton/>
</SidebarMenuItem>
</SM>
<SideBarFooterCredit/>
</SidebarFooter>
</Sidebar>
);
}
@@ -1,87 +0,0 @@
"use client";
import { SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarContent } from "@/components/ui/sidebar";
import { authClient, useSession } from "@/lib/auth/auth-client";
import { Layers, ChartArea, Settings, ShieldHalf } from "lucide-react";
import { SidebarItem, SidebarMenu } from "./SideBarMenu/SideBarMenu";
export const SidebarContentA = () => {
const { data: activeOrganization } = authClient.useActiveOrganization();
const { data: organizations } = authClient.useListOrganizations();
const { data: session } = useSession();
const member = authClient.useActiveMember(); // Moved here — always called
const appItems: SidebarItem[] = [
{
type: "list",
content: {
title: "Projects",
url: "projects",
icon: <Layers />,
},
},
{
type: "list",
content: {
title: "Statistics",
url: "statistics",
icon: <ChartArea />,
},
},
];
if (activeOrganization && member?.data?.role === "admin" || member?.data?.role === "owner") {
appItems.push({
type: "list",
content: {
title: "Settings",
url: "settings",
icon: <Settings />,
},
});
}
const adminItems: SidebarItem[] = [
{
type: "list",
content: {
title: "Agents",
url: "agents",
icon: <ShieldHalf />,
},
},
{
type: "list",
content: {
title: "Administration panel",
url: "admin",
icon: <Settings />,
},
},
];
return (
activeOrganization && (
<>
<SidebarGroup>
<SidebarGroupLabel>Application</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu items={appItems} baseUrl={`/dashboard`} />
</SidebarGroupContent>
</SidebarGroup>
{(session && (session.user.role === "admin" || session.user.role === "superadmin")) && (
<SidebarGroup>
<SidebarGroupLabel>Administration</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu items={adminItems} baseUrl={`/dashboard`} />
</SidebarGroupContent>
</SidebarGroup>
)}
</>
)
);
};
@@ -154,6 +154,7 @@ CREATE TABLE "agents" (
"slug" text NOT NULL,
"name" text NOT NULL,
"description" text NOT NULL,
"is_archived" boolean DEFAULT false,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp,
"last_contact" timestamp,
@@ -171,5 +172,4 @@ ALTER TABLE "backups" ADD CONSTRAINT "backups_database_id_databases_id_fk" FOREI
ALTER TABLE "databases" ADD CONSTRAINT "databases_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "databases" ADD CONSTRAINT "databases_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "restorations" ADD CONSTRAINT "restorations_backup_id_backups_id_fk" FOREIGN KEY ("backup_id") REFERENCES "public"."backups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "restorations" ADD CONSTRAINT "restorations_database_id_databases_id_fk" FOREIGN KEY ("database_id") REFERENCES "public"."databases"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "database_id_status_unique" ON "backups" USING btree ("database_id","status");
ALTER TABLE "restorations" ADD CONSTRAINT "restorations_database_id_databases_id_fk" FOREIGN KEY ("database_id") REFERENCES "public"."databases"("id") ON DELETE cascade ON UPDATE no action;
-1
View File
@@ -1 +0,0 @@
DROP INDEX "database_id_status_unique";
+9 -24
View File
@@ -1,5 +1,5 @@
{
"id": "3d165821-b11d-4be7-b3fb-83c4485e44a8",
"id": "5d68ff75-f31b-4f6a-a16f-f820fc2234aa",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
@@ -814,29 +814,7 @@
"notNull": true
}
},
"indexes": {
"database_id_status_unique": {
"name": "database_id_status_unique",
"columns": [
{
"expression": "database_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "status",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"indexes": {},
"foreignKeys": {
"backups_database_id_databases_id_fk": {
"name": "backups_database_id_databases_id_fk",
@@ -1090,6 +1068,13 @@
"primaryKey": false,
"notNull": true
},
"is_archived": {
"name": "is_archived",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
File diff suppressed because it is too large Load Diff
+2 -9
View File
@@ -5,15 +5,8 @@
{
"idx": 0,
"version": "7",
"when": 1752684754669,
"tag": "0000_awesome_nomad",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1752689557905,
"tag": "0001_wealthy_leo",
"when": 1753637475061,
"tag": "0000_misty_liz_osborn",
"breakpoints": true
}
]
+2 -1
View File
@@ -1,4 +1,4 @@
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import {boolean, pgTable, text, timestamp, uuid} from "drizzle-orm/pg-core";
import { createSelectSchema } from "drizzle-zod";
import { z } from "zod";
import {database} from "@/db/schema/06_database";
@@ -9,6 +9,7 @@ export const agent = pgTable("agents", {
slug: text("slug").notNull().unique(),
name: text("name").notNull().notNull(),
description: text("description").notNull(),
isArchived: boolean("is_archived").default(false),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at"),
lastContact: timestamp("last_contact"),
+1 -1
View File
@@ -18,9 +18,9 @@ import {createRestorationAction, deleteBackupAction} from "@/features/dashboard/
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {StatusBadge} from "@/components/wrappers/common/status-badge";
import {TooltipCustom} from "@/components/wrappers/common/tooltipCustom/TooltipCustom";
import {Backup} from "@/db/schema/06_database";
import {formatFrenchDate} from "@/utils/date-formatting";
import {TooltipCustom} from "@/components/wrappers/common/tooltip-custom";
export function backupColumns(isAlreadyRestore: boolean): ColumnDef<Backup>[] {

Some files were not shown because too many files have changed in this diff Show More