mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
21
Commits
1.1.2
...
1.1.3-rc.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4ebc876bf | ||
|
|
a435a92d41 | ||
|
|
bcbd6bbea1 | ||
|
|
82559b49bd | ||
|
|
8c1b869004 | ||
|
|
c4a0692985 | ||
|
|
750f1cb145 | ||
|
|
b7d3487c53 | ||
|
|
529f157c6e | ||
|
|
c62be9854a | ||
|
|
9c1aa66b41 | ||
|
|
9894e2582d | ||
|
|
8a8ef81fce | ||
|
|
2fdd9f1296 | ||
|
|
5251e01011 | ||
|
|
84dab35f4b | ||
|
|
ae766dab2f | ||
|
|
027928c29f | ||
|
|
e3239639f3 | ||
|
|
7d6038e5f1 | ||
|
|
2a90d4933d |
@@ -5,9 +5,9 @@ NODE_ENV=production
|
||||
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
|
||||
|
||||
# Projet
|
||||
NEXT_PUBLIC_PROJECT_NAME="Portabase"
|
||||
NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
||||
NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io
|
||||
PROJECT_NAME="Portabase"
|
||||
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
||||
PROJECT_URL=http://app.portabase.io
|
||||
PROJECT_SECRET=
|
||||
|
||||
# SMTP (email)
|
||||
@@ -20,6 +20,7 @@ SMTP_FROM=
|
||||
# Google
|
||||
AUTH_GOOGLE_ID=
|
||||
AUTH_GOOGLE_SECRET=
|
||||
AUTH_GOOGLE_METHOD=
|
||||
|
||||
# S3
|
||||
S3_ENDPOINT=http://app.s3.portabase.io
|
||||
@@ -30,4 +31,7 @@ S3_PORT=9000
|
||||
S3_USE_SSL=true
|
||||
|
||||
# Storage Type (s3, local)
|
||||
STORAGE_TYPE=local
|
||||
STORAGE_TYPE=local
|
||||
|
||||
# Retention
|
||||
RETENTION_CRON="* * * * *"
|
||||
@@ -48,22 +48,3 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.set-tags.outputs.tags }}
|
||||
target: prod
|
||||
|
||||
# Do not delete
|
||||
# - name: Build and push Docker image
|
||||
# id: push
|
||||
# uses: docker/build-push-action@v6
|
||||
# with:
|
||||
# context: .
|
||||
# file: ./docker/dockerfile/Dockerfile
|
||||
# push: true
|
||||
# tags: ${{ steps.meta.outputs.tags }}
|
||||
# labels: ${{ steps.meta.outputs.labels }}
|
||||
# target: prod
|
||||
|
||||
# - name: Generate artifact attestation
|
||||
# uses: actions/attest-build-provenance@v2
|
||||
# with:
|
||||
# subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||
# subject-digest: ${{ steps.push.outputs.digest }}
|
||||
# push-to-registry: true
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
.idea
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
|
||||
Binary file not shown.
@@ -19,7 +19,6 @@
|
||||
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
||||
|
||||

|
||||
|
||||
|
||||
</p>
|
||||
</div>
|
||||
@@ -178,6 +177,9 @@ S3_USE_SSL=true
|
||||
|
||||
# Storage Backend: 'local' or 's3'
|
||||
STORAGE_TYPE=local
|
||||
|
||||
# Retention
|
||||
RETENTION_CRON="* * * * *"
|
||||
```
|
||||
|
||||
### Semantic Versioning
|
||||
|
||||
+9
-38
@@ -1,49 +1,20 @@
|
||||
"use client"
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {LayoutAdmin} from "@/components/layout";
|
||||
import Image from "next/image";
|
||||
import {env} from "@/env.mjs";
|
||||
import {useTheme} from "next-themes";
|
||||
import {useSession} from "@/lib/auth/auth-client";
|
||||
import {useRouter} from "next/navigation";
|
||||
import React from "react";
|
||||
import {redirect} from "next/navigation";
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
import {AuthLogoSection} from "@/components/wrappers/auth/auth-logo-section";
|
||||
|
||||
export default async function Layout({children}: { children: React.ReactNode }) {
|
||||
|
||||
export default function Layout({children}: { children: React.ReactNode }) {
|
||||
const user = await currentUser();
|
||||
|
||||
const { resolvedTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession();
|
||||
|
||||
if (session && session.user && !session.user.banned && session.user.role !== "pending") {
|
||||
router.replace("/dashboard/home");
|
||||
if (user && !user.banned && user.role !== "pending") {
|
||||
redirect("/dashboard/home");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const imageTheme = resolvedTheme === "dark" ? "/images/logo-white.png" : "/images/logo-black.png";
|
||||
|
||||
return (
|
||||
<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">
|
||||
<img
|
||||
className="p-12 text-black dark:text-white"
|
||||
src={imageTheme}
|
||||
// loading="eager"
|
||||
alt="Logo"
|
||||
// width={1024}
|
||||
// height={1024}
|
||||
/>
|
||||
<span
|
||||
className="text-sm text-muted-foreground -ml-12 -mb-12">
|
||||
v{env.NEXT_PUBLIC_PROJECT_VERSION}
|
||||
</span>
|
||||
</div>
|
||||
<AuthLogoSection/>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
<footer className="py-4 text-center text-xs justify-items-end text-muted-foreground">
|
||||
|
||||
+12
-24
@@ -1,32 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {useSearchParams} from "next/navigation";
|
||||
|
||||
import {notFound} from "next/navigation";
|
||||
import {env} from "@/env.mjs";
|
||||
import {LoginForm} from "@/components/wrappers/auth/login/login-form/login-form";
|
||||
import {toast} from "sonner";
|
||||
import {Metadata} from "next";
|
||||
|
||||
export default function SignInPage(props: {
|
||||
searchParams: Promise<{ callbackUrl: string | undefined }>
|
||||
}) {
|
||||
|
||||
const [urlParams, setUrlParams] = useState<URLSearchParams>();
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
setUrlParams(urlParams);
|
||||
const error = urlParams.get("error");
|
||||
console.log(urlParams.get("redirect"));
|
||||
if (error?.includes("pending")) {
|
||||
toast.error("Your account is not active.");
|
||||
urlParams.delete("error");
|
||||
window.history.replaceState({}, document.title, window.location.pathname + "?" + urlParams.toString());
|
||||
}
|
||||
}, []);
|
||||
export const metadata: Metadata = {
|
||||
title: "Login",
|
||||
};
|
||||
|
||||
export default async function SignInPage() {
|
||||
const authGoogleEnabled = env.AUTH_GOOGLE_METHOD;
|
||||
if (!authGoogleEnabled) {
|
||||
notFound()
|
||||
}
|
||||
return (
|
||||
<div className="mx-auto grid w-full gap-6">
|
||||
<LoginForm/>
|
||||
<LoginForm authGoogleEnabled={authGoogleEnabled}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {RegisterForm} from "@/components/wrappers/auth/register/register-form/register-form";
|
||||
import {Metadata} from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Register",
|
||||
};
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import {notFound} from "next/navigation";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page} from "@/features/layout/page";
|
||||
import {OrganizationManagement} from "@/components/wrappers/dashboard/admin/organization/organization-management";
|
||||
import {buildOrganizationWithMembers} from "@/utils/common";
|
||||
import {isUUID} from "@/utils/text";
|
||||
import {user} from "@/db/schema/02_user";
|
||||
import {invitation} from "@/db/schema/05_invitation";
|
||||
import {member} from "@/db/schema/04_member";
|
||||
import {organization} from "@/db/schema/03_organization";
|
||||
import {user as drizzleUser} from "@/db/schema/02_user";
|
||||
|
||||
|
||||
export default async function RoutePage(props: PageParams<{ organizationId: string }>) {
|
||||
const {organizationId} = await props.params;
|
||||
|
||||
if (!organizationId) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (!isUUID(organizationId)) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const users = await db.select().from(drizzleUser);
|
||||
|
||||
const organizationData = await db
|
||||
.select({organization, member, user, invitation})
|
||||
.from(organization)
|
||||
.leftJoin(member, eq(drizzleDb.schemas.organization.id, member.organizationId))
|
||||
.leftJoin(invitation, eq(drizzleDb.schemas.invitation.id, invitation.organizationId))
|
||||
.leftJoin(user, eq(drizzleDb.schemas.member.userId, user.id))
|
||||
.where(eq(organization.id, organizationId));
|
||||
|
||||
const formattedData = buildOrganizationWithMembers(organizationData);
|
||||
|
||||
if (!formattedData) return notFound();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<OrganizationManagement organization={formattedData} users={users}/>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,11 @@ import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {AdminTabs} from "@/components/wrappers/dashboard/admin/admin-tabs";
|
||||
import {db} from "@/db";
|
||||
import {isNull} from "drizzle-orm";
|
||||
import {Metadata} from "next";
|
||||
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Admin",
|
||||
};
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
const users = await db.query.user.findMany({
|
||||
@@ -14,17 +17,28 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
}
|
||||
});
|
||||
|
||||
const organizations = await db.query.organization.findMany({
|
||||
where: (fields) => isNull(fields.deletedAt),
|
||||
with: {
|
||||
members: true,
|
||||
},
|
||||
});
|
||||
|
||||
const settings = await db.query.setting.findFirst({
|
||||
where: (fields, {eq}) => eq(fields.name, "system"),
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>Administration Panel</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<AdminTabs settings={settings!} users={users}/>
|
||||
<AdminTabs
|
||||
organizations={organizations}
|
||||
settings={settings!}
|
||||
users={users}/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -8,17 +8,15 @@ import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||
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 {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {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";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
import {Server} from "lucide-react";
|
||||
|
||||
import {generateEdgeKey} from "@/utils/edge_key";
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{ agentId: string }>) {
|
||||
|
||||
@@ -31,11 +29,11 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
if (!agent) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
@@ -48,7 +46,7 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
||||
</Link>
|
||||
</PageTitle>
|
||||
<PageActions className="justify-between">
|
||||
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"} />
|
||||
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/>
|
||||
</PageActions>
|
||||
</div>
|
||||
<PageDescription className="mt-5 sm:mt-0">{agent.description}</PageDescription>
|
||||
@@ -58,7 +56,7 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
||||
<Card className="w-full sm:w-auto flex-1">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Databases</CardTitle>
|
||||
<Server className="h-4 w-4 text-muted-foreground" />
|
||||
<Server className="h-4 w-4 text-muted-foreground"/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{agent.databases.length}</div>
|
||||
@@ -69,7 +67,7 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
||||
<Card className="w-full sm:w-auto flex-1">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Last contact</CardTitle>
|
||||
<Server className="h-4 w-4 text-muted-foreground" />
|
||||
<Server className="h-4 w-4 text-muted-foreground"/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{formatDateLastContact(agent.lastContact)}</div>
|
||||
@@ -83,7 +81,9 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
||||
Edge Key
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AgentCardKey agent={agent}/>
|
||||
<AgentCardKey
|
||||
edgeKey={edgeKey}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<CardsWithPagination cardsPerPage={2} data={agent.databases} cardItem={DatabaseCard}/>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
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 {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 (
|
||||
@@ -9,7 +14,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
<PageTitle>Create new agent</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<AgentForm />
|
||||
<AgentForm/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
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 {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";
|
||||
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";
|
||||
import {Plus} from "lucide-react";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {eq, not} from "drizzle-orm";
|
||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||
export const dynamic = "force-dynamic";
|
||||
import {Metadata} from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Agents",
|
||||
};
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
@@ -39,7 +40,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
{agents.length > 0 ? (
|
||||
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1} />
|
||||
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1}/>
|
||||
) : (
|
||||
<EmptyStatePlaceholder
|
||||
url={"/dashboard/agents/new"}
|
||||
|
||||
+20
-14
@@ -11,7 +11,7 @@ import {db} from "@/db";
|
||||
import {eq, and, inArray} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {getOrganizationProjectDatabases} from "@/lib/services";
|
||||
import {getOrganization} from "@/lib/auth/auth";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
|
||||
@@ -22,8 +22,9 @@ export default async function RoutePage(props: PageParams<{
|
||||
const {projectId, databaseId} = await props.params;
|
||||
|
||||
const organization = await getOrganization({});
|
||||
const activeMember = await getActiveMember()
|
||||
|
||||
if (!organization) {
|
||||
if (!organization || !activeMember) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
@@ -83,6 +84,9 @@ export default async function RoutePage(props: PageParams<{
|
||||
|
||||
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||
|
||||
const isMember = activeMember?.role === "member";
|
||||
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="justify-between gap-2 sm:flex">
|
||||
@@ -90,17 +94,19 @@ export default async function RoutePage(props: PageParams<{
|
||||
<div className=" w-full md:w-fit">
|
||||
{capitalizeFirstLetter(dbItem.name)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 md:justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Do not delete*/}
|
||||
{/*<EditButton/>*/}
|
||||
<RetentionPolicySheet database={dbItem}/>
|
||||
<CronButton database={dbItem}/>
|
||||
{!isMember && (
|
||||
<div className="flex items-center gap-2 md:justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Do not delete*/}
|
||||
{/*<EditButton/>*/}
|
||||
<RetentionPolicySheet database={dbItem}/>
|
||||
<CronButton database={dbItem}/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PageTitle>
|
||||
</div>
|
||||
|
||||
@@ -108,9 +114,9 @@ export default async function RoutePage(props: PageParams<{
|
||||
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
|
||||
)}
|
||||
<PageContent className="flex flex-col w-full h-full">
|
||||
<DatabaseKpi successRate={successRate} database={dbItem} availableBackups={availableBackups}
|
||||
<DatabaseKpi successRate={successRate} database={dbItem} availableBackups={availableBackups}
|
||||
totalBackups={totalBackups}/>
|
||||
<DatabaseTabs settings={settings} database={dbItem} isAlreadyRestore={isAlreadyRestore}
|
||||
<DatabaseTabs activeMember={activeMember} settings={settings} database={dbItem} isAlreadyRestore={isAlreadyRestore}
|
||||
backups={backups}
|
||||
restorations={restorations}/>
|
||||
</PageContent>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {notFound, redirect} from "next/navigation";
|
||||
|
||||
import {db} from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {getOrganization} from "@/lib/auth/auth";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
|
||||
@@ -24,6 +24,8 @@ export default async function RoutePage(props: PageParams<{
|
||||
} = await props.params;
|
||||
|
||||
const organization = await getOrganization({});
|
||||
const activeMember = await getActiveMember()
|
||||
|
||||
if (!organization) {
|
||||
notFound();
|
||||
}
|
||||
@@ -48,23 +50,27 @@ export default async function RoutePage(props: PageParams<{
|
||||
redirect("/dashboard/projects");
|
||||
}
|
||||
|
||||
const isMember = activeMember?.role === "member";
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="justify-between gap-2 sm:flex">
|
||||
<PageTitle className="flex items-center">
|
||||
{capitalizeFirstLetter(proj.name)}
|
||||
<Link className={buttonVariants({variant: "outline"})} href={`/dashboard/projects/${proj.id}/edit`}>
|
||||
<GearIcon className="w-7 h-7"/>
|
||||
</Link>
|
||||
{!isMember && (
|
||||
<Link className={buttonVariants({variant: "outline"})}
|
||||
href={`/dashboard/projects/${proj.id}/edit`}>
|
||||
<GearIcon className="w-7 h-7"/>
|
||||
</Link>
|
||||
)}
|
||||
</PageTitle>
|
||||
<PageActions className="justify-between">
|
||||
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
||||
</PageActions>
|
||||
{!isMember && (
|
||||
<PageActions className="justify-between">
|
||||
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
||||
</PageActions>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<PageDescription>The list of associated databases</PageDescription>
|
||||
|
||||
<PageContent className="flex flex-col w-full h-full">
|
||||
{proj.databases.length > 0 ? (
|
||||
<CardsWithPagination
|
||||
|
||||
@@ -8,11 +8,12 @@ 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<{ }>) {
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
const organization = await getOrganization({});
|
||||
|
||||
if (!organization ) {
|
||||
if (!organization) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,18 @@ import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/
|
||||
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";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||
import {Metadata} from "next";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{ }>) {
|
||||
export const metadata: Metadata = {
|
||||
title: "Projects",
|
||||
};
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
const organization = await getOrganization({});
|
||||
const activeMember = await getActiveMember()
|
||||
|
||||
if (!organization) {
|
||||
notFound();
|
||||
@@ -28,12 +34,14 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
||||
databases: true,
|
||||
},
|
||||
});
|
||||
const isMember = activeMember?.role === "member";
|
||||
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>Projects</PageTitle>
|
||||
{projects.length > 0 && (
|
||||
{(projects.length > 0 && !isMember) && (
|
||||
<PageActions>
|
||||
<Link href={`/dashboard/projects/new`}>
|
||||
<Button>+ Create Project</Button>
|
||||
@@ -44,13 +52,17 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
||||
|
||||
<PageContent>
|
||||
{projects.length > 0 ? (
|
||||
<CardsWithPagination organizationSlug={organization.slug} data={projects} cardItem={ProjectCard}
|
||||
cardsPerPage={4} numberOfColumns={1}/>
|
||||
) : (
|
||||
<EmptyStatePlaceholder
|
||||
url={"/dashboard/projects/new"}
|
||||
text={"Create new Project"}
|
||||
<CardsWithPagination
|
||||
organizationSlug={organization.slug}
|
||||
data={projects}
|
||||
cardItem={ProjectCard}
|
||||
cardsPerPage={4}
|
||||
numberOfColumns={1}
|
||||
/>
|
||||
) : isMember ? (
|
||||
<EmptyStatePlaceholder text="No project available"/>
|
||||
) : (
|
||||
<EmptyStatePlaceholder url="/dashboard/projects/new" text="Create new Project"/>
|
||||
)}
|
||||
</PageContent>
|
||||
</Page>
|
||||
|
||||
@@ -18,7 +18,6 @@ export default async function RoutePage(props: PageParams<{
|
||||
where: (fields) => isNull(fields.deletedAt)
|
||||
});
|
||||
|
||||
|
||||
if (!user || !users || !organization || organization.slug == "default") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@ import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/edit-
|
||||
import {
|
||||
SettingsOrganizationMembersTable
|
||||
} from "@/components/wrappers/dashboard/settings/settings-organization-members-table";
|
||||
import {Metadata} from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Settings",
|
||||
};
|
||||
|
||||
export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||
const organization = await getOrganization({});
|
||||
@@ -22,10 +26,7 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||
}
|
||||
|
||||
const isMember = activeMember?.role === "member";
|
||||
|
||||
if (isMember) {
|
||||
notFound();
|
||||
}
|
||||
const isOwner = activeMember?.role === "owner";
|
||||
|
||||
return (
|
||||
<Page>
|
||||
@@ -37,14 +38,11 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||
)}
|
||||
</PageTitle>
|
||||
<PageActions>
|
||||
{!isMember && organization.slug !== "default" && (
|
||||
{isOwner && organization.slug !== "default" && (
|
||||
<DeleteOrganizationButton organizationSlug={organization.slug}/>
|
||||
)}
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
{/*<PageDescription>*/}
|
||||
{/* Manage your organization settings.*/}
|
||||
{/*</PageDescription>*/}
|
||||
<PageContent>
|
||||
<SettingsOrganizationMembersTable organization={organization}/>
|
||||
</PageContent>
|
||||
|
||||
@@ -8,7 +8,12 @@ import {db} from "@/db";
|
||||
import {and, 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";
|
||||
import {Building2, DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
|
||||
import {Metadata} from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Statistics",
|
||||
};
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
const organization = await getOrganization({});
|
||||
@@ -45,79 +50,6 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
});
|
||||
|
||||
|
||||
|
||||
// const tomorrow = new Date();
|
||||
// tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
//
|
||||
// const before = new Date();
|
||||
// before.setDate(before.getDate() - 1);
|
||||
//
|
||||
// const backupsEvolution = [
|
||||
// {
|
||||
// id: '22e84aa4-228c-45b3-82ec-846a639cd509',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
// {
|
||||
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
// {
|
||||
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
// {
|
||||
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
// {
|
||||
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
||||
// createdAt: new Date(before)
|
||||
// },
|
||||
// {
|
||||
// id: '6a6106fe-7f45-48eb-a56f-0a1e734126a1',
|
||||
// createdAt: new Date(before)
|
||||
// },
|
||||
// {
|
||||
// id: 'a529d790-502e-4609-ad37-9b1c00c73477',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
// {
|
||||
// id: 'a8c105a8-3e29-423e-b7dd-d3218092cde1',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
// {
|
||||
// id: 'd33aaf4f-8525-4490-addb-12e3c8650d6d',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
// {
|
||||
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
|
||||
// createdAt: new Date(tomorrow)
|
||||
// },
|
||||
// {
|
||||
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
|
||||
// createdAt: new Date(tomorrow)
|
||||
// },
|
||||
// {
|
||||
// id: 'e88a588a-2353-4470-9976-8c3eb2ffc88d',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
// {
|
||||
// id: 'ee11441e-4b41-4c1b-9d91-929565b4204a',
|
||||
// createdAt: new Date()
|
||||
// },
|
||||
//
|
||||
// // Entries with tomorrow's date
|
||||
// {
|
||||
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
|
||||
// createdAt: new Date(tomorrow)
|
||||
// },
|
||||
// {
|
||||
// id: 'c0a8323d-9241-4896-9e64-01e905c24e51',
|
||||
// createdAt: new Date(tomorrow)
|
||||
// }
|
||||
// ];
|
||||
|
||||
|
||||
const backupsRate = await db
|
||||
.select({
|
||||
createdAt: drizzleDb.schemas.backup.createdAt,
|
||||
@@ -130,7 +62,6 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
.orderBy(drizzleDb.schemas.backup.createdAt);
|
||||
|
||||
|
||||
|
||||
const restorationsCountResult = await db
|
||||
.select({
|
||||
count: count(),
|
||||
@@ -139,60 +70,84 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
.where(inArray(drizzleDb.schemas.restoration.databaseId, databaseIds));
|
||||
|
||||
|
||||
|
||||
const restorationsCount = restorationsCountResult[0]?.count ?? 0;
|
||||
const projectsCount = projects.length;
|
||||
const backupsEvolutionCount = backupsEvolution.length;
|
||||
|
||||
|
||||
const sortedBackupsEvolution = backupsEvolution.sort(
|
||||
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||
);
|
||||
|
||||
const Placeholder = ({text}: { text: string }) => (
|
||||
<div className="flex h-48 items-center justify-center text-sm text-muted-foreground">{text}</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>Statistics</PageTitle>
|
||||
<PageTitle>Statistics Overview</PageTitle>
|
||||
</PageHeader>
|
||||
|
||||
<PageContent className="flex flex-col gap-y-4">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<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>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Projects</CardTitle>
|
||||
<Building2 className="h-4 w-4 text-muted-foreground"/>
|
||||
</CardHeader>
|
||||
<CardContent className="text-3xl font-bold">{projectsCount}</CardContent>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{projectsCount}</div>
|
||||
<p className="text-xs text-muted-foreground">Active projects in this organization</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<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>
|
||||
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Backups</CardTitle>
|
||||
<DatabaseBackup className="h-4 w-4 text-muted-foreground"/>
|
||||
</CardHeader>
|
||||
<CardContent className="text-3xl font-bold">{backupsEvolutionCount}</CardContent>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{backupsEvolutionCount}</div>
|
||||
<p className="text-xs text-muted-foreground">Total backups executed across all databases</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<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>
|
||||
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Restorations</CardTitle>
|
||||
<RefreshCcw className="h-4 w-4 text-muted-foreground"/>
|
||||
</CardHeader>
|
||||
<CardContent className="text-3xl font-bold">{restorationsCount}</CardContent>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{restorationsCount}</div>
|
||||
<p className="text-xs text-muted-foreground">Total restoration operations performed</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Evolution of the number of backups</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<EvolutionLineChart data={sortedBackupsEvolution}/>
|
||||
{sortedBackupsEvolution.length > 0 ? (
|
||||
<EvolutionLineChart data={sortedBackupsEvolution}/>
|
||||
) : (
|
||||
<Placeholder text="No backup data available"/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Success rate of backups</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PercentageLineChart data={backupsRate}/>
|
||||
{backupsRate.length > 0 ? (
|
||||
<PercentageLineChart data={backupsRate}/>
|
||||
) : (
|
||||
<Placeholder text="No backup rate data available"/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,11 @@ import {db} from "@/db";
|
||||
import {asc, inArray} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {listOrganizations} from "@/lib/auth/auth";
|
||||
import {Metadata} from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Home",
|
||||
};
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
|
||||
@@ -7,6 +7,11 @@ import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/butto
|
||||
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 {Metadata} from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Profile",
|
||||
};
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
const user = await currentUser();
|
||||
@@ -39,11 +44,8 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
{user.name}
|
||||
<Badge className="ml-3 hidden lg:block">{user.role}</Badge>
|
||||
</PageTitle>
|
||||
{/*<PageActions className="mt-2 hidden sm:block">*/}
|
||||
{/* <ButtonDeleteAccount text="Delete my account"/>*/}
|
||||
{/*</PageActions>*/}
|
||||
</div>
|
||||
<PageContent >
|
||||
<PageContent>
|
||||
<UserForm
|
||||
userId={user.id}
|
||||
sessions={sessions} accounts={accounts}
|
||||
@@ -53,9 +55,6 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
role: user.role ?? undefined,
|
||||
}}
|
||||
/>
|
||||
{/*<div className="mt-4 sm:hidden ">*/}
|
||||
{/* <ButtonDeleteAccount text="Delete my account"/>*/}
|
||||
{/*</div>*/}
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -1,2 +1,51 @@
|
||||
import fs from "node:fs";
|
||||
import forge from "node-forge";
|
||||
|
||||
|
||||
export async function decryptedDump(file: File, aesKeyHex: string, ivHex: string, fileExtension: string ): Promise<File> {
|
||||
const privateKeyPem = fs.readFileSync("private/keys/server_private.pem", "utf8");
|
||||
const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
|
||||
|
||||
// Decrypt AES key with RSA-OAEP
|
||||
const encryptedAesKey = forge.util.hexToBytes(aesKeyHex);
|
||||
const aesKey = privateKey.decrypt(encryptedAesKey, "RSA-OAEP", {
|
||||
md: forge.md.sha256.create(),
|
||||
mgf1: {md: forge.md.sha256.create()},
|
||||
});
|
||||
|
||||
// Read encrypted file content
|
||||
const encryptedBuffer = Buffer.from(await file.arrayBuffer());
|
||||
const iv = forge.util.hexToBytes(ivHex);
|
||||
|
||||
// AES decryption
|
||||
const decipher = forge.cipher.createDecipher("AES-CBC", aesKey);
|
||||
decipher.start({iv});
|
||||
decipher.update(forge.util.createBuffer(encryptedBuffer.toString("binary")));
|
||||
const success = decipher.finish();
|
||||
|
||||
if (!success) {
|
||||
throw new Error("Decryption failed");
|
||||
}
|
||||
|
||||
const decryptedBytes = decipher.output.getBytes();
|
||||
const decryptedBuffer = Buffer.from(decryptedBytes, "binary");
|
||||
|
||||
// Return a File so you can use file.arrayBuffer() later
|
||||
return new File(
|
||||
[decryptedBuffer],
|
||||
file.name.replace(/\.enc$/, fileExtension),
|
||||
{type: "application/octet-stream"}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function getFileExtension(dbType: string) {
|
||||
switch (dbType) {
|
||||
case "postgresql":
|
||||
return ".dump";
|
||||
case "mysql":
|
||||
return ".sql";
|
||||
default:
|
||||
return ".dump";
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,13 @@ import {isUuidv4} from "@/utils/verify-uuid";
|
||||
import {uploadLocalPrivate, uploadS3Private} from "@/features/upload/private/upload.action";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import {eventEmitter} from "../../../events/route";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {db} from "@/db";
|
||||
import {Backup} from "@/db/schema/07_database";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {env} from "@/env.mjs";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {decryptedDump, getFileExtension} from "./helpers";
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
@@ -27,6 +28,8 @@ export async function POST(
|
||||
|
||||
const agentId = (await params).agentId;
|
||||
const formData = await request.formData();
|
||||
const aesKeyHex = formData.get("aes_key") as string;
|
||||
const ivHex = formData.get("iv") as string;
|
||||
const generatedId = formData.get("generatedId") as string | null;
|
||||
const method = formData.get("method") as string | null;
|
||||
|
||||
@@ -102,16 +105,22 @@ export async function POST(
|
||||
if (status === "success") {
|
||||
const file = formData.get("file") as File | null;
|
||||
|
||||
if (!aesKeyHex || !ivHex) {
|
||||
return NextResponse.json({error: "Missing fields"}, {status: 400});
|
||||
}
|
||||
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{error: "File is required for successful backup"},
|
||||
{status: 400}
|
||||
);
|
||||
}
|
||||
|
||||
const fileExtension = getFileExtension(database.dbms)
|
||||
const decryptedFile = await decryptedDump(file, aesKeyHex, ivHex, fileExtension);
|
||||
const uuid = uuidv4();
|
||||
const fileName = `${uuid}.dump`;
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const fileName = `${uuid}${fileExtension}`;
|
||||
const buffer = Buffer.from(await decryptedFile.arrayBuffer());
|
||||
|
||||
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||
if (!settings) {
|
||||
@@ -173,4 +182,5 @@ export async function POST(
|
||||
{status: 500}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import * as drizzleDb from "@/db";
|
||||
import {db} from "@/db";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
|
||||
|
||||
export type BodyResultRestore = {
|
||||
generatedId: string
|
||||
status: string
|
||||
@@ -13,7 +12,6 @@ export type BodyResultRestore = {
|
||||
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
|
||||
|
||||
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{params}: { params: Promise<{ agentId: string }> }
|
||||
@@ -27,7 +25,6 @@ export async function POST(
|
||||
|
||||
console.log(body)
|
||||
|
||||
|
||||
if (!isUuidv4(body.generatedId)) {
|
||||
return NextResponse.json(
|
||||
{error: "generatedId is not a valid uuid"},
|
||||
@@ -71,10 +68,7 @@ export async function POST(
|
||||
|
||||
eventEmitter.emit('modification', {update: true});
|
||||
|
||||
|
||||
return Response.json(response, {status: 200})
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in POST handler:', error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -124,13 +124,12 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
||||
try {
|
||||
|
||||
if (settings.storage == "local") {
|
||||
data = await getFileUrlPresignedLocal(fileName!)
|
||||
data = await getFileUrlPresignedLocal({fileName: fileName!})
|
||||
} else if (settings.storage == "s3") {
|
||||
|
||||
data = await getFileUrlPreSignedS3Action(`backups/${backupToRestore?.database.project?.slug}/${fileName}`);
|
||||
}
|
||||
|
||||
|
||||
if (data?.data?.success) {
|
||||
urlBackup = data.data.value ?? "";
|
||||
} else {
|
||||
|
||||
@@ -20,7 +20,7 @@ export type Body = {
|
||||
|
||||
// Function to test the get file url presigned local
|
||||
export async function GET(request: Request) {
|
||||
const url = await getFileUrlPresignedLocal("d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump")
|
||||
const url = await getFileUrlPresignedLocal({fileName:"d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump"})
|
||||
return Response.json({
|
||||
message: url
|
||||
})
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import {NextResponse} from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
PROJECT_URL: process.env.PROJECT_URL,
|
||||
PROJECT_NAME: process.env.PROJECT_NAME,
|
||||
PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION,
|
||||
});
|
||||
}
|
||||
+22
-12
@@ -1,9 +1,19 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import {EventEmitter} from 'events';
|
||||
import {auth} from "@/lib/auth/auth";
|
||||
import {headers} from "next/headers";
|
||||
import {NextResponse} from "next/server";
|
||||
|
||||
export const eventEmitter = new EventEmitter();
|
||||
|
||||
export async function GET(request: Request) {
|
||||
console.log('GET request received');
|
||||
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers(),
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({error: "Unauthorized"}, {status: 403});
|
||||
}
|
||||
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
@@ -37,13 +47,13 @@ export async function GET(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
console.log('POST request received');
|
||||
const data = await request.json();
|
||||
console.log('Data received:', data);
|
||||
|
||||
// Emit the event to all connected clients
|
||||
eventEmitter.emit('modification', data);
|
||||
|
||||
return new Response('Event sent', { status: 200 });
|
||||
}
|
||||
// export async function POST(request: Request) {
|
||||
// console.log('POST request received');
|
||||
// const data = await request.json();
|
||||
// console.log('Data received:', data);
|
||||
//
|
||||
// // Emit the event to all connected clients
|
||||
// eventEmitter.emit('modification', data);
|
||||
//
|
||||
// return new Response('Event sent', {status: 200});
|
||||
// }
|
||||
@@ -12,16 +12,16 @@ export async function GET(
|
||||
const expires = searchParams.get('expires');
|
||||
const fileName = (await params).fileName
|
||||
|
||||
const privateLocalDir = "private/uploads/files/";
|
||||
const filePath = path.join(privateLocalDir, fileName);
|
||||
const uploadsDir = "private/uploads/files/";
|
||||
const uploadPath = path.join(uploadsDir, fileName);
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return NextResponse.json(
|
||||
{error: 'File not found'},
|
||||
{status: 404}
|
||||
);
|
||||
let filePath = null;
|
||||
if (fs.existsSync(uploadPath)) {
|
||||
filePath = uploadPath;
|
||||
} else {
|
||||
return NextResponse.json({error: "File not found"}, {status: 404})
|
||||
}
|
||||
|
||||
const expectedToken = crypto.createHash('sha256').update(`${fileName}${expires}`).digest('hex');
|
||||
@@ -31,8 +31,8 @@ export async function GET(
|
||||
{status: 403}
|
||||
);
|
||||
}
|
||||
//@ts-ignore
|
||||
const expiresAt = parseInt(expires, 10);
|
||||
|
||||
const expiresAt = parseInt(expires!, 10);
|
||||
if (Date.now() > expiresAt) {
|
||||
return NextResponse.json(
|
||||
{error: 'Signed token expired'},
|
||||
|
||||
@@ -1,38 +1,103 @@
|
||||
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";
|
||||
import fs from "fs/promises";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
function nodeStreamToWebStream(nodeStream: stream.Readable) {
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
nodeStream.on("data", chunk => controller.enqueue(chunk));
|
||||
nodeStream.on("end", () => controller.close());
|
||||
nodeStream.on("error", err => controller.error(err));
|
||||
},
|
||||
cancel() {
|
||||
nodeStream.destroy();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const privateS3ImageDir = "images/";
|
||||
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
req: Request,
|
||||
{params}: { params: Promise<{ fileName: string }> }
|
||||
) {
|
||||
const fileName = (await params).fileName;
|
||||
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
|
||||
.select()
|
||||
.from(drizzleDb.schemas.setting)
|
||||
.where(eq(drizzleDb.schemas.setting.name, "system"))
|
||||
.limit(1);
|
||||
|
||||
if (!settings) throw new Error("System settings not found.");
|
||||
|
||||
const storageType = settings.storage; // "local" or "s3"
|
||||
const ext = fileName.split(".").pop()?.toLowerCase();
|
||||
const contentType =
|
||||
ext === "png"
|
||||
? "image/png"
|
||||
: ext === "jpg" || ext === "jpeg"
|
||||
? "image/jpeg"
|
||||
: ext === "gif"
|
||||
? "image/gif"
|
||||
: ext === "webp"
|
||||
? "image/webp"
|
||||
: "application/octet-stream";
|
||||
|
||||
try {
|
||||
const fileName = (await params).fileName;
|
||||
if (storageType === "local") {
|
||||
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
|
||||
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
const file = await fs.readFile(filePath);
|
||||
|
||||
console.log("fileName", fileName);
|
||||
|
||||
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
|
||||
|
||||
// Check if the file exists
|
||||
try {
|
||||
await fs.access(filePath); // Ensures the file exists
|
||||
} catch {
|
||||
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
||||
return new NextResponse(file, {
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
"Cache-Control": "no-store",
|
||||
"Content-Disposition": `inline; filename="${fileName}"`,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// if not found locally, fallback to S3
|
||||
}
|
||||
}
|
||||
|
||||
// Read the file
|
||||
const fileContent = await fs.readFile(filePath); // Returns a Buffer
|
||||
const exists = await checkFileExistsInBucket({
|
||||
bucketName: env.S3_BUCKET_NAME!,
|
||||
fileName: `${privateS3ImageDir}${fileName}`,
|
||||
});
|
||||
if (!exists) return NextResponse.json({error: "File not found"}, {status: 404});
|
||||
|
||||
return new NextResponse(fileContent, {
|
||||
const nodeStream = await getObjectFromClient({
|
||||
bucketName: env.S3_BUCKET_NAME!,
|
||||
fileName: `${privateS3ImageDir}${fileName}`,
|
||||
});
|
||||
const webStream = nodeStreamToWebStream(nodeStream);
|
||||
|
||||
return new NextResponse(webStream, {
|
||||
headers: {
|
||||
"Content-Disposition": `attachment; filename="${fileName}"`,
|
||||
"Content-Type": "application/octet-stream", // Adjust MIME type as needed
|
||||
"Content-Type": contentType,
|
||||
"Cache-Control": "no-store",
|
||||
"Content-Disposition": `inline; filename="${fileName}"`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error reading file:", error);
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
||||
} catch (err) {
|
||||
console.error("Error streaming image:", err);
|
||||
return NextResponse.json({error: "Error fetching file"}, {status: 500});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export type BodyInit = {
|
||||
initialize: boolean;
|
||||
};
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body: BodyInit = await request.json();
|
||||
|
||||
console.log(body);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: "Initialization successfully done!",
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error in POST initialization:", error);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
+7
-4
@@ -1,16 +1,19 @@
|
||||
import React from "react";
|
||||
import type {Metadata} from "next";
|
||||
import {Inter} from "next/font/google";
|
||||
import "./globals.css";
|
||||
import {Providers} from "./providers";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer";
|
||||
import {inter} from "@/fonts/fonts";
|
||||
|
||||
const inter = Inter({subsets: ["latin"]});
|
||||
const title = process.env.PROJECT_NAME ?? "App Title";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: process.env.NEXT_PUBLIC_PROJECT_NAME ?? "App Title",
|
||||
description: process.env.NEXT_PUBLIC_PROJECT_DESCRIPTION ?? undefined,
|
||||
title: {
|
||||
default: title,
|
||||
template: `%s - ${title}`
|
||||
},
|
||||
description: process.env.PROJECT_DESCRIPTION ?? undefined,
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import BackButton from "@/components/wrappers/common/button/back-button";
|
||||
|
||||
export default async function NotFound() {
|
||||
return(
|
||||
@@ -7,6 +8,7 @@ export default async function NotFound() {
|
||||
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">Not found</h1>
|
||||
<p className="leading-7 [&:not(:first-child)]:mt-6">The content you are trying to view is not available.</p>
|
||||
</div>
|
||||
<BackButton>Go home</BackButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+17
-20
@@ -3,22 +3,20 @@ name: portabase-prod
|
||||
services:
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/dockerfile/Dockerfile
|
||||
target: prod
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: docker/dockerfile/Dockerfile
|
||||
# target: prod
|
||||
image: solucetechnologies/portabase:1.1.3-rc.3
|
||||
ports:
|
||||
- '8887:80'
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
container_name: portabase-app-prod
|
||||
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
@@ -35,20 +33,19 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
|
||||
s3:
|
||||
image: docker.io/bitnami/minio:latest
|
||||
ports:
|
||||
- '9000:9000'
|
||||
- '9001:9001'
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
environment:
|
||||
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
|
||||
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
|
||||
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
|
||||
# s3:
|
||||
# image: docker.io/bitnami/minio:latest
|
||||
# ports:
|
||||
# - '9000:9000'
|
||||
# - '9001:9001'
|
||||
# volumes:
|
||||
# - minio_data:/data
|
||||
# environment:
|
||||
# - MINIO_ROOT_USER=${S3_ACCESS_KEY}
|
||||
# - MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
|
||||
# - MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
|
||||
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
minio_data:
|
||||
# minio_data:
|
||||
|
||||
@@ -1,25 +1,6 @@
|
||||
name: portabase-dev
|
||||
|
||||
services:
|
||||
# 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:
|
||||
@@ -36,38 +17,5 @@ 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
|
||||
|
||||
s3:
|
||||
container_name: s3-portabase-dev
|
||||
image: docker.io/bitnami/minio:latest
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
|
||||
environment:
|
||||
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
|
||||
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
|
||||
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
|
||||
- MINIO_BROWSER=on
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
minio_data:
|
||||
# pgadmin-data:
|
||||
|
||||
@@ -94,9 +94,6 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
COPY --chown=nextjs:nodejs src/db ./src/db
|
||||
|
||||
|
||||
|
||||
|
||||
USER root
|
||||
|
||||
COPY ./docker/entrypoints/app-prod-entrypoint.sh /app/app-prod-entrypoint.sh
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
#npx drizzle-kit generate
|
||||
#npx drizzle-kit migrate
|
||||
#
|
||||
#npm run dev
|
||||
#
|
||||
#exec "$@"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "▶ Running Drizzle codegen..."
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
#echo " ____ __ __ "
|
||||
#echo " / __ \____ _____/ /_____ _/ /_ ____ _________ "
|
||||
#echo " / /_/ / __ \/ ___/ __/ __ / __ \/ __ / ___/ _ \ "
|
||||
#echo " / ____/ /_/ / / / /_/ /_/ / /_/ / /_/ (__ ) __/ "
|
||||
#echo " /_/ \____/_/ \__/\__,_/_.___/\__,_/____/\___/ "
|
||||
#echo " "
|
||||
#echo " Community Edition v1.1.1 "
|
||||
#echo " "
|
||||
|
||||
node server.js
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,16 @@
|
||||
import {defineConfig, globalIgnores} from 'eslint/config'
|
||||
import nextVitals from 'eslint-config-next/core-web-vitals'
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
'.next/**',
|
||||
'out/**',
|
||||
'build/**',
|
||||
'next-env.d.ts',
|
||||
]),
|
||||
])
|
||||
|
||||
export default eslintConfig
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -41,9 +41,6 @@ const nextConfig: NextConfig = {
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
|
||||
+115
-113
@@ -1,115 +1,117 @@
|
||||
{
|
||||
"name": "portabase",
|
||||
"version": "1.1.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 8887",
|
||||
"build": "next build --experimental-build-mode compile",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"email": "email dev",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:drop": "drizzle-kit drop",
|
||||
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@radix-ui/react-accordion": "^1.2.10",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.13",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.6",
|
||||
"@radix-ui/react-avatar": "^1.1.9",
|
||||
"@radix-ui/react-checkbox": "^1.3.1",
|
||||
"@radix-ui/react-collapsible": "^1.1.10",
|
||||
"@radix-ui/react-context-menu": "^2.2.14",
|
||||
"@radix-ui/react-dialog": "^1.1.13",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||
"@radix-ui/react-hover-card": "^1.1.13",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.6",
|
||||
"@radix-ui/react-menubar": "^1.1.14",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.12",
|
||||
"@radix-ui/react-popover": "^1.1.13",
|
||||
"@radix-ui/react-progress": "^1.1.6",
|
||||
"@radix-ui/react-radio-group": "^1.3.6",
|
||||
"@radix-ui/react-scroll-area": "^1.2.8",
|
||||
"@radix-ui/react-select": "^2.2.4",
|
||||
"@radix-ui/react-separator": "^1.1.6",
|
||||
"@radix-ui/react-slider": "^1.3.4",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
"@radix-ui/react-switch": "^1.2.4",
|
||||
"@radix-ui/react-tabs": "^1.1.11",
|
||||
"@radix-ui/react-toast": "^1.2.13",
|
||||
"@radix-ui/react-toggle": "^1.1.8",
|
||||
"@radix-ui/react-toggle-group": "^1.1.9",
|
||||
"@radix-ui/react-tooltip": "^1.2.6",
|
||||
"@react-email/components": "^0.0.41",
|
||||
"@t3-oss/env-nextjs": "^0.13.4",
|
||||
"@tanstack/react-query": "^5.76.1",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@zenstackhq/runtime": "2.14.2",
|
||||
"argon2": "^0.43.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-auth": "1.3.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dockerode": "^4.0.6",
|
||||
"dotenv": "^16.5.0",
|
||||
"drizzle-orm": "^0.43.1",
|
||||
"drizzle-zod": "^0.7.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.510.0",
|
||||
"minio": "^8.0.5",
|
||||
"next": "15.5.2",
|
||||
"next-safe-action": "^7.10.8",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-cron": "^4.2.1",
|
||||
"nodemailer": "^7.0.3",
|
||||
"npm-check-updates": "^18.0.1",
|
||||
"pg": "^8.16.0",
|
||||
"react": "19.1.0",
|
||||
"react-day-picker": "9.7.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-email": "^4.0.13",
|
||||
"react-hook-form": "^7.56.3",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
"react-twc": "^1.4.2",
|
||||
"recharts": "^2.15.3",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vaul": "^1.1.2",
|
||||
"ws": "^8.18.2",
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/react": "^6.0.0",
|
||||
"@tailwindcss/postcss": "^4.1.7",
|
||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||
"@types/node": "^22.15.18",
|
||||
"@types/pg": "^8.15.2",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@zenstackhq/openapi": "^2.14.2",
|
||||
"@zenstackhq/tanstack-query": "^2.14.2",
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-config-next": "15.3.2",
|
||||
"eslint-plugin-tailwindcss": "^3.18.0",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"tsx": "^4.19.4",
|
||||
"tw-animate-css": "^1.2.9",
|
||||
"typescript": "^5.8.3",
|
||||
"zenstack": "2.14.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1"
|
||||
"name": "portabase",
|
||||
"version": "1.1.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 8887",
|
||||
"build": "next build --experimental-build-mode compile",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"email": "email dev",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:drop": "drizzle-kit drop",
|
||||
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@radix-ui/react-accordion": "^1.2.10",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.13",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.6",
|
||||
"@radix-ui/react-avatar": "^1.1.9",
|
||||
"@radix-ui/react-checkbox": "^1.3.1",
|
||||
"@radix-ui/react-collapsible": "^1.1.10",
|
||||
"@radix-ui/react-context-menu": "^2.2.14",
|
||||
"@radix-ui/react-dialog": "^1.1.13",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||
"@radix-ui/react-hover-card": "^1.1.13",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.6",
|
||||
"@radix-ui/react-menubar": "^1.1.14",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.12",
|
||||
"@radix-ui/react-popover": "^1.1.13",
|
||||
"@radix-ui/react-progress": "^1.1.6",
|
||||
"@radix-ui/react-radio-group": "^1.3.6",
|
||||
"@radix-ui/react-scroll-area": "^1.2.8",
|
||||
"@radix-ui/react-select": "^2.2.4",
|
||||
"@radix-ui/react-separator": "^1.1.6",
|
||||
"@radix-ui/react-slider": "^1.3.4",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
"@radix-ui/react-switch": "^1.2.4",
|
||||
"@radix-ui/react-tabs": "^1.1.11",
|
||||
"@radix-ui/react-toast": "^1.2.13",
|
||||
"@radix-ui/react-toggle": "^1.1.8",
|
||||
"@radix-ui/react-toggle-group": "^1.1.9",
|
||||
"@radix-ui/react-tooltip": "^1.2.6",
|
||||
"@react-email/components": "^0.0.41",
|
||||
"@t3-oss/env-nextjs": "^0.13.4",
|
||||
"@tanstack/react-query": "^5.76.1",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@zenstackhq/runtime": "2.14.2",
|
||||
"argon2": "^0.43.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-auth": "1.3.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dockerode": "^4.0.6",
|
||||
"dotenv": "^16.5.0",
|
||||
"drizzle-orm": "^0.43.1",
|
||||
"drizzle-zod": "^0.7.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.510.0",
|
||||
"minio": "^8.0.5",
|
||||
"next": "16.0.0",
|
||||
"next-safe-action": "^7.10.8",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-cron": "^4.2.1",
|
||||
"node-forge": "^1.3.1",
|
||||
"nodemailer": "^7.0.3",
|
||||
"npm-check-updates": "^18.0.1",
|
||||
"pg": "^8.16.0",
|
||||
"react": "^19.2.0",
|
||||
"react-day-picker": "9.7.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-email": "^4.0.13",
|
||||
"react-hook-form": "^7.56.3",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
"react-twc": "^1.4.2",
|
||||
"recharts": "^2.15.3",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vaul": "^1.1.2",
|
||||
"ws": "^8.18.2",
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/react": "^6.0.0",
|
||||
"@tailwindcss/postcss": "^4.1.7",
|
||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||
"@types/node": "^22.15.18",
|
||||
"@types/node-forge": "^1",
|
||||
"@types/pg": "^8.15.2",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@zenstackhq/openapi": "^2.14.2",
|
||||
"@zenstackhq/tanstack-query": "^2.14.2",
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"eslint": "^9.39.0",
|
||||
"eslint-config-next": "^16.0.1",
|
||||
"eslint-plugin-tailwindcss": "^3.18.0",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"tsx": "^4.19.4",
|
||||
"tw-animate-css": "^1.2.9",
|
||||
"typescript": "^5.8.3",
|
||||
"zenstack": "2.14.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1"
|
||||
}
|
||||
|
||||
+14
-28
@@ -1,11 +1,10 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { loggingMiddleware } from "@/middleware/loggingMiddleware";
|
||||
import { errorHandler } from "@/middleware/errorHandler";
|
||||
import { auth } from "@/lib/auth/auth";
|
||||
import { headers } from "next/headers";
|
||||
import { signOut } from "@/lib/auth/auth-client";
|
||||
import {NextRequest, NextResponse} from "next/server";
|
||||
import {loggingMiddleware} from "@/middleware/loggingMiddleware";
|
||||
import {errorHandler} from "@/middleware/errorHandler";
|
||||
import {auth} from "@/lib/auth/auth";
|
||||
import {headers} from "next/headers";
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
export async function proxy(request: NextRequest) {
|
||||
const url = request.nextUrl.clone();
|
||||
const redirectUrl = encodeURIComponent(request.nextUrl.pathname)
|
||||
|
||||
@@ -13,40 +12,33 @@ export async function middleware(request: NextRequest) {
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers(),
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.redirect(new URL(`/login?redirect=${redirectUrl}`, request.url));
|
||||
}
|
||||
|
||||
if (session.user.banned) {
|
||||
signOut();
|
||||
await auth.api.signOut({headers: await headers()});
|
||||
return NextResponse.redirect(new URL("/login?error=banned", request.url));
|
||||
}
|
||||
|
||||
if (session.user.role === "pending") {
|
||||
signOut();
|
||||
await auth.api.signOut({headers: await headers()});
|
||||
return NextResponse.redirect(new URL(`/login?error=pending?redirect=${redirectUrl}`, request.url));
|
||||
}
|
||||
|
||||
if (url.pathname === "/dashboard") {
|
||||
return NextResponse.redirect(new URL(`/dashboard/home`, request.url));
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// Exclude `/api/auth` and its subpaths
|
||||
if (url.pathname.startsWith("/api/auth")) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/api")) {
|
||||
const routeExists = checkRouteExists(url.pathname);
|
||||
// If the route does not exist, return a 404 JSON response
|
||||
if (!routeExists) {
|
||||
return new NextResponse(JSON.stringify({ message: "This API route does not exist.", status: 404 }), {
|
||||
return new NextResponse(JSON.stringify({message: "This API route does not exist.", status: 404}), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {"Content-Type": "application/json"},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -56,31 +48,25 @@ export async function middleware(request: NextRequest) {
|
||||
errorHandler(err);
|
||||
}
|
||||
}
|
||||
// Function to check if the route exists (supports dynamic routes)
|
||||
|
||||
function checkRouteExists(pathname: string) {
|
||||
// Define static and dynamic routes with patterns
|
||||
const routePatterns = [
|
||||
//do not delete
|
||||
// /^\/api\/auth\/\d+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123)
|
||||
// /^\/api\/auth\/\w+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123)
|
||||
// /^\/api\/agent\/healthcheck\/\w+$/, // Dynamic route with an alphanumeric parameter (e.g., /api/user/username)
|
||||
/^\/api\/agent\/[^/]+\/status\/?$/, // Dynamic route for /api/agent/[id]/status
|
||||
/^\/api\/agent\/[^/]+\/status\/?$/,
|
||||
/^\/api\/agent\/[^/]+\/backup\/?$/,
|
||||
/^\/api\/agent\/[^/]+\/restore\/?$/,
|
||||
/^\/api\/files\/[^/]+\/?$/,
|
||||
/^\/api\/images\/[^/]+\/?$/,
|
||||
/^\/api\/events\/?$/,
|
||||
/^\/api\/init\/?$/,
|
||||
/^\/api\/config\/?$/,
|
||||
];
|
||||
return routePatterns.some((pattern) => pattern.test(pathname));
|
||||
}
|
||||
|
||||
export const config = {
|
||||
runtime: "nodejs",
|
||||
matcher: [
|
||||
// '/api/agent/:path*',
|
||||
"/api/:path*",
|
||||
"/dashboard/:path*",
|
||||
"/dashboard",
|
||||
],
|
||||
]
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 175 KiB |
@@ -0,0 +1,191 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useState, useRef, useEffect, forwardRef } from "react"
|
||||
import { Search, X } from "lucide-react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface IEntry {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
interface SearchInputProps {
|
||||
value?: IEntry
|
||||
onChange?: (value: IEntry) => void
|
||||
onSelect?: (value: IEntry) => void
|
||||
name?: string
|
||||
placeholder?: string
|
||||
entries?: IEntry[]
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
||||
(
|
||||
{
|
||||
value: controlledValue,
|
||||
onChange,
|
||||
onSelect,
|
||||
name,
|
||||
placeholder = "Search entries...",
|
||||
entries = [],
|
||||
disabled = false,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [internalValue, setInternalValue] = useState<IEntry | null>(null)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [filteredEntries, setFilteredEntries] = useState<IEntry[]>([])
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
const internalRef = useRef<HTMLInputElement>(null)
|
||||
const listRef = useRef<HTMLUListElement>(null)
|
||||
|
||||
const query = controlledValue?.label ?? internalValue?.label ?? ""
|
||||
const inputRef = (ref as React.RefObject<HTMLInputElement>) || internalRef
|
||||
|
||||
// Filter entries based on query
|
||||
useEffect(() => {
|
||||
if (query.trim()) {
|
||||
const filtered = entries.filter((entry) =>
|
||||
entry.label.toLowerCase().includes(query.toLowerCase()),
|
||||
)
|
||||
setFilteredEntries(filtered)
|
||||
setSelectedIndex(-1)
|
||||
} else {
|
||||
setFilteredEntries([])
|
||||
}
|
||||
}, [query, entries])
|
||||
|
||||
const handleValueChange = (newValue: IEntry | null) => {
|
||||
if (controlledValue === undefined) {
|
||||
setInternalValue(newValue)
|
||||
}
|
||||
if (newValue) {
|
||||
onChange?.(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle keyboard navigation
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (!isOpen || filteredEntries.length === 0) return
|
||||
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
e.preventDefault()
|
||||
setSelectedIndex((prev) =>
|
||||
prev < filteredEntries.length - 1 ? prev + 1 : 0,
|
||||
)
|
||||
break
|
||||
case "ArrowUp":
|
||||
e.preventDefault()
|
||||
setSelectedIndex((prev) =>
|
||||
prev > 0 ? prev - 1 : filteredEntries.length - 1,
|
||||
)
|
||||
break
|
||||
case "Enter":
|
||||
e.preventDefault()
|
||||
if (selectedIndex >= 0) {
|
||||
handleSelect(filteredEntries[selectedIndex])
|
||||
}
|
||||
break
|
||||
case "Escape":
|
||||
setIsOpen(false)
|
||||
setSelectedIndex(-1)
|
||||
inputRef.current?.blur()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelect = (entry: IEntry) => {
|
||||
handleValueChange(entry)
|
||||
onSelect?.(entry)
|
||||
setIsOpen(false)
|
||||
setSelectedIndex(-1)
|
||||
inputRef.current?.blur()
|
||||
}
|
||||
|
||||
const clearSearch = () => {
|
||||
handleValueChange(null)
|
||||
setIsOpen(false)
|
||||
setSelectedIndex(-1)
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("relative w-full", className)}>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
{...props}
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
value={query}
|
||||
disabled={disabled}
|
||||
onChange={(e) => {
|
||||
const newLabel = e.target.value
|
||||
handleValueChange({ value: newLabel, label: newLabel })
|
||||
}}
|
||||
onFocus={() => !disabled && setIsOpen(true)}
|
||||
onBlur={() => {
|
||||
// Delay closing to allow clicking on entries
|
||||
setTimeout(() => setIsOpen(false), 150)
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="pl-10 pr-10"
|
||||
/>
|
||||
{query && !disabled && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={clearSearch}
|
||||
className="absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2 p-0 hover:bg-muted"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Results dropdown */}
|
||||
{isOpen && !disabled && filteredEntries.length > 0 && (
|
||||
<div className="absolute top-full z-50 w-full mt-1 bg-popover border rounded-md shadow-md">
|
||||
<ul ref={listRef} className="max-h-60 overflow-auto py-1" role="listbox">
|
||||
{filteredEntries.map((entry, index) => (
|
||||
<li
|
||||
key={entry.value}
|
||||
role="option"
|
||||
aria-selected={index === selectedIndex}
|
||||
className={cn(
|
||||
"px-3 py-2 text-sm cursor-pointer transition-colors",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
index === selectedIndex && "bg-accent text-accent-foreground",
|
||||
)}
|
||||
onClick={() => handleSelect(entry)}
|
||||
>
|
||||
{entry.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No results message */}
|
||||
{isOpen && !disabled && query && filteredEntries.length === 0 && (
|
||||
<div className="absolute top-full z-50 w-full mt-1 bg-popover border rounded-md shadow-md">
|
||||
<div className="px-3 py-2 text-sm text-muted-foreground">
|
||||
No results found for "{query}"
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
SearchInput.displayName = "SearchInput"
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
import {env} from "@/env.mjs";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {useTheme} from "next-themes";
|
||||
|
||||
|
||||
export const AuthLogoSection = () => {
|
||||
|
||||
const {resolvedTheme} = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
|
||||
const imageTheme = resolvedTheme === "dark" ? "/images/logo-white.png" : "/images/logo-black.png";
|
||||
|
||||
return (
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md flex items-center justify-center space-x-2">
|
||||
<img
|
||||
className="p-12 text-black dark:text-white"
|
||||
src={imageTheme}
|
||||
alt="Logo"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground -ml-12 -mb-12">v{env.NEXT_PUBLIC_PROJECT_VERSION}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { signIn } from "@/lib/auth/auth-client";
|
||||
import { JSX } from "react";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {signIn} from "@/lib/auth/auth-client";
|
||||
import {JSX} from "react";
|
||||
|
||||
export type AuthButtonProps = {
|
||||
providers: SocialProviderType[];
|
||||
callBackURL?: string;
|
||||
};
|
||||
|
||||
export type SocialProviderType = {
|
||||
@@ -41,7 +42,7 @@ export const SocialAuthButton = (props: AuthButtonProps): JSX.Element => {
|
||||
e.preventDefault();
|
||||
void signIn.social({
|
||||
provider: provider.id,
|
||||
callbackURL: "/dashboard/profile",
|
||||
callbackURL: props.callBackURL ?? "/dashboard/profile",
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,23 +1,172 @@
|
||||
// "use client";
|
||||
//
|
||||
// import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||
// import {FormControl, 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 {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/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";
|
||||
// import {useEffect, useState} from "react";
|
||||
//
|
||||
// export type loginFormProps = {
|
||||
// defaultValues?: LoginType;
|
||||
// authGoogleEnabled: boolean;
|
||||
//
|
||||
// };
|
||||
//
|
||||
// export const LoginForm = (props: loginFormProps) => {
|
||||
// const router = useRouter();
|
||||
//
|
||||
// const form = useZodForm({
|
||||
// schema: LoginSchema,
|
||||
// });
|
||||
//
|
||||
// const [urlParams, setUrlParams] = useState<URLSearchParams>();
|
||||
//
|
||||
// useEffect(() => {
|
||||
// const urlParams = new URLSearchParams(window.location.search);
|
||||
// console.log(urlParams);
|
||||
// setUrlParams(urlParams);
|
||||
// const error = urlParams.get("error");
|
||||
// console.log(urlParams.get("redirect"));
|
||||
// if (error?.includes("pending")) {
|
||||
// toast.error("Your account is not active.");
|
||||
// urlParams.delete("error");
|
||||
// window.history.replaceState({}, document.title, window.location.pathname + "?" + urlParams.toString());
|
||||
// }
|
||||
// }, []);
|
||||
//
|
||||
//
|
||||
// const mutation = useMutation({
|
||||
// mutationFn: async (values: LoginType) => {
|
||||
// const {error} = await signIn.email(
|
||||
// {
|
||||
// password: values.password,
|
||||
// email: values.email,
|
||||
// callbackURL: urlParams?.get("redirect") ?? "/dashboard/profile",
|
||||
// }, {
|
||||
// onSuccess: () => {
|
||||
// toast.success("Login success");
|
||||
// },
|
||||
// });
|
||||
// if (error) {
|
||||
// toast.error(error.message);
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
//
|
||||
// const availableProviders: SocialProviderType[] = [];
|
||||
//
|
||||
// if (props.authGoogleEnabled) {
|
||||
// availableProviders.push(
|
||||
// {
|
||||
// id: "google",
|
||||
// name: "Google",
|
||||
// icon: <Icon icon={"logos:google-icon"} width="25" height="25"/>,
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
//
|
||||
// return (
|
||||
// <TooltipProvider>
|
||||
// <Card>
|
||||
// <CardHeader>
|
||||
// <div className="grid gap-2 text-center mb-2">
|
||||
// <h1 className="text-3xl font-bold">Login</h1>
|
||||
// <p className="text-balance text-muted-foreground">Enter your informations below to login</p>
|
||||
// </div>
|
||||
// </CardHeader>
|
||||
// <CardContent>
|
||||
// <Form
|
||||
// form={form}
|
||||
// className="flex flex-col gap-4"
|
||||
// onSubmit={async (values) => {
|
||||
// await mutation.mutateAsync(values);
|
||||
// }}
|
||||
// >
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="email"
|
||||
// defaultValue=""
|
||||
// render={({field}) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Email</FormLabel>
|
||||
// <FormControl>
|
||||
// <Input autoComplete="email webauthn"
|
||||
// placeholder="exemple@portabase.io" {...field} />
|
||||
// </FormControl>
|
||||
// <FormMessage/>
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="password"
|
||||
// defaultValue=""
|
||||
// render={({field}) => (
|
||||
// <FormItem>
|
||||
// <div className="flex items-center">
|
||||
// <FormLabel>Password</FormLabel>
|
||||
// {/* <Link href={"/forgot-password"} className="ml-auto inline-block text-sm underline">
|
||||
// Forgot your password?
|
||||
// </Link>*/}
|
||||
// </div>
|
||||
// <FormControl>
|
||||
// <PasswordInput autoComplete="current-password webauthn"
|
||||
// placeholder="Your password" {...field} />
|
||||
// </FormControl>
|
||||
// <FormMessage/>
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// <Button>Sign in</Button>
|
||||
// <div className="mt-4 text-center text-sm">
|
||||
// Don't have an account?{" "}
|
||||
// <Link href={"/register"} className="underline">
|
||||
// Sign up
|
||||
// </Link>
|
||||
// </div>
|
||||
// </Form>
|
||||
// <SocialAuthButton
|
||||
// callBackURL={urlParams?.get("redirect") ?? "/dashboard/profile"}
|
||||
// providers={availableProviders}/>
|
||||
// </CardContent>
|
||||
// </Card>
|
||||
// </TooltipProvider>
|
||||
// );
|
||||
// };
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { FormControl, 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 { toast } from "sonner";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||
import {FormControl, 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 {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/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";
|
||||
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";
|
||||
import {useEffect, useState} from "react";
|
||||
|
||||
export type loginFormProps = {
|
||||
defaultValues?: LoginType;
|
||||
authGoogleEnabled: boolean;
|
||||
};
|
||||
|
||||
export const LoginForm = (props: loginFormProps) => {
|
||||
@@ -27,27 +176,57 @@ export const LoginForm = (props: loginFormProps) => {
|
||||
schema: LoginSchema,
|
||||
});
|
||||
|
||||
const [urlParams] = useState(() =>
|
||||
new URLSearchParams(typeof window !== "undefined" ? window.location.search : "")
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const error = urlParams.get("error");
|
||||
if (error?.includes("pending")) {
|
||||
toast.error("Your account is not active.");
|
||||
urlParams.delete("error");
|
||||
window.history.replaceState({}, document.title, window.location.pathname + "?" + urlParams.toString());
|
||||
}
|
||||
}, [urlParams]);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: LoginType) => {
|
||||
const { error } = await signIn.email(values, {
|
||||
onSuccess: () => {
|
||||
try {
|
||||
const callbackURL =
|
||||
urlParams.get("redirect")?.startsWith("/")
|
||||
? urlParams.get("redirect")
|
||||
: "/dashboard/profile";
|
||||
|
||||
const {error} = await signIn.email({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
callbackURL: callbackURL ?? "/dashboard/profile",
|
||||
});
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
toast.success("Login success");
|
||||
router.push("/dashboard/profile");
|
||||
},
|
||||
});
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Unexpected client error during login");
|
||||
}
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast.error(err.message || "Client error");
|
||||
},
|
||||
});
|
||||
|
||||
const availableProviders: SocialProviderType[] = [
|
||||
{
|
||||
const availableProviders: SocialProviderType[] = [];
|
||||
|
||||
if (props.authGoogleEnabled) {
|
||||
availableProviders.push({
|
||||
id: "google",
|
||||
name: "Google",
|
||||
icon: <Icon icon={"logos:google-icon"} width="25" height="25" />,
|
||||
},
|
||||
];
|
||||
icon: <Icon icon="logos:google-icon" width="25" height="25"/>,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
@@ -55,7 +234,9 @@ export const LoginForm = (props: loginFormProps) => {
|
||||
<CardHeader>
|
||||
<div className="grid gap-2 text-center mb-2">
|
||||
<h1 className="text-3xl font-bold">Login</h1>
|
||||
<p className="text-balance text-muted-foreground">Enter your informations below to login</p>
|
||||
<p className="text-balance text-muted-foreground">
|
||||
Enter your information below to login
|
||||
</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -70,13 +251,17 @@ export const LoginForm = (props: loginFormProps) => {
|
||||
control={form.control}
|
||||
name="email"
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input autoComplete="email webauthn" placeholder="exemple@portabase.io" {...field} />
|
||||
<Input
|
||||
autoComplete="email"
|
||||
placeholder="example@portabase.io"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -84,30 +269,38 @@ export const LoginForm = (props: loginFormProps) => {
|
||||
control={form.control}
|
||||
name="password"
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center">
|
||||
<FormLabel>Password</FormLabel>
|
||||
{/* <Link href={"/forgot-password"} className="ml-auto inline-block text-sm underline">
|
||||
Forgot your password?
|
||||
</Link>*/}
|
||||
{/* Optional forgot password link */}
|
||||
</div>
|
||||
<FormControl>
|
||||
<PasswordInput autoComplete="current-password webauthn" placeholder="Your password" {...field} />
|
||||
<PasswordInput
|
||||
autoComplete="current-password"
|
||||
placeholder="Your password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button>Sign in</Button>
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
{mutation.isPending ? "Signing in..." : "Sign in"}
|
||||
</Button>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
<Link href={"/register"} className="underline">
|
||||
<Link href="/register" className="underline">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</Form>
|
||||
<SocialAuthButton providers={availableProviders} />
|
||||
|
||||
<SocialAuthButton
|
||||
callBackURL={urlParams.get("redirect") ?? "/dashboard/profile"}
|
||||
providers={availableProviders}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -30,8 +30,8 @@ export const RegisterForm = (props: registerFormProps) => {
|
||||
await signUp.email(values, {
|
||||
onSuccess: () => {
|
||||
toast.success(`Success`);
|
||||
router.push(`/login`);
|
||||
router.refresh();
|
||||
router.push(`/login`);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type BackButtonProps = React.ComponentProps<typeof Button> & {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function BackButton({ children, ...props }: BackButtonProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Button onClick={() => router.back()} aria-label={children?.toString()} {...props}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,66 @@
|
||||
"use client";
|
||||
// "use client";
|
||||
//
|
||||
// import { Button } from "@/components/ui/button";
|
||||
// import { ButtonHTMLAttributes } from "react";
|
||||
// import { Loader2 } from "lucide-react";
|
||||
//
|
||||
// export type VariantButton = {
|
||||
// secondary: string;
|
||||
// default: string;
|
||||
// outline: string;
|
||||
// ghost: string;
|
||||
// link: string;
|
||||
// destructive: string;
|
||||
// };
|
||||
// export type sizeButton = {
|
||||
// default: string;
|
||||
// icon: string;
|
||||
// sm: string;
|
||||
// lg: string;
|
||||
// };
|
||||
//
|
||||
// export type ButtonWithConfirmProps = {
|
||||
// icon?: any;
|
||||
// text: string;
|
||||
// variant?: keyof VariantButton;
|
||||
// className?: string;
|
||||
// onClick: () => void;
|
||||
// isPending?: boolean;
|
||||
// size: keyof sizeButton;
|
||||
// };
|
||||
//
|
||||
// export const ButtonWithLoading = ({
|
||||
// icon,
|
||||
// text,
|
||||
// variant,
|
||||
// className,
|
||||
// onClick,
|
||||
// isPending,
|
||||
// size,
|
||||
// ...props // catch all remaining props
|
||||
// }: ButtonWithConfirmProps & ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
// return (
|
||||
// <Button
|
||||
// onClick={() => {
|
||||
// onClick();
|
||||
// }}
|
||||
// variant={variant ? variant : "default"}
|
||||
// className={className}
|
||||
// {...props} // forward the remaining props to the Button component
|
||||
// size={size || "default"}
|
||||
// >
|
||||
// {isPending && <Loader2 className="animate-spin mr-4" size={16} />}
|
||||
// {text}
|
||||
// <>{icon ? icon : null}</>
|
||||
// </Button>
|
||||
// );
|
||||
// };
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ButtonHTMLAttributes } from "react";
|
||||
'use client'
|
||||
|
||||
import { ButtonHTMLAttributes, ReactNode } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export type VariantButton = {
|
||||
secondary: string;
|
||||
@@ -12,46 +70,46 @@ export type VariantButton = {
|
||||
link: string;
|
||||
destructive: string;
|
||||
};
|
||||
export type sizeButton = {
|
||||
|
||||
export type SizeButton = {
|
||||
default: string;
|
||||
icon: string;
|
||||
sm: string;
|
||||
lg: string;
|
||||
};
|
||||
|
||||
export type ButtonWithConfirmProps = {
|
||||
icon?: any;
|
||||
text: string;
|
||||
export type ButtonWithLoadingProps = {
|
||||
children?: string | ReactNode;
|
||||
icon?: ReactNode;
|
||||
variant?: keyof VariantButton;
|
||||
className?: string;
|
||||
onClick: () => void;
|
||||
onClick?: () => void;
|
||||
isPending?: boolean;
|
||||
size: keyof sizeButton;
|
||||
};
|
||||
size?: keyof SizeButton;
|
||||
} & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
export const ButtonWithLoading = ({
|
||||
icon,
|
||||
text,
|
||||
variant,
|
||||
className,
|
||||
onClick,
|
||||
isPending,
|
||||
size,
|
||||
...props // catch all remaining props
|
||||
}: ButtonWithConfirmProps & ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
icon,
|
||||
children,
|
||||
variant = "default",
|
||||
className,
|
||||
onClick,
|
||||
isPending,
|
||||
size = "default",
|
||||
...rest
|
||||
}: ButtonWithLoadingProps) => {
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClick();
|
||||
}}
|
||||
variant={variant ? variant : "default"}
|
||||
onClick={() => onClick?.()}
|
||||
variant={variant}
|
||||
className={className}
|
||||
{...props} // forward the remaining props to the Button component
|
||||
size={size || "default"}
|
||||
size={size}
|
||||
{...rest}
|
||||
>
|
||||
{isPending && <Loader2 className="animate-spin mr-4" size={16} />}
|
||||
{text}
|
||||
{isPending && <Loader2 className="mr-2 animate-spin" size={16} />}
|
||||
{children && children}
|
||||
<>{icon ? icon : null}</>
|
||||
{/*{icon && <span className="ml-2">{icon}</span>}*/}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,22 +3,29 @@ import {cn} from "@/lib/utils";
|
||||
import {Plus} from "lucide-react";
|
||||
|
||||
type EmptyStatePlaceholderProps = {
|
||||
url: string;
|
||||
url?: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
|
||||
export const EmptyStatePlaceholder = ({url, text}: EmptyStatePlaceholderProps) => {
|
||||
return (
|
||||
<Link
|
||||
href={url}
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-center w-full rounded-2xl border border-dashed border-muted p-6 lg:p-10",
|
||||
"hover:bg-muted/50 transition-colors text-muted-foreground hover:text-primary text-center space-y-2"
|
||||
)}
|
||||
>
|
||||
<Plus className="w-5 h-5 lg:w-6 lg:h-6"/>
|
||||
<span className="text-sm lg:text-base font-medium">{text}</span>
|
||||
</Link>
|
||||
<>{url ?
|
||||
<Link
|
||||
href={url}
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-center w-full rounded-2xl border border-dashed border-muted p-6 lg:p-10",
|
||||
"hover:bg-muted/50 transition-colors text-muted-foreground hover:text-primary text-center space-y-2"
|
||||
)}
|
||||
>
|
||||
<Plus className="w-5 h-5 lg:w-6 lg:h-6"/>
|
||||
<span className="text-sm lg:text-base font-medium">{text}</span>
|
||||
</Link>
|
||||
:
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<p className="text-lg text-muted-foreground">{text}</p>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -1,20 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
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 {SettingsEmailTab} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/settings-email-tab";
|
||||
import {SettingsStorageTab} from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/settings-storage-tab";
|
||||
import {User, UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useRouter, useSearchParams} from "next/navigation";
|
||||
import {AdminUsersTable} from "@/components/wrappers/dashboard/admin/admin-user-tab/admin-user-table";
|
||||
import {AdminUsersTable} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/admin-user-table";
|
||||
import {
|
||||
AdminOrganizationsTable
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-organizations-tab/admin-organizations-table";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
|
||||
export type AdminTabsProps = {
|
||||
users: UserWithAccounts[];
|
||||
settings: Setting;
|
||||
organizations: OrganizationWithMembers[];
|
||||
};
|
||||
|
||||
export const AdminTabs = ({users, settings}: AdminTabsProps) => {
|
||||
export const AdminTabs = ({users, settings, organizations}: AdminTabsProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -35,6 +40,9 @@ export const AdminTabs = ({users, settings}: AdminTabsProps) => {
|
||||
<TabsTrigger className="w-full" value="users">
|
||||
Users
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="w-full" value="organizations">
|
||||
Organizations
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="w-full" value="email">
|
||||
Email
|
||||
</TabsTrigger>
|
||||
@@ -42,10 +50,12 @@ export const AdminTabs = ({users, settings}: AdminTabsProps) => {
|
||||
Storage
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="users">
|
||||
<AdminUsersTable users={users}/>
|
||||
</TabsContent>
|
||||
<TabsContent value="organizations">
|
||||
<AdminOrganizationsTable organizations={organizations}/>
|
||||
</TabsContent>
|
||||
<TabsContent value="email">
|
||||
<SettingsEmailTab settings={settings}/>
|
||||
</TabsContent>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client"
|
||||
|
||||
import {useState} from "react";
|
||||
import {Plus} from "lucide-react";
|
||||
|
||||
import {
|
||||
Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger
|
||||
} from "@/components/ui/dialog";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {AdminOrganizationForm} from "@/components/wrappers/dashboard/admin/organization/admin-organization-form";
|
||||
|
||||
type AdminOrganizationAddModalProps = {}
|
||||
|
||||
|
||||
export const AdminOrganizationAddModal = (props: AdminOrganizationAddModalProps) => {
|
||||
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus/> add
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>add organization</DialogTitle>
|
||||
<DialogDescription>
|
||||
your description
|
||||
</DialogDescription>
|
||||
<AdminOrganizationForm onSuccess={() => setOpen(false)}/>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ErrorContext } from "@better-fetch/fetch";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
||||
import { OrganizationSchema } from "@/components/wrappers/dashboard/admin/organization/organization.schema";
|
||||
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth/auth-client";
|
||||
import { slugify } from "@/utils/slugify";
|
||||
|
||||
type AdminOrganizationFormProps = {
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
export const AdminOrganizationForm = ({ onSuccess }: AdminOrganizationFormProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
const form = useZodForm({ schema: OrganizationSchema });
|
||||
|
||||
const mutationCreateOrganisation = useMutation({
|
||||
mutationFn: async ({ name }: OrganizationSchema) => {
|
||||
const slug = slugify(name);
|
||||
await authClient.organization.checkSlug(
|
||||
{
|
||||
slug: slug,
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
await authClient.organization.create(
|
||||
{
|
||||
name: name,
|
||||
slug: slug,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success("Organization created successfully.");
|
||||
router.refresh();
|
||||
onSuccess?.();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
onError: (error: ErrorContext) => {
|
||||
toast.error(error.error.message);
|
||||
onSuccess?.();
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutationCreateOrganisation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Name of your organization" {...field} value={field.value ?? ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex gap-4 justify-end">
|
||||
<ButtonWithLoading isPending={mutationCreateOrganisation.isPending}>Validate</ButtonWithLoading>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { AdminOrganizationList } from "@/components/wrappers/dashboard/admin/organization/admin-orgnization-list";
|
||||
import { AdminOrganizationAddModal } from "@/components/wrappers/dashboard/admin/organization/admin-organization-add-modal";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
|
||||
type AdminOrganizationSectionProps = {
|
||||
organizations: OrganizationWithMembers[];
|
||||
};
|
||||
|
||||
export const AdminOrganizationSection = ({ organizations }: AdminOrganizationSectionProps) => {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Add a new organization</CardTitle>
|
||||
<CardAction>
|
||||
<AdminOrganizationAddModal />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AdminOrganizationList organizations={organizations} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
"use client"
|
||||
import { DataTable } from "@/components/wrappers/common/table/data-table";
|
||||
import { organizationsListColumns } from "@/components/wrappers/dashboard/admin/organization/table-colums";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
|
||||
type AdminOrganizationListProps = {
|
||||
organizations: OrganizationWithMembers[];
|
||||
};
|
||||
|
||||
export const AdminOrganizationList = ({ organizations }: AdminOrganizationListProps) => {
|
||||
return <DataTable columns={organizationsListColumns()} data={organizations} enablePagination={true} enableSelect={false} />;
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
"use client"
|
||||
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
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";
|
||||
|
||||
export type ButtonDeleteFleetProps = {
|
||||
text?: string;
|
||||
organisationId: string
|
||||
};
|
||||
|
||||
export const ButtonDeleteOrganization = (props: ButtonDeleteFleetProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
const {data: organizations, refetch} = authClient.useListOrganizations();
|
||||
|
||||
|
||||
const mutationDeleteOrganisation = useMutation({
|
||||
mutationFn: () => deleteOrganizationAction({id: props.organisationId}),
|
||||
onSuccess: async (result) => {
|
||||
if (result?.data?.success) {
|
||||
await authClient.organization.setActive({
|
||||
organizationSlug: "default",
|
||||
});
|
||||
toast.success("Organization deleted!");
|
||||
router.refresh()
|
||||
refetch()
|
||||
} else {
|
||||
toast.error("An error occurred.");
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error("network error:", error);
|
||||
toast.error(error?.message || "A network error occurred.");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<ButtonWithConfirm
|
||||
title={props.text ? props.text : ""}
|
||||
description={"Are you sure you want to delete this organization?"}
|
||||
button={{
|
||||
main: {
|
||||
variant: "outline",
|
||||
icon: <Trash2 color="red"/>,
|
||||
},
|
||||
confirm: {
|
||||
className: "w-full",
|
||||
text: "Delete",
|
||||
icon: <Trash2/>,
|
||||
variant: "destructive",
|
||||
onClick: async () => {
|
||||
await mutationDeleteOrganisation.mutateAsync()
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
className: "w-full",
|
||||
text: "Cancel",
|
||||
icon: <Trash2/>,
|
||||
variant: "outline",
|
||||
},
|
||||
}}
|
||||
isPending={mutationDeleteOrganisation.isPending}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
"use server";
|
||||
|
||||
import { ServerActionResult } from "@/types/action-type";
|
||||
import { z } from "zod";
|
||||
import { auth } from "@/lib/auth/auth";
|
||||
import { MemberRoleType } from "@/types/common";
|
||||
import { Member } from "better-auth/plugins/organization";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
|
||||
export const addMemberOrganizationAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
organizationId: z.string(),
|
||||
role: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }): Promise<ServerActionResult<Member | null>> => {
|
||||
try {
|
||||
const data = await auth.api.addMember({
|
||||
body: {
|
||||
userId: parsedInput.userId,
|
||||
role: parsedInput.role as MemberRoleType,
|
||||
organizationId: parsedInput.organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: data,
|
||||
actionSuccess: {
|
||||
message: "Member added successfully",
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "An error occurred while addinng member",
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
|
||||
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
||||
import {
|
||||
AddMemberSchema,
|
||||
AddMemberSchemaType
|
||||
} from "@/components/wrappers/dashboard/admin/organization/organization.schema";
|
||||
import {SearchInput} from "@/components/ui/search-input";
|
||||
import {
|
||||
addMemberOrganizationAction
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/add-member.action";
|
||||
import {toast} from "sonner";
|
||||
import {OrganizationWithMembers, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
|
||||
type OrganizationAddMemberFormProps = {
|
||||
onSuccessAction?: () => void;
|
||||
users: User[];
|
||||
organization: OrganizationWithMembersAndUsers;
|
||||
};
|
||||
|
||||
export const OrganizationAddMemberForm = ({onSuccessAction, users, organization}: OrganizationAddMemberFormProps) => {
|
||||
|
||||
const organizationMemberUserIds = organization.members.map((member) => member.user.id);
|
||||
const filteredUsers = users
|
||||
.filter((user) => !organizationMemberUserIds.includes(user.id))
|
||||
.map((user) => ({value: user.id, label: `${user.name} | ${user.email}`}));
|
||||
const router = useRouter();
|
||||
const form = useZodForm({schema: AddMemberSchema});
|
||||
|
||||
const mutationAddMemberOrganisation = useMutation({
|
||||
mutationFn: async (data: AddMemberSchemaType) => {
|
||||
console.log(data);
|
||||
const result = await addMemberOrganizationAction({
|
||||
userId: data.userId,
|
||||
organizationId: organization.id,
|
||||
role: "member",
|
||||
});
|
||||
console.log(result);
|
||||
toast.success("Member successfully added!");
|
||||
router.refresh();
|
||||
onSuccessAction?.();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
onSuccessAction?.();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutationAddMemberOrganisation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="userId"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>User</FormLabel>
|
||||
<FormControl>
|
||||
<SearchInput
|
||||
name="userId"
|
||||
placeholder="Enter a user email"
|
||||
entries={filteredUsers}
|
||||
onSelect={(entySelected: any) => {
|
||||
console.log("Form selection:", entySelected);
|
||||
field.onChange(entySelected.value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-4 justify-end">
|
||||
<ButtonWithLoading
|
||||
isPending={mutationAddMemberOrganisation.isPending}>Confirm</ButtonWithLoading>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { OrganizationAddMemberForm } from "@/components/wrappers/dashboard/admin/organization/details/organization-add-member-form";
|
||||
import { useState } from "react";
|
||||
import { UserPlus } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {OrganizationWithMembers, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
|
||||
type OrganizationAddMemberModalProps = {
|
||||
users: User[];
|
||||
organization: OrganizationWithMembersAndUsers;
|
||||
};
|
||||
|
||||
export const OrganizationAddMemberModal = ({ users, organization }: OrganizationAddMemberModalProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<UserPlus className="w-4 h-4 mr-2" />
|
||||
Add member
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add member to your organization</DialogTitle>
|
||||
<DialogDescription>Select a user to add to your organization</DialogDescription>
|
||||
</DialogHeader>
|
||||
<OrganizationAddMemberForm users={users} organization={organization} onSuccessAction={() => setOpen(!open)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { authClient } from "@/lib/auth/auth-client";
|
||||
import { toast } from "sonner";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
|
||||
type OrganizationDeleteMemberModalProps = {
|
||||
open: boolean;
|
||||
member: MemberWithUser;
|
||||
onOpenChangeAction: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export const OrganizationDeleteMemberModal = ({ member, open, onOpenChangeAction }: OrganizationDeleteMemberModalProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await authClient.organization.removeMember(
|
||||
{
|
||||
memberIdOrEmail: member.id,
|
||||
organizationId: member.organizationId,
|
||||
},
|
||||
{
|
||||
onSuccess: async (response) => {
|
||||
console.log(response);
|
||||
toast.success("Member successfully deleted!");
|
||||
onOpenChangeAction(false);
|
||||
router.refresh();
|
||||
},
|
||||
onError: async (error) => {
|
||||
console.log(error);
|
||||
toast.error("An error occurred while deleting member!");
|
||||
onOpenChangeAction(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChangeAction}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure you want to delete {member.user.name } ?</AlertDialogTitle>
|
||||
<AlertDialogDescription>This action is irreversible: it will permanently delete this member’s data.</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<ButtonWithLoading onClick={async () => await mutation.mutateAsync()}>Validate</ButtonWithLoading>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {MoreHorizontal, Settings, Trash2} from "lucide-react";
|
||||
import {
|
||||
OrganizationDeleteMemberModal
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/organization-delete-member-modal";
|
||||
import {useState} from "react";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {
|
||||
OrganizationMemberChangeRoleModal
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/organization-member-change-role";
|
||||
import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
|
||||
type OrganizationMemberCardProps = {
|
||||
member: MemberWithUser;
|
||||
organization: OrganizationWithMembersAndUsers;
|
||||
};
|
||||
|
||||
export const OrganizationMemberCard = ({member, organization}: OrganizationMemberCardProps) => {
|
||||
|
||||
const [isModalDeleteOpen, setIsModalDeleteOpen] = useState(false);
|
||||
const [isModalRoleOpen, setIsModalRoleOpen] = useState(false);
|
||||
const {data: session, isPending, error} = authClient.useSession();
|
||||
|
||||
if (isPending || error) return null;
|
||||
const isCurrentUser = session?.user?.id === member.user.id;
|
||||
const isOwner = member?.role === "owner";
|
||||
|
||||
return (
|
||||
<div key={member.id}
|
||||
className="flex flex-col md:flex-row md:items-center justify-between p-4 border rounded-lg">
|
||||
<OrganizationDeleteMemberModal member={member} open={isModalDeleteOpen}
|
||||
onOpenChangeAction={setIsModalDeleteOpen}/>
|
||||
<OrganizationMemberChangeRoleModal member={member} open={isModalRoleOpen}
|
||||
onOpenChangeAction={setIsModalRoleOpen}/>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar>
|
||||
<AvatarImage src={member.user.image || ""} alt={member.user.name}/>
|
||||
<AvatarFallback>
|
||||
{member.user.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-medium">{member.user.name}</div>
|
||||
<div className="text-sm text-muted-foreground">{member.user.email}</div>
|
||||
<div
|
||||
className="text-xs text-muted-foreground">Joined {new Date(member.createdAt).toLocaleDateString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-4 md:mt-0">
|
||||
<Badge variant={getRoleBadgeVariant(member.role)}>{member.role}</Badge>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreHorizontal className="w-4 h-4"/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onSelect={() => setIsModalRoleOpen(true)}>
|
||||
<Settings className="w-4 h-4 mr-2"/>
|
||||
Change role
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator/>
|
||||
<DropdownMenuItem onSelect={() => setIsModalDeleteOpen(true)} className="text-red-600">
|
||||
<Trash2 className="w-4 h-4 mr-2"/>
|
||||
Remove member
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getRoleBadgeVariant = (role: string) => {
|
||||
switch (role.toLowerCase()) {
|
||||
case "owner":
|
||||
return "default";
|
||||
case "admin":
|
||||
return "secondary";
|
||||
default:
|
||||
return "outline";
|
||||
}
|
||||
};
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import {useState} from "react";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {toast} from "sonner";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {MemberRoleType} from "@/types/common";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
import {updateMemberRoleAction} from "@/components/wrappers/dashboard/settings/update-member.action";
|
||||
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema";
|
||||
import {
|
||||
updateMemberRoleAdminAction
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/role-member.action";
|
||||
|
||||
type OrganizationMemberChangeRoleModalProps = {
|
||||
open: boolean;
|
||||
member: MemberWithUser;
|
||||
onOpenChangeAction: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export const OrganizationMemberChangeRoleModal = (props: OrganizationMemberChangeRoleModalProps) => {
|
||||
const {member, open, onOpenChangeAction} = props;
|
||||
|
||||
const router = useRouter();
|
||||
const [role, setRole] = useState<MemberRoleType>(member.role as MemberRoleType);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateMemberRoleAdminAction({
|
||||
memberId: member.id,
|
||||
organizationId: member.organizationId,
|
||||
role: RoleSchemaMember.parse(role),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success("Member successfully updated");
|
||||
onOpenChangeAction(false);
|
||||
router.refresh();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error);
|
||||
toast.error("An error occurred while updating member");
|
||||
onOpenChangeAction(false);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChangeAction}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Change the user’s role</DialogTitle>
|
||||
<DialogDescription>Modify the role of this user within your organization.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Select defaultValue={member.role ?? ""} onValueChange={(role) => setRole(role as MemberRoleType)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Sélectionnez un rôle"/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="member">Member</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
<SelectItem value="owner">Owner</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DialogFooter>
|
||||
<div className="flex gap-4 justify-end">
|
||||
<ButtonWithLoading
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onOpenChangeAction(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</ButtonWithLoading>
|
||||
<ButtonWithLoading
|
||||
isPending={mutation.isPending}
|
||||
onClick={async () => {
|
||||
await mutation.mutateAsync();
|
||||
}}
|
||||
>
|
||||
Validate
|
||||
</ButtonWithLoading>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
"use server";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {Member} from "better-auth/plugins";
|
||||
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema";
|
||||
import {db as dbClient} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
|
||||
|
||||
export const updateMemberRoleAdminAction = userAction.schema(
|
||||
z.object({
|
||||
memberId: z.string(),
|
||||
organizationId: z.string(),
|
||||
role: RoleSchemaMember,
|
||||
})
|
||||
).action(async ({parsedInput}): Promise<ServerActionResult<Member>> => {
|
||||
try {
|
||||
|
||||
const [updatedMember] = await dbClient
|
||||
.update(drizzleDb.schemas.member)
|
||||
.set(withUpdatedAt({
|
||||
role: parsedInput.role as string,
|
||||
}))
|
||||
.where(and(eq(drizzleDb.schemas.member.id, parsedInput.memberId), eq(drizzleDb.schemas.member.organizationId, parsedInput.organizationId)))
|
||||
.returning();
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: updatedMember,
|
||||
actionSuccess: {
|
||||
message: "Member has been successfully updated.",
|
||||
messageParams: {},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to update member role.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
|
||||
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
||||
import {
|
||||
UpdateOrganizationSchema,
|
||||
UpdateOrganizationSchemaType
|
||||
} from "@/components/wrappers/dashboard/admin/organization/organization.schema";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
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";
|
||||
|
||||
type UpdateOrganizationFormProps = {
|
||||
onSuccessAction?: () => void;
|
||||
defaultValues: OrganizationWithMembersAndUsers;
|
||||
};
|
||||
|
||||
export const UpdateOrganizationForm = ({onSuccessAction, defaultValues}: UpdateOrganizationFormProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
const {data: organizations, refetch} = authClient.useListOrganizations();
|
||||
|
||||
const isDefaultOrganization = defaultValues.slug == "default";
|
||||
|
||||
const form = useZodForm({
|
||||
schema: UpdateOrganizationSchema,
|
||||
defaultValues: defaultValues,
|
||||
disabled: isDefaultOrganization,
|
||||
});
|
||||
|
||||
|
||||
const mutationUpdateOrganisation = useMutation({
|
||||
mutationFn: ({name}: UpdateOrganizationSchemaType) => updateOrganizationAction({
|
||||
data: {
|
||||
name: name,
|
||||
users: [],
|
||||
slug: defaultValues.slug
|
||||
},
|
||||
organizationId: defaultValues.id,
|
||||
}),
|
||||
onSuccess: async (result) => {
|
||||
if (result?.data?.success) {
|
||||
toast.success("Organization updated successfully.");
|
||||
router.refresh();
|
||||
refetch()
|
||||
} 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.");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutationUpdateOrganisation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="" {...field} value={field.value ?? ""}/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-4 justify-end">
|
||||
<ButtonWithLoading disabled={isDefaultOrganization} isPending={mutationUpdateOrganisation.isPending}>Validate</ButtonWithLoading>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import {Building2, Shield, Users} from "lucide-react";
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
import {
|
||||
UpdateOrganizationForm
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/update-organization-form";
|
||||
import {
|
||||
OrganizationMemberCard
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/organization-member-card";
|
||||
import {useRouter, useSearchParams} from "next/navigation";
|
||||
import {useEffect, useState} from "react";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
import {
|
||||
OrganizationAddMemberModal
|
||||
} from "@/components/wrappers/dashboard/admin/organization/details/organization-add-member-modal";
|
||||
import {cn} from "@/lib/utils";
|
||||
|
||||
type OrganizationManagementProps = {
|
||||
organization: OrganizationWithMembersAndUsers;
|
||||
users: User[];
|
||||
};
|
||||
|
||||
export const OrganizationManagement = ({organization, users}: OrganizationManagementProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [tab, setTab] = useState<string>(() => searchParams.get("tab") ?? "members");
|
||||
|
||||
useEffect(() => {
|
||||
const newTab = searchParams.get("tab") ?? "members";
|
||||
setTab(newTab);
|
||||
}, [searchParams]);
|
||||
|
||||
const handleChangeTab = (value: string) => {
|
||||
router.push(`?tab=${value}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className=" space-y-8">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center justify-center w-12 h-12 dark:bg-gray-700 bg-gray-100 rounded-lg">
|
||||
<Building2 className="w-6 h-6 "/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{capitalizeFirstLetter(organization.name)}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-3 md:mt-0">
|
||||
<OrganizationAddMemberModal organization={organization} users={users}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Members</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground"/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{organization.members.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Number of members</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Administrators</CardTitle>
|
||||
<Shield className="h-4 w-4 text-muted-foreground"/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div
|
||||
className="text-2xl font-bold">{organization.members.filter((m) => m.role === "admin" || m.role === "owner").length}</div>
|
||||
<p className="text-xs text-muted-foreground">With admin roles</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<Tabs className="space-y-6" value={tab} onValueChange={handleChangeTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="members">Members</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="members" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Organization members</CardTitle>
|
||||
<CardDescription>Manage who has access to your organization and their
|
||||
roles.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{organization.members.map((member: MemberWithUser) => (
|
||||
<OrganizationMemberCard key={member.id} member={member}
|
||||
organization={organization}/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="settings" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Settings</CardTitle>
|
||||
<CardDescription>Organization configuration settings.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<UpdateOrganizationForm defaultValues={organization}/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const AddMemberSchema = z.object({
|
||||
userId: z.string().min(1, "Invalid field"),
|
||||
});
|
||||
|
||||
export const UpdateOrganizationSchema = z.object({
|
||||
name: z.string().min(5),
|
||||
});
|
||||
|
||||
export const OrganizationSchema = z.object({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export const OrganizationInvitationSchema = z.object({
|
||||
email: z.string(),
|
||||
invitedByUsername: z.string(),
|
||||
invitedByEmail: z.string(),
|
||||
teamName: z.string(),
|
||||
inviteLink: z.string()
|
||||
});
|
||||
|
||||
export type OrganizationInvitationType = z.infer<typeof OrganizationInvitationSchema>;
|
||||
export type OrganizationSchema = z.infer<typeof OrganizationSchema>;
|
||||
export type UpdateOrganizationSchemaType = z.infer<typeof UpdateOrganizationSchema>;
|
||||
export type AddMemberSchemaType = z.infer<typeof AddMemberSchema>;
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
import {ColumnDef} from "@tanstack/react-table";
|
||||
import {ButtonDeleteOrganization} from "@/components/wrappers/dashboard/admin/organization/button-delete-organization";
|
||||
import Link from "next/link";
|
||||
import {Settings} from "lucide-react";
|
||||
import {buttonVariants} from "@/components/ui/button";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
|
||||
export function organizationsListColumns(): ColumnDef<OrganizationWithMembers>[] {
|
||||
|
||||
return [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
},
|
||||
{
|
||||
accessorKey: "members",
|
||||
header: "Members",
|
||||
cell: ({row}) => {
|
||||
const membersCount = row.original.members?.length;
|
||||
return <div className="flex items-center gap-3">{membersCount}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
id: "actions",
|
||||
cell: ({row}) => {
|
||||
const isDefaultOrganization = row.original.slug == "default";
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{!isDefaultOrganization && (
|
||||
<ButtonDeleteOrganization organisationId={row.original.id}/>
|
||||
)}
|
||||
<Link className={buttonVariants({variant: "outline"})}
|
||||
href={`admin/organization/${row.original.id}`}>
|
||||
<Settings/>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
import { z } from "zod";
|
||||
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema";
|
||||
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
+2
-2
@@ -19,11 +19,11 @@ import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import {
|
||||
EmailFormSchema,
|
||||
EmailFormType
|
||||
} from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema";
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/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";
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
|
||||
+12
-13
@@ -1,13 +1,13 @@
|
||||
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";
|
||||
import { sendEmail } from "@/utils/email-helper";
|
||||
import TestEmailSettings from "../../../../../../emails/TestEmailSettings";
|
||||
import { render } from "@react-email/render";
|
||||
import { toast } from "sonner";
|
||||
import {EmailForm} from "@/components/wrappers/dashboard/admin/tabs/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";
|
||||
import {sendEmail} from "@/utils/email-helper";
|
||||
import {render} from "@react-email/render";
|
||||
import {toast} from "sonner";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {EmailFormType} from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema";
|
||||
import {EmailFormType} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
||||
import TestEmailSettings from "../../../../../../../emails/TestEmailSettings";
|
||||
|
||||
export type SettingsEmailTabProps = {
|
||||
settings: Setting;
|
||||
@@ -47,14 +47,13 @@ export const SettingsEmailTab = (props: SettingsEmailTabProps) => {
|
||||
onClick={async () => {
|
||||
await handleSendMailTest();
|
||||
}}
|
||||
icon={<Send />}
|
||||
text="Send email test"
|
||||
icon={<Send/>}
|
||||
size="default"
|
||||
/>
|
||||
>Send email test</ButtonWithLoading>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings as EmailFormType : undefined } />
|
||||
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings as EmailFormType : undefined}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
"use client"
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {AdminOrganizationList} from "@/components/wrappers/dashboard/admin/organization/admin-orgnization-list";
|
||||
|
||||
export type AdminOrganizationsTableProps = {
|
||||
organizations: OrganizationWithMembers[];
|
||||
|
||||
};
|
||||
|
||||
export const AdminOrganizationsTable = (props: AdminOrganizationsTableProps) => {
|
||||
const {organizations} = props;
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4 h-full py-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Active organizations</CardTitle>
|
||||
<CardDescription>Manage all system organizations</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AdminOrganizationList organizations={organizations} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
"use client"
|
||||
import {ColumnDef} from "@tanstack/react-table";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
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/button-delete-account/delete-account.action";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||
import {formatFrenchDate} from "@/utils/date-formatting";
|
||||
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
||||
import {ButtonDeleteUser} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/button-delete-use";
|
||||
import {Organization} from "@/db/schema/03_organization";
|
||||
|
||||
export const organizationsColumnsAdmin: ColumnDef<Organization>[] = [
|
||||
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
},
|
||||
|
||||
// {
|
||||
// header: "Action",
|
||||
// id: "actions",
|
||||
// cell: ({row}) => {
|
||||
// const router = useRouter();
|
||||
// const {data: session, isPending} = useSession();
|
||||
// const isSuperAdmin = session?.user.role == "superadmin";
|
||||
//
|
||||
// return (
|
||||
// <ButtonDeleteUser
|
||||
// disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
|
||||
// userId={row.original.id}/>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
];
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Download} from "lucide-react";
|
||||
import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action";
|
||||
import {toast} from "sonner";
|
||||
|
||||
export type AdminSettingsTabProps = {};
|
||||
|
||||
export const AdminSettingsTab = (props: AdminSettingsTabProps) => {
|
||||
|
||||
const handleDownloadKey = async () => {
|
||||
|
||||
let url: string = "";
|
||||
const data = await getFileUrlPresignedLocal({dir: "private/keys/", fileName: "server_public.pem"})
|
||||
if (data?.data?.success) {
|
||||
url = data.data.value ?? "";
|
||||
} else {
|
||||
// @ts-ignore
|
||||
const errorMessage = data?.data?.actionError?.message || "Failed to get file!";
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
window.open(url, "_self");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4 h-full py-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Instance settings</CardTitle>
|
||||
<CardDescription>Manage portabase settings</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Download Public Key</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Used for encrypting communications with this instance.
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={handleDownloadKey} variant="outline" size="sm">
|
||||
<Download className="h-4 w-4 mr-2"/>
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+5
-7
@@ -2,7 +2,7 @@ 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/admin-storage-tab/storage-s3/storage-s3-form";
|
||||
import {StorageS3Form} from "@/components/wrappers/dashboard/admin/tabs/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";
|
||||
@@ -11,9 +11,9 @@ import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {
|
||||
updateStorageSettingsAction
|
||||
} from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.action";
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.action";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {S3FormType} from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.schema";
|
||||
import {S3FormType} from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.schema";
|
||||
|
||||
export type SettingsStorageTabProps = {
|
||||
settings: Setting;
|
||||
@@ -70,7 +70,7 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
||||
<AlertDescription>
|
||||
Actually you can only store you data in one place : s3 compatible or in local. For exemple you
|
||||
cannot choose to store images in one place
|
||||
and .dump files in another.
|
||||
and backups files in another.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="flex flex-col h-full py-4 ">
|
||||
@@ -93,9 +93,7 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
||||
onClick={async () => {
|
||||
await mutation.mutateAsync();
|
||||
}}
|
||||
icon={<ShieldCheck/>}
|
||||
text="Test connexion"
|
||||
/>
|
||||
icon={<ShieldCheck/>}>Test connexion</ButtonWithLoading>
|
||||
</div>
|
||||
</div>
|
||||
{isSwitched && (
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { db } from "@/db";
|
||||
import { S3FormSchema, StorageSwitchSchema } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.schema";
|
||||
import { S3FormSchema, StorageSwitchSchema } from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
+2
-2
@@ -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/admin-storage-tab/storage-s3/s3-form.schema";
|
||||
import { S3FormSchema, S3FormType } from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.schema";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { updateS3SettingsAction } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.action";
|
||||
import { updateS3SettingsAction } from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.action";
|
||||
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
|
||||
|
||||
export type S3FormProps = {
|
||||
-1
@@ -66,7 +66,6 @@ export const accountsColumns: ColumnDef<{
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonWithLoading
|
||||
variant="outline"
|
||||
text=""
|
||||
disabled={row.original.provider === "credential" || table.getRowModel().rows.length <= 1}
|
||||
icon={<Unlink color="red" size={15}/>}
|
||||
onClick={async () => {
|
||||
+6
-3
@@ -1,7 +1,7 @@
|
||||
import {User, UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/admin-user-tab/columns-users";
|
||||
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/columns-users";
|
||||
|
||||
export type AdminUsersTableProps = {
|
||||
users: UserWithAccounts[];
|
||||
@@ -18,7 +18,10 @@ export const AdminUsersTable = (props: AdminUsersTableProps) => {
|
||||
<CardDescription>Manage your users</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable columns={usersColumnsAdmin} data={users}/>
|
||||
<DataTable
|
||||
enableSelect={false}
|
||||
columns={usersColumnsAdmin}
|
||||
data={users}/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -0,0 +1,61 @@
|
||||
"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 {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-delete-account/delete-account.action";
|
||||
|
||||
export type ButtonDeleteUserProps = {
|
||||
userId: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const ButtonDeleteUser = (props: ButtonDeleteUserProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => deleteUserAction(props.userId),
|
||||
onSuccess: async () => {
|
||||
toast.success("User deleted successfully.");
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
<ButtonWithConfirm
|
||||
title={""}
|
||||
|
||||
description="Are you sure you want to remove this user? This action cannot be undone."
|
||||
button={{
|
||||
main: {
|
||||
disabled: !!props.disabled,
|
||||
text: "",
|
||||
variant: "outline",
|
||||
size: "sm",
|
||||
icon: <Trash2 color="red" size={15}/>,
|
||||
},
|
||||
confirm: {
|
||||
className: "w-full",
|
||||
text: "Delete",
|
||||
icon: <Trash2/>,
|
||||
variant: "destructive",
|
||||
onClick: () => {
|
||||
mutation.mutate()
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
className: "w-full",
|
||||
text: "Cancel",
|
||||
icon: <Trash2/>,
|
||||
variant: "outline",
|
||||
},
|
||||
}}
|
||||
isPending={mutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+5
-21
@@ -13,6 +13,7 @@ import {UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||
import {formatFrenchDate} from "@/utils/date-formatting";
|
||||
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
||||
import {ButtonDeleteUser} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/button-delete-use";
|
||||
|
||||
export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
||||
{
|
||||
@@ -72,7 +73,7 @@ export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
||||
accessorKey: "accounts",
|
||||
header: "Provider ID",
|
||||
cell: ({row}) => {
|
||||
return(
|
||||
return (
|
||||
<div>
|
||||
{row.original.accounts.map((item) => (
|
||||
<div key={item.id}>
|
||||
@@ -98,27 +99,10 @@ export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
||||
const {data: session, isPending} = useSession();
|
||||
const isSuperAdmin = session?.user.role == "superadmin";
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => deleteUserAction(row.original.id),
|
||||
onSuccess: async () => {
|
||||
toast.success("User deleted successfully.");
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonWithLoading
|
||||
disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
|
||||
variant="outline"
|
||||
text=""
|
||||
icon={<Trash2 color="red" size={15}/>}
|
||||
onClick={async () => {
|
||||
await mutation.mutateAsync();
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<ButtonDeleteUser
|
||||
disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
|
||||
userId={row.original.id}/>
|
||||
);
|
||||
},
|
||||
},
|
||||
-5
@@ -19,10 +19,6 @@ export const sessionsColumns: ColumnDef<Session>[] = [
|
||||
return timeAgo(row.original.expiresAt);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "ipAddress",
|
||||
header: "IP Address",
|
||||
},
|
||||
{
|
||||
id: "device",
|
||||
header: "Device",
|
||||
@@ -73,7 +69,6 @@ export const sessionsColumns: ColumnDef<Session>[] = [
|
||||
<ButtonWithLoading
|
||||
variant="outline"
|
||||
disabled={session?.session.id === row.original.id}
|
||||
text=""
|
||||
icon={<Unlink color="red" size={15} />}
|
||||
onClick={async () => {
|
||||
await mutation.mutateAsync();
|
||||
@@ -1,25 +1,20 @@
|
||||
"use client";
|
||||
import {generateEdgeKey} from "@/utils/edge_key";
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
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/08_agent";
|
||||
|
||||
export type AgentCardKeyProps = {
|
||||
agent: Agent;
|
||||
edgeKey: string;
|
||||
};
|
||||
|
||||
export const AgentCardKey = (props: AgentCardKeyProps) => {
|
||||
const edge_key = generateEdgeKey(getServerUrl(), props.agent.id);
|
||||
const [code, setCode] = useState<string>(`${edge_key}`);
|
||||
|
||||
export const AgentCardKey = ({edgeKey}: AgentCardKeyProps) => {
|
||||
const [code, setCode] = useState<string>(`${edgeKey}`);
|
||||
return (
|
||||
<>
|
||||
<PasswordInput
|
||||
value={code}
|
||||
onChange={() => {
|
||||
setCode(edge_key);
|
||||
setCode(edgeKey);
|
||||
}}
|
||||
/>
|
||||
<CopyButton className="mt-5" value={code}/>
|
||||
|
||||
@@ -14,7 +14,9 @@ export const AgentCard = (props: agentCardProps) => {
|
||||
const { data: agent } = props;
|
||||
|
||||
return (
|
||||
<Link href={`/dashboard/agents/${agent.id}`} className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md">
|
||||
<Link href={`/dashboard/agents/${agent.id}`}
|
||||
className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md rounded-xl"
|
||||
>
|
||||
<Card className="flex flex-row justify-between">
|
||||
<div className="flex-1 text-left">
|
||||
<CardHeader className="text-2xl font-bold">{agent.name}</CardHeader>
|
||||
|
||||
@@ -52,11 +52,14 @@ export const AgentForm = (props: agentFormProps) => {
|
||||
return;
|
||||
}
|
||||
toast.success(`Success ${isCreate ? "creating" : "updating"} agent`);
|
||||
router.push(`/dashboard/agents/${data.id}`);
|
||||
router.refresh();
|
||||
router.push(`/dashboard/agents/${data.id}`);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Card>
|
||||
|
||||
@@ -38,12 +38,11 @@ export const BackupButton = (props: BackupButtonProps) => {
|
||||
<ButtonWithLoading
|
||||
icon={<DatabaseZap/>}
|
||||
disabled={props.disable}
|
||||
text={isMobile ? "" : "Backup"}
|
||||
isPending={mutation.isPending}
|
||||
size={"default"}
|
||||
onClick={async () => {
|
||||
await HandleAction();
|
||||
}}
|
||||
/>
|
||||
>{isMobile ? "" : "Backup"}</ButtonWithLoading>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ export const LoggedInDropdown = (props: LoggedInDropdownProps) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>{props.children}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" className="w-[--radix-popper-anchor-width]">
|
||||
<DropdownMenuContent side="top" className="min-w-[var(--radix-popper-anchor-width)]">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
redirect("/dashboard/profile");
|
||||
|
||||
@@ -10,15 +10,16 @@ import {SidebarMenuCustomMain} from "@/components/wrappers/dashboard/common/side
|
||||
import {SideBarFooterCredit} from "@/components/wrappers/dashboard/common/sidebar/side-bar-footer-credit";
|
||||
import {OrganizationCombobox} from "@/components/wrappers/dashboard/organization/organization-combobox";
|
||||
import {LoggedInButton} from "@/components/wrappers/dashboard/common/logged-in/logged-in-button";
|
||||
import {env} from "@/env.mjs";
|
||||
|
||||
export function AppSidebar() {
|
||||
|
||||
const projectName = env.PROJECT_NAME;
|
||||
return (
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarLogo/>
|
||||
<SidebarLogo projectName={projectName ?? "Portabase"}/>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<OrganizationCombobox/>
|
||||
@@ -32,10 +33,10 @@ export function AppSidebar() {
|
||||
|
||||
<SidebarMenu className="mb-2">
|
||||
<SidebarMenuItem>
|
||||
<LoggedInButton />
|
||||
<LoggedInButton/>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
<SideBarFooterCredit />
|
||||
<SideBarFooterCredit/>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
import { useSidebar } from "@/components/ui/sidebar";
|
||||
import Link from "next/link";
|
||||
import { env } from "@/env.mjs";
|
||||
import { useTheme } from "next-themes";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const SidebarLogo = () => {
|
||||
export const SidebarLogo = ({projectName}: {
|
||||
projectName: string;
|
||||
}) => {
|
||||
const { state, isMobile } = useSidebar();
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
@@ -28,7 +29,7 @@ export const SidebarLogo = () => {
|
||||
<Image
|
||||
loading="eager"
|
||||
src={"/images/logo.png"}
|
||||
alt={`Logo ${env.NEXT_PUBLIC_PROJECT_NAME}`}
|
||||
alt={`Logo ${projectName}`}
|
||||
className="h-10 w-10 object-contain"
|
||||
height={10}
|
||||
width={10}
|
||||
@@ -38,7 +39,7 @@ export const SidebarLogo = () => {
|
||||
<Image
|
||||
loading="eager"
|
||||
src={imageTheme}
|
||||
alt={`Logo ${env.NEXT_PUBLIC_PROJECT_NAME}`}
|
||||
alt={`Logo ${projectName}`}
|
||||
className="object-contain"
|
||||
width={190}
|
||||
height={90}
|
||||
|
||||
@@ -29,11 +29,12 @@ export const SidebarMenuCustomMain = () => {
|
||||
const groupContent: SidebarGroupItem["group_content"] = [
|
||||
{ title: "Projects", url: "/projects", icon: Layers, details:true },
|
||||
{ title: "Statistics", url: "/statistics", icon: ChartArea },
|
||||
{ title: "Settings", url: "/settings", icon: Settings, details:true }
|
||||
];
|
||||
|
||||
if (activeOrganization && (member?.data?.role === "admin" || member?.data?.role === "owner")) {
|
||||
groupContent.push({ title: "Settings", url: "/settings", icon: Settings, details:true });
|
||||
}
|
||||
// if (activeOrganization && (member?.data?.role === "admin" || member?.data?.role === "owner")) {
|
||||
// groupContent.push({ title: "Settings", url: "/settings", icon: Settings, details:true });
|
||||
// }
|
||||
|
||||
const items: SidebarGroupItem[] = [
|
||||
{
|
||||
|
||||
@@ -41,7 +41,6 @@ export function CreateOrganizationModal({open, onOpenChange, onSuccess}: createO
|
||||
await authClient.organization.setActive({organizationSlug: result.data.value.slug});
|
||||
onSuccess?.();
|
||||
toast.success(result.data.actionSuccess?.message || "Organization Created.");
|
||||
// router.push("/");
|
||||
router.replace(`/dashboard/home`);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
@@ -82,26 +81,6 @@ export function CreateOrganizationModal({open, onOpenChange, onSuccess}: createO
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/*<FormField*/}
|
||||
{/* control={form.control}*/}
|
||||
{/* name="slug"*/}
|
||||
{/* defaultValue=""*/}
|
||||
{/* render={({field}) => (*/}
|
||||
{/* <FormItem>*/}
|
||||
{/* <FormLabel>Slug</FormLabel>*/}
|
||||
{/* <FormControl>*/}
|
||||
{/* <Input*/}
|
||||
{/* {...field}*/}
|
||||
{/* onChange={(e) => {*/}
|
||||
{/* const value = e.target.value.replaceAll(" ", "-").toLowerCase();*/}
|
||||
{/* field.onChange(value);*/}
|
||||
{/* }}*/}
|
||||
{/* />*/}
|
||||
{/* </FormControl>*/}
|
||||
{/* <FormMessage/>*/}
|
||||
{/* </FormItem>*/}
|
||||
{/* )}*/}
|
||||
{/*/>*/}
|
||||
<DialogFooter>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<Button type="submit">Create</Button>
|
||||
|
||||
+2
-3
@@ -2,7 +2,6 @@
|
||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import {deleteOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {setCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {toast} from "sonner";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
@@ -17,17 +16,17 @@ export const DeleteOrganizationButton = (props: DeleteOrganizationButtonProps) =
|
||||
const {data: organizations, refetch} = authClient.useListOrganizations();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => deleteOrganizationAction(props.organizationSlug),
|
||||
mutationFn: () => deleteOrganizationAction({slug: props.organizationSlug}),
|
||||
|
||||
onSuccess: async (result) => {
|
||||
if (result?.data?.success) {
|
||||
await authClient.organization.setActive({
|
||||
organizationSlug: "default",
|
||||
});
|
||||
router.push("/");
|
||||
toast.success(result.data.actionSuccess?.message || "Organization deleted.");
|
||||
router.refresh()
|
||||
refetch()
|
||||
router.push("/");
|
||||
} else {
|
||||
// @ts-ignore
|
||||
const errorMsg = result?.data?.actionError?.message || result?.data?.actionError?.messageParams?.message || "Failed to delete the organization.";
|
||||
|
||||
@@ -39,5 +39,6 @@ export function OrganizationCombobox() {
|
||||
|
||||
return <>{state === "expanded" &&
|
||||
<ComboBox sideBar values={values} defaultValue={activeOrganization?.slug} onValueChange={onValueChange}
|
||||
reload={handleReset}/>}</>;
|
||||
reload={handleReset}/>}
|
||||
</>;
|
||||
}
|
||||
|
||||
@@ -86,8 +86,6 @@ export const OrganizationForm = (props: organizationFormProps) => {
|
||||
console.error("Mutation network error:", error);
|
||||
toast.error(error?.message || "A network error occurred.");
|
||||
},
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -8,12 +8,11 @@ import {
|
||||
OrganizationFormSchema
|
||||
} from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema";
|
||||
import {db} from "@/db";
|
||||
import {and, eq, inArray} from "drizzle-orm";
|
||||
import {auth, checkSlugOrganization, createOrganization, deleteOrganization} from "@/lib/auth/auth";
|
||||
import {and, eq, inArray, or} from "drizzle-orm";
|
||||
import {auth, checkSlugOrganization, createOrganization} from "@/lib/auth/auth";
|
||||
import {slugify} from "@/utils/slugify";
|
||||
import {Organization} from "@/db/schema/03_organization";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {headers} from "next/headers";
|
||||
|
||||
export const createOrganizationAction = userAction.schema(OrganizationSchema).action(async ({parsedInput}): Promise<ServerActionResult<Organization>> => {
|
||||
try {
|
||||
@@ -84,7 +83,6 @@ export const updateOrganizationAction = userAction
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (!organization) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -113,61 +111,11 @@ export const updateOrganizationAction = userAction
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// await db
|
||||
// .insert(drizzleDb.schemas.member)
|
||||
// .values(
|
||||
// usersToAdd.map((userId) => ({
|
||||
// userId,
|
||||
// organizationId: organization.id,
|
||||
// role: "member",
|
||||
// }))
|
||||
// )
|
||||
// .execute();
|
||||
}
|
||||
|
||||
if (usersToRemove.length > 0) {
|
||||
await db.delete(drizzleDb.schemas.member).where(and(inArray(drizzleDb.schemas.member.userId, usersToRemove), eq(drizzleDb.schemas.member.organizationId, organization.id))).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(drizzleDb.schemas.organization)
|
||||
.set({
|
||||
@@ -200,11 +148,24 @@ export const updateOrganizationAction = userAction
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteOrganizationAction = userAction.schema(z.string()).action(
|
||||
export const deleteOrganizationAction = userAction.schema(
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
slug: z.string().optional(),
|
||||
})
|
||||
).action(
|
||||
async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
|
||||
try {
|
||||
const conditions = [];
|
||||
if (parsedInput.id) {
|
||||
conditions.push(eq(drizzleDb.schemas.organization.id, parsedInput.id));
|
||||
}
|
||||
if (parsedInput.slug) {
|
||||
conditions.push(eq(drizzleDb.schemas.organization.slug, parsedInput.slug));
|
||||
}
|
||||
|
||||
const org = await db.query.organization.findFirst({
|
||||
where: eq(drizzleDb.schemas.organization.slug, parsedInput),
|
||||
where: or(...conditions),
|
||||
});
|
||||
|
||||
if (!org) {
|
||||
@@ -221,8 +182,6 @@ export const deleteOrganizationAction = userAction.schema(z.string()).action(
|
||||
let deletedOrganization: Organization;
|
||||
|
||||
try {
|
||||
// TODO : Improve with better auth, always getting 403 error
|
||||
// deletedOrganization = await deleteOrganization(org.id) as Organization;
|
||||
[deletedOrganization] = await db
|
||||
.delete(drizzleDb.schemas.organization)
|
||||
.where(eq(drizzleDb.schemas.organization.id, org.id))
|
||||
|
||||
@@ -12,8 +12,8 @@ import { UserSchema, UserType } from "@/components/wrappers/dashboard/profile/us
|
||||
import { toast } from "sonner";
|
||||
import { updateUserAction } from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
|
||||
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||
import {sessionsColumns} from "@/components/wrappers/dashboard/admin/admin-user-tab/sessions/table-columns";
|
||||
import {accountsColumns} from "@/components/wrappers/dashboard/admin/admin-user-tab/accounts/table-columns";
|
||||
import {sessionsColumns} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/sessions/table-columns";
|
||||
import {accountsColumns} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/accounts/table-columns";
|
||||
import {Session} from "better-auth";
|
||||
|
||||
export type UserFormProps = {
|
||||
@@ -54,8 +54,8 @@ export const UserForm = (props: UserFormProps) => {
|
||||
}
|
||||
|
||||
toast.success(`Profile updated successfully.`);
|
||||
router.push(`/dashboard/profile`);
|
||||
router.refresh();
|
||||
router.push(`/dashboard/profile`);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user