diff --git a/app/(customer)/dashboard/agents/[agentId]/page.tsx b/app/(customer)/dashboard/agents/[agentId]/page.tsx index 8909b8f5..98ace108 100644 --- a/app/(customer)/dashboard/agents/[agentId]/page.tsx +++ b/app/(customer)/dashboard/agents/[agentId]/page.tsx @@ -3,28 +3,12 @@ import {Page, PageActions, PageContent, PageDescription, PageHeader, PageTitle} import {Button} from "@/components/ui/button"; import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"; import {Card, CardContent, CardHeader} from "@/components/ui/card"; -import {backupColumns} from "@/features/backup/columns"; -import {restoreColumns} from "@/features/restore/columns"; +import {TablePagination} from "@/components/wrappers/table/table-pagination"; +import {columns} from "@/features/backup/columns"; import {DataTableWithPagination} from "@/components/wrappers/table/data-table-with-pagination"; -import {prisma} from "@/prisma"; -import {GearIcon} from "@radix-ui/react-icons"; -import Link from "next/link"; -export default async function RoutePage(props: PageParams<{ agentId: string }>) { - - // const agent = await prisma.agent.findUnique({ - // where: { - // id: props.params.agentId, - // }, - // }) - - const agent = { - "id": props.params.agentId, - "name": "Agent 1", - "description": "My beautiful project!", - "lastContact": null, - } +export default async function RoutePage(props: PageParams<{}>) { const backups = [ {'id': 'backup-1', 'createdAt': '2023-11-27T11:26:54.870914Z', 'status': 'pending'}, @@ -62,81 +46,55 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>) {'id': 'restore-10', 'backupId': 'backup-10', 'createdAt': '2023-11-25T11:26:54.871000Z', 'status': 'failed'} ] - const databaseId = 'db-123'; - - const totalBackups = await prisma.backup.count({ - where: { - databaseId: databaseId, - }, - }); - - const successfulBackups = await prisma.backup.count({ - where: { - databaseId: databaseId, - status: 'success', - }, - }); - - const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null - - return ( - {agent.name} - - - + Agent 1 - - {agent.description} + My beautiful project! - -
+ +
- + Backups - + Success rate - - {successRate ?? "Unavailable for now."} - + - + Last contact - - {agent.lastContact?.toDateString() ?? "Never connected"} - +
- - + Backup - Backup + Restore - - + + - - + + +
diff --git a/app/(customer)/dashboard/agents/page.tsx b/app/(customer)/dashboard/agents/page.tsx index 38c883d5..f4b25e03 100644 --- a/app/(customer)/dashboard/agents/page.tsx +++ b/app/(customer)/dashboard/agents/page.tsx @@ -36,7 +36,7 @@ export default async function RoutePage(props: PageParams<{}>) { - +
-
+
{children}
diff --git a/app/(customer)/dashboard/profile/page.tsx b/app/(customer)/dashboard/profile/page.tsx index b74954db..7b86781f 100644 --- a/app/(customer)/dashboard/profile/page.tsx +++ b/app/(customer)/dashboard/profile/page.tsx @@ -6,6 +6,11 @@ import {requiredCurrentUser} from "@/auth/current-user"; import {UserForm} from "@/components/wrappers/Dashboard/Profile/UserForm/UserForm"; import {prisma} from "@/prisma"; import {Badge} from "@/components/ui/badge"; +import Link from "next/link"; +import {Button} from "@/components/ui/button"; +import {ButtonWithConfirm} from "@/components/wrappers/Button/ButtonWithConfirm/ButtonWithConfirm"; +import {ButtonDeleteAccount} from "@/components/wrappers/Dashboard/Profile/ButtonDeleteAccount/ButtonDeleteAccount"; +import {useIsMobile} from "@/hooks/use-mobile"; export default async function RoutePage(props: PageParams<{}>) { @@ -22,23 +27,32 @@ export default async function RoutePage(props: PageParams<{}>) { console.log(userInfo) + const test = () => { + console.log("test") + } + return ( - - - - {user.name?.[0]} - {user.image ? ( - - ) : null} - - {user.name} - {userInfo.authMethod} - - + {/**/} +
+ + + {user.name?.[0]} + {user.image ? ( + + ) : null} + + {user.name} + {userInfo.authMethod} + + + + +
+ {/*
*/} - +
-) + ) } \ No newline at end of file diff --git a/app/api/agent/[agentId]/status/route.ts b/app/api/agent/[agentId]/status/route.ts new file mode 100644 index 00000000..e601e83d --- /dev/null +++ b/app/api/agent/[agentId]/status/route.ts @@ -0,0 +1,47 @@ +import {prisma} from "@/prisma"; +import {NextResponse} from "next/server"; + +export async function POST( + request: Request, + {params}: { params: Promise<{ agentId: string }> } +) { + const agentId = (await params).agentId + + const agent = await prisma.agent.findFirst({ + where: { + id: agentId + } + }) + + if(!agent){ + return NextResponse.json({error: "Agent not found"}, {status: 404}) + } + + await prisma.agent.update({ + where: { + id: agent.id, + }, + data: { + lastContact: new Date(), + } + }); + + const response = { + agent: { + id: agentId, + lastContact: agent.lastContact + }, + backup: { + action: false, + cron : "" + }, + restore: { + action: false, + file: "" + } + } + + return Response.json({ + message: response + }) +} \ No newline at end of file diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 00000000..ecb9ada5 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,39 @@ +import {NextRequest, NextResponse} from 'next/server' +import {loggingMiddleware} from "@/middleware/loggingMiddleware"; +import {errorHandler} from "@/middleware/errorHandler"; + +export function middleware(request: NextRequest) { + const url = request.nextUrl.clone(); + 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 }), + { status: 404, headers: { 'Content-Type': 'application/json' } } + ); + } + } + try{ + loggingMiddleware(request); + }catch(err){ + errorHandler(err) + } + + +} +// Function to check if the route exists (supports dynamic routes) +function checkRouteExists(pathname) { + // 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 + ]; + return routePatterns.some(pattern => pattern.test(pathname)); +} +export const config = { + matcher: ['/api/agent/:path*'], +}; \ No newline at end of file diff --git a/package.json b/package.json index 9f02445a..b1b4e371 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack -p 80", + "dev": "next dev -p 80", "build": "next build", "start": "next start", "lint": "next lint" @@ -67,6 +67,7 @@ "sonner": "^1.6.1", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", + "uuid": "^11.0.3", "vaul": "^1.1.1", "zod": "^3.23.8" }, diff --git a/prisma/migrations/20241103104432_2024_11_03/migration.sql b/prisma/migrations/20241103104432_2024_11_03/migration.sql old mode 100644 new mode 100755 diff --git a/prisma/migrations/20241103192308_2024_11_03/migration.sql b/prisma/migrations/20241103192308_2024_11_03/migration.sql old mode 100644 new mode 100755 diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f44dc634..de672dd0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -61,39 +61,35 @@ model User { image String? role String? password String? - authMethod String? @map("auth_method") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt @map("updated_at") - accounts Account[] - sessions Session[] + accounts Account[] + sessions Session[] + authMethod String? + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime? @updatedAt @map("updated_at") @@map("users") } model Agent { - id String @id @default(cuid()) - slug String @unique + id String @id @default(cuid()) + slug String @unique name String description String? - lastContact DateTime? @map("last_contact") - createdAt DateTime @default(now()) @map("created_at") - - databases Database[] + lastContact DateTime? @map("last_contact") + createdAt DateTime @default(now()) @map("created_at") + databases Database[] } model Database { - id String @id @default(cuid()) + id String @id @default(cuid()) + agentId String @map("agent_id") + agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade) + name String description String? backupPolicy String? @map("backup_policy") createdAt DateTime @default(now()) @map("created_at") - - agentId String @map("agent_id") - agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade) - - backups Backup[] - restaurations Restauration[] } enum Status { @@ -104,25 +100,16 @@ enum Status { } model Backup { - id String @id @default(cuid()) - status Status @default(waiting) - file String? - createdAt DateTime @default(now()) @map("created_at") - + id String @id @default(cuid()) databaseId String @map("database_id") - database Database @relation(fields: [databaseId], references: [id], onDelete: Cascade) - - restaurations Restauration[] + status Status @default(waiting) + file String? + createdAt DateTime @default(now()) @map("created_at") } -model Restauration { +model Restore { id String @id @default(cuid()) + backupId String @map("backup_id") status Status @default(waiting) createdAt DateTime @default(now()) @map("created_at") - - backupId String @map("backup_id") - backup Backup @relation(fields: [backupId], references: [id], onDelete: Cascade) - - databaseId String? @map("database_id") - database Database? @relation(fields: [databaseId], references: [id], onDelete: Cascade) } diff --git a/src/components/wrappers/Button/ButtonWithConfirm/ButtonWithConfirm.tsx b/src/components/wrappers/Button/ButtonWithConfirm/ButtonWithConfirm.tsx new file mode 100644 index 00000000..5798e0d2 --- /dev/null +++ b/src/components/wrappers/Button/ButtonWithConfirm/ButtonWithConfirm.tsx @@ -0,0 +1,45 @@ +"use client" +import {Button} from "@/components/ui/button"; +import {useState} from "react"; +import {Loader2} from "lucide-react"; + +export type VariantButton = { + secondary: string + default: string + outline: string + ghost: string + link: string + destructive: string +} + +export type ButtonWithConfirmProps = { + icon?: any, + text: string, + variant?: keyof VariantButton , + className?: string, + onClick?: () => void, + isPending? : boolean +}; + +export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => { + const [isConfirming, setIsConfirming] = useState(false) + return( + + ) +} \ No newline at end of file diff --git a/src/components/wrappers/Dashboard/Profile/ButtonDeleteAccount/ButtonDeleteAccount.tsx b/src/components/wrappers/Dashboard/Profile/ButtonDeleteAccount/ButtonDeleteAccount.tsx new file mode 100644 index 00000000..98ef5170 --- /dev/null +++ b/src/components/wrappers/Dashboard/Profile/ButtonDeleteAccount/ButtonDeleteAccount.tsx @@ -0,0 +1,32 @@ +"use client" +import {useMutation} from "@tanstack/react-query"; +import {useRouter} from "next/navigation"; +import {signOutAction} from "@/features/auth/auth.action"; +import {ButtonWithConfirm} from "@/components/wrappers/Button/ButtonWithConfirm/ButtonWithConfirm"; +import {deleteUserAction} from "@/components/wrappers/Dashboard/Profile/ButtonDeleteAccount/delete-account.action"; +import {Trash2} from "lucide-react"; + +export type ButtonDeleteAccountProps = {} + +export const ButtonDeleteAccount = (props: ButtonDeleteAccountProps) => { + + const mutation = useMutation({ + mutationFn: () => deleteUserAction(""), + onSuccess: async () => { + await signOutAction(); + }, + }) + + return ( + { + mutation.mutate() + }} + variant={"destructive"} + isPending={mutation.isPending} + className="gap-2" + icon={} + /> + ) +} diff --git a/src/components/wrappers/Dashboard/Profile/ButtonDeleteAccount/delete-account.action.ts b/src/components/wrappers/Dashboard/Profile/ButtonDeleteAccount/delete-account.action.ts new file mode 100644 index 00000000..c6f54ed8 --- /dev/null +++ b/src/components/wrappers/Dashboard/Profile/ButtonDeleteAccount/delete-account.action.ts @@ -0,0 +1,42 @@ +"use server" +import {userAction} from "@/safe-actions"; +import {prisma} from "@/prisma"; +import {z} from "zod"; +import {v4 as uuidv4} from "uuid"; + + +export const deleteUserAction = userAction + .schema(z.string()) + .action(async ({parsedInput, ctx}) => { + + const uuid = uuidv4() + + const user = await prisma.user.update({ + where: { + id: ctx.user.id, + }, + data: { + email: `${uuid}@portabase.com`, + name: `${uuid}`, + } + }) + + const account = await prisma.account.findFirst({ + where: { + userId: ctx.user.id, + } + }) + if(account){ + await prisma.account.delete({ + where: { + id: account.id + } + + }) + } + + + return { + data: user, + } + }); \ No newline at end of file diff --git a/src/components/wrappers/cards-with-pagination.tsx b/src/components/wrappers/cards-with-pagination.tsx index 4c78ed2a..e3c2e892 100644 --- a/src/components/wrappers/cards-with-pagination.tsx +++ b/src/components/wrappers/cards-with-pagination.tsx @@ -42,14 +42,14 @@ export const CardsWithPagination = (props: cardsWithPaginationProps) => { return ( -
-
+
+
{currentCards.map((card, key) => ( ))}
{ const {className, totalPages, currentPage, goToPage, goToPrevPage, goToNextPage, maxVisiblePages = 3} = props return ( - + diff --git a/src/components/wrappers/table/data-table-with-pagination.tsx b/src/components/wrappers/table/data-table-with-pagination.tsx index b1e68d38..b3f39fea 100644 --- a/src/components/wrappers/table/data-table-with-pagination.tsx +++ b/src/components/wrappers/table/data-table-with-pagination.tsx @@ -30,9 +30,9 @@ export function DataTableWithPagination({columns, data}: DataTabl }) return ( -
+
- +
) } diff --git a/src/components/wrappers/table/table-pagination-size.tsx b/src/components/wrappers/table/table-pagination-size.tsx index f9a20a7d..071ede60 100644 --- a/src/components/wrappers/table/table-pagination-size.tsx +++ b/src/components/wrappers/table/table-pagination-size.tsx @@ -1,4 +1,3 @@ -import {useEffect} from "react"; import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"; import {cn} from "@/lib/utils"; @@ -13,12 +12,8 @@ export const TablePaginationSize = (props: tablePaginationSizeProps) => { const {className, table, pageSizeOptions = [10, 20, 30, 40, 50]} = props - useEffect(() => { - table.setPageSize(Number(pageSizeOptions[0])) - }, []); - return ( -
+

Rows per page