mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
refactor(agents): reorganize into actions/components/schemas/hooks
This commit is contained in:
@@ -9,12 +9,12 @@ import { db } from "@/db";
|
|||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {eq, isNull} from "drizzle-orm";
|
import {eq, isNull} from "drizzle-orm";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { ButtonDeleteAgent } from "@/features/agents/agent-delete-button";
|
import { ButtonDeleteAgent } from "@/features/agents/components/agent-delete-button";
|
||||||
import { capitalizeFirstLetter } from "@/utils/text";
|
import { capitalizeFirstLetter } from "@/utils/text";
|
||||||
import { generateEdgeKey } from "@/utils/edge_key";
|
import { generateEdgeKey } from "@/utils/edge_key";
|
||||||
import { getServerUrl } from "@/utils/get-server-url";
|
import { getServerUrl } from "@/utils/get-server-url";
|
||||||
import { AgentContentPage } from "@/features/agents/agent-content";
|
import { AgentContentPage } from "@/features/agents/components/agent-content";
|
||||||
import { AgentDialog } from "@/features/agents/agent-dialog";
|
import { AgentDialog } from "@/features/agents/components/agent-dialog";
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(
|
export default async function RoutePage(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {AgentCard} from "@/features/agents/agent-card";
|
import {AgentCard} from "@/features/agents/components/agent-card";
|
||||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
@@ -7,7 +7,7 @@ import {db} from "@/db";
|
|||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {and, desc, eq, isNull, not} from "drizzle-orm";
|
import {and, desc, eq, isNull, not} from "drizzle-orm";
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
import {AgentDialog} from "@/features/agents/agent-dialog";
|
import {AgentDialog} from "@/features/agents/components/agent-dialog";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Agents",
|
title: "Agents",
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import {db} from "@/db";
|
|||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {ButtonDeleteAgent} from "@/features/agents/agent-delete-button";
|
import {ButtonDeleteAgent} from "@/features/agents/components/agent-delete-button";
|
||||||
import {capitalizeFirstLetter} from "@/utils/text";
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
import {generateEdgeKey} from "@/utils/edge_key";
|
import {generateEdgeKey} from "@/utils/edge_key";
|
||||||
import {getServerUrl} from "@/utils/get-server-url";
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
import {AgentContentPage} from "@/features/agents/agent-content";
|
import {AgentContentPage} from "@/features/agents/components/agent-content";
|
||||||
import {AgentDialog} from "@/features/agents/agent-dialog";
|
import {AgentDialog} from "@/features/agents/components/agent-dialog";
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {withApiKey} from "@/lib/api-v1/middleware";
|
|||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
import {ApiKeyContext} from "@/lib/api-v1/types";
|
import {ApiKeyContext} from "@/lib/api-v1/types";
|
||||||
import {getAgent, resolveAgentAccess} from "@/lib/api-v1/services/agents";
|
import {getAgent, resolveAgentAccess} from "@/lib/api-v1/services/agents";
|
||||||
import {deleteAgentService} from "@/features/agents/agent-delete.action";
|
import {deleteAgentService} from "@/features/agents/actions/agent-delete.action";
|
||||||
|
|
||||||
const log = logger.child({module: "api/v1/agents/[id]"});
|
const log = logger.child({module: "api/v1/agents/[id]"});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as drizzleDb from "@/db";
|
|||||||
import { inArray, eq, and, or, isNull } from "drizzle-orm";
|
import { inArray, eq, and, or, isNull } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import {createAgentService} from "@/features/agents/agents.action";
|
import {createAgentService} from "@/features/agents/actions/agents.action";
|
||||||
import { ActionError } from "@/lib/safe-actions/actions";
|
import { ActionError } from "@/lib/safe-actions/actions";
|
||||||
import {getAccessibleAgentIds} from "@/lib/api-v1/services/agents";
|
import {getAccessibleAgentIds} from "@/lib/api-v1/services/agents";
|
||||||
import {ApiKeyContext} from "@/lib/api-v1/types";
|
import {ApiKeyContext} from "@/lib/api-v1/types";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import {action, ActionError, userAction} from "@/lib/safe-actions/actions";
|
import {action, ActionError, userAction} from "@/lib/safe-actions/actions";
|
||||||
import {AgentSchema} from "@/features/agents/agents.schema";
|
import {AgentSchema} from "@/features/agents/schemas/agents.schema";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {eq, and, ne, count, desc} from "drizzle-orm";
|
import {eq, and, ne, count, desc} from "drizzle-orm";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
@@ -11,7 +11,7 @@ import {Badge} from "@/components/ui/badge";
|
|||||||
import {truncateWords} from "@/utils/text";
|
import {truncateWords} from "@/utils/text";
|
||||||
import {useIsMobile} from "@/hooks/use-mobile";
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
||||||
import {useAgentUpdateCheck} from "@/features/agents/use-agent-update-check";
|
import {useAgentUpdateCheck} from "@/features/agents/hooks/use-agent-update-check";
|
||||||
|
|
||||||
export type agentCardProps = {
|
export type agentCardProps = {
|
||||||
data: AgentWith;
|
data: AgentWith;
|
||||||
+3
-3
@@ -3,10 +3,10 @@
|
|||||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
import {Server} from "lucide-react";
|
import {Server} from "lucide-react";
|
||||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||||
import {AgentCardKey} from "@/features/agents/agent-card-key";
|
import {AgentCardKey} from "@/features/agents/components/agent-card-key";
|
||||||
import {AgentWithDatabases} from "@/db/schema/08_agent";
|
import {AgentWithDatabases} from "@/db/schema/08_agent";
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import {useQuery} from "@tanstack/react-query";
|
||||||
import {getAgentAction} from "@/features/agents/agents.action";
|
import {getAgentAction} from "@/features/agents/actions/agents.action";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import {Separator} from "@/components/ui/separator";
|
import {Separator} from "@/components/ui/separator";
|
||||||
import {Badge} from "@/components/ui/badge";
|
import {Badge} from "@/components/ui/badge";
|
||||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
||||||
import {AgentDatabaseCard} from "@/features/agents/agent-database-card";
|
import {AgentDatabaseCard} from "@/features/agents/components/agent-database-card";
|
||||||
import {HealthCheckGraph} from "@/features/database/health-grid";
|
import {HealthCheckGraph} from "@/features/database/health-grid";
|
||||||
import {HealthcheckLog} from "@/db/schema/15_healthcheck-log";
|
import {HealthcheckLog} from "@/db/schema/15_healthcheck-log";
|
||||||
|
|
||||||
+1
-1
@@ -5,7 +5,7 @@ import {ButtonWithConfirm} from "@/components/common/button-with-confirm";
|
|||||||
import {useMutation} from "@tanstack/react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {deleteAgentAction} from "@/features/agents/agent-delete.action";
|
import {deleteAgentAction} from "@/features/agents/actions/agent-delete.action";
|
||||||
import {useIsMobile} from "@/hooks/use-mobile";
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
|
|
||||||
export type ButtonDeleteAgentProps = {
|
export type ButtonDeleteAgentProps = {
|
||||||
+2
-2
@@ -7,7 +7,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {AgentForm} from "@/features/agents/agent-form";
|
import {AgentForm} from "@/features/agents/components/agent-form";
|
||||||
import {Button, buttonVariants} from "@/components/ui/button";
|
import {Button, buttonVariants} from "@/components/ui/button";
|
||||||
import {Plus} from "lucide-react";
|
import {Plus} from "lucide-react";
|
||||||
import {GearIcon} from "@radix-ui/react-icons";
|
import {GearIcon} from "@radix-ui/react-icons";
|
||||||
@@ -16,7 +16,7 @@ import {useState} from "react";
|
|||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||||
import {AgentOrganisationForm} from "@/features/agents/agent-organizations.form";
|
import {AgentOrganisationForm} from "@/features/agents/components/agent-organizations.form";
|
||||||
import {AgentWith} from "@/db/schema/08_agent";
|
import {AgentWith} from "@/db/schema/08_agent";
|
||||||
|
|
||||||
type AgentDialogProps = {
|
type AgentDialogProps = {
|
||||||
@@ -15,9 +15,9 @@ import {Button} from "@/components/ui/button";
|
|||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||||
import {AgentSchema, AgentType} from "@/features/agents/agents.schema";
|
import {AgentSchema, AgentType} from "@/features/agents/schemas/agents.schema";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {createAgentAction, updateAgentAction} from "@/features/agents/agents.action";
|
import {createAgentAction, updateAgentAction} from "@/features/agents/actions/agents.action";
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
|
|
||||||
export type agentFormProps = {
|
export type agentFormProps = {
|
||||||
+2
-2
@@ -8,8 +8,8 @@ import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
|||||||
import {MultiSelect} from "@/components/common/multi-select";
|
import {MultiSelect} from "@/components/common/multi-select";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {AgentWith} from "@/db/schema/08_agent";
|
import {AgentWith} from "@/db/schema/08_agent";
|
||||||
import {AgentOrganizationSchema, AgentOrganizationType} from "@/features/agents/agent-organizations.schema";
|
import {AgentOrganizationSchema, AgentOrganizationType} from "@/features/agents/schemas/agent-organizations.schema";
|
||||||
import {updateAgentOrganizationsAction} from "@/features/agents/agent-organizations.action";
|
import {updateAgentOrganizationsAction} from "@/features/agents/actions/agent-organizations.action";
|
||||||
|
|
||||||
|
|
||||||
type AgentOrganisationFormProps = {
|
type AgentOrganisationFormProps = {
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { userAction } from "@/lib/safe-actions/actions";
|
import { userAction } from "@/lib/safe-actions/actions";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getAgentAction } from "@/features/agents/agents.action";
|
import { getAgentAction } from "@/features/agents/actions/agents.action";
|
||||||
|
|
||||||
export const getAgentStatusAction = userAction
|
export const getAgentStatusAction = userAction
|
||||||
.schema(z.object({ agentId: z.string() }))
|
.schema(z.object({ agentId: z.string() }))
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useOnboarding } from "@onboardjs/react";
|
import { useOnboarding } from "@onboardjs/react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { createAgentAction } from "@/features/agents/agents.action";
|
import { createAgentAction } from "@/features/agents/actions/agents.action";
|
||||||
import type { OnboardingAgent, OnboardingDefaultsData } from "@/features/onboarding/types";
|
import type { OnboardingAgent, OnboardingDefaultsData } from "@/features/onboarding/types";
|
||||||
import { generateEdgeKey } from "@/utils/edge_key";
|
import { generateEdgeKey } from "@/utils/edge_key";
|
||||||
import { getServerUrl } from "@/utils/get-server-url";
|
import { getServerUrl } from "@/utils/get-server-url";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useOnboarding } from "@onboardjs/react";
|
import { useOnboarding } from "@onboardjs/react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { deleteAgentAction } from "@/features/agents/agent-delete.action";
|
import { deleteAgentAction } from "@/features/agents/actions/agent-delete.action";
|
||||||
import type { OnboardingAgent } from "@/features/onboarding/types";
|
import type { OnboardingAgent } from "@/features/onboarding/types";
|
||||||
|
|
||||||
export const useDeleteAgent = () => {
|
export const useDeleteAgent = () => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState } from "react";
|
|||||||
import { useOnboarding } from "@onboardjs/react";
|
import { useOnboarding } from "@onboardjs/react";
|
||||||
import { Server } from "lucide-react";
|
import { Server } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AgentCardKey } from "@/features/agents/agent-card-key";
|
import { AgentCardKey } from "@/features/agents/components/agent-card-key";
|
||||||
import type { OnboardingAgent } from "@/features/onboarding/types";
|
import type { OnboardingAgent } from "@/features/onboarding/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
|||||||
import {cn} from "@/lib/utils";
|
import {cn} from "@/lib/utils";
|
||||||
import {Agent} from "@/db/schema/08_agent";
|
import {Agent} from "@/db/schema/08_agent";
|
||||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
||||||
import {AgentCard} from "@/features/agents/agent-card";
|
import {AgentCard} from "@/features/agents/components/agent-card";
|
||||||
import {AgentDialog} from "@/features/agents/agent-dialog";
|
import {AgentDialog} from "@/features/agents/components/agent-dialog";
|
||||||
|
|
||||||
export type OrganizationAgentsTabProps = {
|
export type OrganizationAgentsTabProps = {
|
||||||
organization: OrganizationWithMembers;
|
organization: OrganizationWithMembers;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
} from "@/features/settings/storage.action";
|
} from "@/features/settings/storage.action";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {Switch} from "@/components/ui/switch";
|
import {Switch} from "@/components/ui/switch";
|
||||||
import {downloadMasterKeyAction} from "@/features/agents/keys.action";
|
import {downloadMasterKeyAction} from "@/features/agents/actions/keys.action";
|
||||||
|
|
||||||
export type SettingsStorageSectionProps = {
|
export type SettingsStorageSectionProps = {
|
||||||
settings: Setting;
|
settings: Setting;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";
|
import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";
|
||||||
import "@/lib/api-v1/openapi/registry";
|
import "@/lib/api-v1/openapi/registry";
|
||||||
import {AgentSchema} from "@/features/agents/agents.schema";
|
import {AgentSchema} from "@/features/agents/schemas/agents.schema";
|
||||||
import {agentSchema} from "@/db/schema/08_agent";
|
import {agentSchema} from "@/db/schema/08_agent";
|
||||||
|
|
||||||
const UuidParam = z
|
const UuidParam = z
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import { auth } from "@/lib/auth/auth";
|
import { auth } from "@/lib/auth/auth";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import {User} from "@/db/schema/02_user";
|
import { User } from "@/db/schema/02_user";
|
||||||
|
|
||||||
export const currentUser = async () => {
|
export const currentUser = async () => {
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: await headers(),
|
headers: await headers(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return session.user as User;
|
return session.user as User;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,19 +2,27 @@ import { createHmac } from "crypto";
|
|||||||
import { env } from "@/env.mjs";
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
export function signPasskeyContext(name: string, email: string): string {
|
export function signPasskeyContext(name: string, email: string): string {
|
||||||
const payload = Buffer.from(JSON.stringify({ name, email, exp: Date.now() + 5 * 60 * 1000 })).toString("base64url");
|
const payload = Buffer.from(
|
||||||
const sig = createHmac("sha256", env.PROJECT_SECRET).update(payload).digest("base64url");
|
JSON.stringify({ name, email, exp: Date.now() + 5 * 60 * 1000 }),
|
||||||
return `${payload}.${sig}`;
|
).toString("base64url");
|
||||||
|
const sig = createHmac("sha256", env.PROJECT_SECRET)
|
||||||
|
.update(payload)
|
||||||
|
.digest("base64url");
|
||||||
|
return `${payload}.${sig}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function verifyPasskeyContext(token: string): { name: string; email: string } | null {
|
export function verifyPasskeyContext(
|
||||||
const dot = token.lastIndexOf(".");
|
token: string,
|
||||||
if (dot === -1) return null;
|
): { name: string; email: string } | null {
|
||||||
const payload = token.slice(0, dot);
|
const dot = token.lastIndexOf(".");
|
||||||
const sig = token.slice(dot + 1);
|
if (dot === -1) return null;
|
||||||
const expected = createHmac("sha256", env.PROJECT_SECRET).update(payload).digest("base64url");
|
const payload = token.slice(0, dot);
|
||||||
if (sig !== expected) return null;
|
const sig = token.slice(dot + 1);
|
||||||
const data = JSON.parse(Buffer.from(payload, "base64url").toString());
|
const expected = createHmac("sha256", env.PROJECT_SECRET)
|
||||||
if (data.exp < Date.now()) return null;
|
.update(payload)
|
||||||
return { name: data.name, email: data.email };
|
.digest("base64url");
|
||||||
|
if (sig !== expected) return null;
|
||||||
|
const data = JSON.parse(Buffer.from(payload, "base64url").toString());
|
||||||
|
if (data.exp < Date.now()) return null;
|
||||||
|
return { name: data.name, email: data.email };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ const pending = ac.newRole({
|
|||||||
agent: [],
|
agent: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
//org
|
|
||||||
const orgMember = ac.newRole({
|
const orgMember = ac.newRole({
|
||||||
project: ["list"],
|
project: ["list"],
|
||||||
database: ["list"],
|
database: ["list"],
|
||||||
|
|||||||
+13
-13
@@ -1,16 +1,16 @@
|
|||||||
import nodemailer from "nodemailer";
|
import nodemailer from "nodemailer";
|
||||||
import {Server} from "./types"
|
import { Server } from "./types";
|
||||||
|
|
||||||
export const createTransporter = (server: Server) => {
|
export const createTransporter = (server: Server) => {
|
||||||
const portNumber = Number(server.port);
|
const portNumber = Number(server.port);
|
||||||
return nodemailer.createTransport({
|
return nodemailer.createTransport({
|
||||||
pool: true,
|
pool: true,
|
||||||
host: server.host,
|
host: server.host,
|
||||||
port: portNumber,
|
port: portNumber,
|
||||||
secure: server.secure ?? portNumber === 465,
|
secure: server.secure ?? portNumber === 465,
|
||||||
auth: {
|
auth: {
|
||||||
user: server.user,
|
user: server.user,
|
||||||
pass: server.pass,
|
pass: server.pass,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+38
-33
@@ -1,42 +1,47 @@
|
|||||||
"use server"
|
"use server";
|
||||||
import {db} from "@/db";
|
import { db } from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {createTransporter} from "@/lib/email/helpers";
|
import { createTransporter } from "@/lib/email/helpers";
|
||||||
import {Payload} from "@/lib/email/types";
|
import { Payload } from "@/lib/email/types";
|
||||||
|
|
||||||
export const sendEmail = async (data: Payload) => {
|
export const sendEmail = async (data: Payload) => {
|
||||||
const settings = await db
|
const settings = await db
|
||||||
.select()
|
.select()
|
||||||
.from(drizzleDb.schemas.setting)
|
.from(drizzleDb.schemas.setting)
|
||||||
.where(eq(drizzleDb.schemas.setting.name, "system"))
|
.where(eq(drizzleDb.schemas.setting.name, "system"))
|
||||||
.then((res) => res[0]);
|
.then((res) => res[0]);
|
||||||
|
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
throw new Error("SMTP system settings not found.");
|
throw new Error("SMTP system settings not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!settings.smtpHost || !settings.smtpPort || !settings.smtpUser || !settings.smtpPassword || !settings.smtpFrom) {
|
if (
|
||||||
console.warn("Incomplete SMTP settings. Email not sent.");
|
!settings.smtpHost ||
|
||||||
return;
|
!settings.smtpPort ||
|
||||||
}
|
!settings.smtpUser ||
|
||||||
|
!settings.smtpPassword ||
|
||||||
|
!settings.smtpFrom
|
||||||
|
) {
|
||||||
|
console.warn("Incomplete SMTP settings. Email not sent.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const emailsArray = data.to.split(",")
|
const emailsArray = data.to.split(",").map((email) => email.trim());
|
||||||
.map(email => email.trim());
|
|
||||||
|
|
||||||
const transporter = createTransporter({
|
const transporter = createTransporter({
|
||||||
host: settings.smtpHost,
|
host: settings.smtpHost,
|
||||||
port: Number(settings.smtpPort),
|
port: Number(settings.smtpPort),
|
||||||
user: settings.smtpUser,
|
user: settings.smtpUser,
|
||||||
pass: settings.smtpPassword,
|
pass: settings.smtpPassword,
|
||||||
from: settings.smtpFrom,
|
from: settings.smtpFrom,
|
||||||
secure: settings.smtpSecure ?? false,
|
secure: settings.smtpSecure ?? false,
|
||||||
});
|
});
|
||||||
await transporter.verify();
|
await transporter.verify();
|
||||||
|
|
||||||
return await transporter.sendMail({
|
return await transporter.sendMail({
|
||||||
...data,
|
...data,
|
||||||
to: emailsArray,
|
to: emailsArray,
|
||||||
from: settings.smtpFrom ?? undefined,
|
from: settings.smtpFrom ?? undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+10
-12
@@ -1,19 +1,17 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
|
||||||
export type Payload = {
|
export type Payload = {
|
||||||
to: string;
|
to: string;
|
||||||
from?: string;
|
from?: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
html: any;
|
html: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Server = {
|
export type Server = {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
user: string;
|
user: string;
|
||||||
pass: string;
|
pass: string;
|
||||||
from: string;
|
from: string;
|
||||||
secure: boolean;
|
secure: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,10 @@ export type ApiV1Result =
|
|||||||
| { ok: true; status: number; data: unknown }
|
| { ok: true; status: number; data: unknown }
|
||||||
| { ok: false; status: number; error: string };
|
| { ok: false; status: number; error: string };
|
||||||
|
|
||||||
/**
|
|
||||||
* Thin fetch wrapper that proxies calls to the existing /api/v1 REST layer.
|
|
||||||
* Forwards the caller's API key so the REST route runs its own auth + permission checks.
|
|
||||||
*/
|
|
||||||
export async function apiV1Fetch(
|
export async function apiV1Fetch(
|
||||||
path: string,
|
path: string,
|
||||||
options: RequestInit,
|
options: RequestInit,
|
||||||
apiKey: string
|
apiKey: string,
|
||||||
): Promise<ApiV1Result> {
|
): Promise<ApiV1Result> {
|
||||||
const url = `${getServerUrl()}${path}`;
|
const url = `${getServerUrl()}${path}`;
|
||||||
|
|
||||||
@@ -29,7 +25,6 @@ export async function apiV1Fetch(
|
|||||||
return { ok: false, status: 0, error: "Failed to reach internal API" };
|
return { ok: false, status: 0, error: "Failed to reach internal API" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 204 No Content has no body
|
|
||||||
if (res.status === 204) {
|
if (res.status === 204) {
|
||||||
return { ok: true, status: 204, data: null };
|
return { ok: true, status: 204, data: null };
|
||||||
}
|
}
|
||||||
@@ -53,9 +48,7 @@ export async function apiV1Fetch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data =
|
const data =
|
||||||
typeof body === "object" &&
|
typeof body === "object" && body !== null && "data" in body
|
||||||
body !== null &&
|
|
||||||
"data" in body
|
|
||||||
? (body as { data: unknown }).data
|
? (body as { data: unknown }).data
|
||||||
: body;
|
: body;
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { registerBackupTools } from "./tools/backups";
|
|||||||
*/
|
*/
|
||||||
export function createPortabaseMcpServer(
|
export function createPortabaseMcpServer(
|
||||||
_ctx: ApiKeyContext,
|
_ctx: ApiKeyContext,
|
||||||
apiKey: string
|
apiKey: string,
|
||||||
): McpServer {
|
): McpServer {
|
||||||
const server = new McpServer({
|
const server = new McpServer({
|
||||||
name: "portabase",
|
name: "portabase",
|
||||||
|
|||||||
+28
-14
@@ -1,8 +1,7 @@
|
|||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {apiV1Fetch} from "@/lib/mcp/http-client";
|
import { apiV1Fetch } from "@/lib/mcp/http-client";
|
||||||
import {err, ok} from "@/lib/mcp/tools/response";
|
import { err, ok } from "@/lib/mcp/tools/response";
|
||||||
|
|
||||||
|
|
||||||
export function registerAgentTools(server: McpServer, apiKey: string) {
|
export function registerAgentTools(server: McpServer, apiKey: string) {
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -10,9 +9,13 @@ export function registerAgentTools(server: McpServer, apiKey: string) {
|
|||||||
"List all agents accessible to the authenticated user",
|
"List all agents accessible to the authenticated user",
|
||||||
{},
|
{},
|
||||||
async () => {
|
async () => {
|
||||||
const result = await apiV1Fetch("/api/v1/agents", { method: "GET" }, apiKey);
|
const result = await apiV1Fetch(
|
||||||
|
"/api/v1/agents",
|
||||||
|
{ method: "GET" },
|
||||||
|
apiKey,
|
||||||
|
);
|
||||||
return result.ok ? ok(result.data) : err(result.error);
|
return result.ok ? ok(result.data) : err(result.error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -20,9 +23,13 @@ export function registerAgentTools(server: McpServer, apiKey: string) {
|
|||||||
"Get details for a specific agent, including its associated databases",
|
"Get details for a specific agent, including its associated databases",
|
||||||
{ id: z.string().describe("Agent ID") },
|
{ id: z.string().describe("Agent ID") },
|
||||||
async ({ id }) => {
|
async ({ id }) => {
|
||||||
const result = await apiV1Fetch(`/api/v1/agents/${id}`, { method: "GET" }, apiKey);
|
const result = await apiV1Fetch(
|
||||||
|
`/api/v1/agents/${id}`,
|
||||||
|
{ method: "GET" },
|
||||||
|
apiKey,
|
||||||
|
);
|
||||||
return result.ok ? ok(result.data) : err(result.error);
|
return result.ok ? ok(result.data) : err(result.error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -31,7 +38,6 @@ export function registerAgentTools(server: McpServer, apiKey: string) {
|
|||||||
{
|
{
|
||||||
name: z.string().min(1).describe("Agent name"),
|
name: z.string().min(1).describe("Agent name"),
|
||||||
organizationId: z
|
organizationId: z
|
||||||
.string()
|
|
||||||
.uuid()
|
.uuid()
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Organization ID to scope the agent to (optional)"),
|
.describe("Organization ID to scope the agent to (optional)"),
|
||||||
@@ -40,10 +46,10 @@ export function registerAgentTools(server: McpServer, apiKey: string) {
|
|||||||
const result = await apiV1Fetch(
|
const result = await apiV1Fetch(
|
||||||
"/api/v1/agents",
|
"/api/v1/agents",
|
||||||
{ method: "POST", body: JSON.stringify({ name, organizationId }) },
|
{ method: "POST", body: JSON.stringify({ name, organizationId }) },
|
||||||
apiKey
|
apiKey,
|
||||||
);
|
);
|
||||||
return result.ok ? ok(result.data) : err(result.error);
|
return result.ok ? ok(result.data) : err(result.error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -51,11 +57,15 @@ export function registerAgentTools(server: McpServer, apiKey: string) {
|
|||||||
"Delete an agent by ID",
|
"Delete an agent by ID",
|
||||||
{ id: z.string().describe("Agent ID") },
|
{ id: z.string().describe("Agent ID") },
|
||||||
async ({ id }) => {
|
async ({ id }) => {
|
||||||
const result = await apiV1Fetch(`/api/v1/agents/${id}`, { method: "DELETE" }, apiKey);
|
const result = await apiV1Fetch(
|
||||||
|
`/api/v1/agents/${id}`,
|
||||||
|
{ method: "DELETE" },
|
||||||
|
apiKey,
|
||||||
|
);
|
||||||
return result.ok
|
return result.ok
|
||||||
? ok({ message: `Agent ${id} deleted successfully` })
|
? ok({ message: `Agent ${id} deleted successfully` })
|
||||||
: err(result.error);
|
: err(result.error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -63,8 +73,12 @@ export function registerAgentTools(server: McpServer, apiKey: string) {
|
|||||||
"Get the edge key for an agent (used by the agent to authenticate with Portabase)",
|
"Get the edge key for an agent (used by the agent to authenticate with Portabase)",
|
||||||
{ id: z.string().describe("Agent ID") },
|
{ id: z.string().describe("Agent ID") },
|
||||||
async ({ id }) => {
|
async ({ id }) => {
|
||||||
const result = await apiV1Fetch(`/api/v1/agents/${id}/key`, { method: "GET" }, apiKey);
|
const result = await apiV1Fetch(
|
||||||
|
`/api/v1/agents/${id}/key`,
|
||||||
|
{ method: "GET" },
|
||||||
|
apiKey,
|
||||||
|
);
|
||||||
return result.ok ? ok(result.data) : err(result.error);
|
return result.ok ? ok(result.data) : err(result.error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {err, ok} from "@/lib/mcp/tools/response";
|
import { err, ok } from "@/lib/mcp/tools/response";
|
||||||
import {apiV1Fetch} from "@/lib/mcp/http-client";
|
import { apiV1Fetch } from "@/lib/mcp/http-client";
|
||||||
|
|
||||||
export function registerBackupTools(server: McpServer, apiKey: string) {
|
export function registerBackupTools(server: McpServer, apiKey: string) {
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -12,10 +12,10 @@ export function registerBackupTools(server: McpServer, apiKey: string) {
|
|||||||
const result = await apiV1Fetch(
|
const result = await apiV1Fetch(
|
||||||
`/api/v1/databases/${databaseId}/backup`,
|
`/api/v1/databases/${databaseId}/backup`,
|
||||||
{ method: "GET" },
|
{ method: "GET" },
|
||||||
apiKey
|
apiKey,
|
||||||
);
|
);
|
||||||
return result.ok ? ok(result.data) : err(result.error);
|
return result.ok ? ok(result.data) : err(result.error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -29,10 +29,10 @@ export function registerBackupTools(server: McpServer, apiKey: string) {
|
|||||||
const result = await apiV1Fetch(
|
const result = await apiV1Fetch(
|
||||||
`/api/v1/databases/${databaseId}/backup/${backupId}`,
|
`/api/v1/databases/${databaseId}/backup/${backupId}`,
|
||||||
{ method: "GET" },
|
{ method: "GET" },
|
||||||
apiKey
|
apiKey,
|
||||||
);
|
);
|
||||||
return result.ok ? ok(result.data) : err(result.error);
|
return result.ok ? ok(result.data) : err(result.error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -43,10 +43,10 @@ export function registerBackupTools(server: McpServer, apiKey: string) {
|
|||||||
const result = await apiV1Fetch(
|
const result = await apiV1Fetch(
|
||||||
`/api/v1/databases/${databaseId}/backup`,
|
`/api/v1/databases/${databaseId}/backup`,
|
||||||
{ method: "POST", body: JSON.stringify({}) },
|
{ method: "POST", body: JSON.stringify({}) },
|
||||||
apiKey
|
apiKey,
|
||||||
);
|
);
|
||||||
return result.ok ? ok(result.data) : err(result.error);
|
return result.ok ? ok(result.data) : err(result.error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -56,7 +56,6 @@ export function registerBackupTools(server: McpServer, apiKey: string) {
|
|||||||
databaseId: z.string().describe("Database ID"),
|
databaseId: z.string().describe("Database ID"),
|
||||||
backupId: z.string().uuid().describe("Backup ID"),
|
backupId: z.string().uuid().describe("Backup ID"),
|
||||||
backupStorageId: z
|
backupStorageId: z
|
||||||
.string()
|
|
||||||
.uuid()
|
.uuid()
|
||||||
.describe("Backup storage ID (from get_backup storages list)"),
|
.describe("Backup storage ID (from get_backup storages list)"),
|
||||||
},
|
},
|
||||||
@@ -67,9 +66,9 @@ export function registerBackupTools(server: McpServer, apiKey: string) {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ backupId, backupStorageId }),
|
body: JSON.stringify({ backupId, backupStorageId }),
|
||||||
},
|
},
|
||||||
apiKey
|
apiKey,
|
||||||
);
|
);
|
||||||
return result.ok ? ok(result.data) : err(result.error);
|
return result.ok ? ok(result.data) : err(result.error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,48 @@
|
|||||||
import {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import {z} from "zod";
|
import { z } from "zod";
|
||||||
import {err, ok} from "@/lib/mcp/tools/response";
|
import { err, ok } from "@/lib/mcp/tools/response";
|
||||||
import {apiV1Fetch} from "@/lib/mcp/http-client";
|
import { apiV1Fetch } from "@/lib/mcp/http-client";
|
||||||
|
|
||||||
|
|
||||||
export function registerDatabaseTools(server: McpServer, apiKey: string) {
|
export function registerDatabaseTools(server: McpServer, apiKey: string) {
|
||||||
server.tool(
|
server.tool(
|
||||||
"list_databases",
|
"list_databases",
|
||||||
"List all databases accessible to the authenticated user",
|
"List all databases accessible to the authenticated user",
|
||||||
{},
|
{},
|
||||||
async () => {
|
async () => {
|
||||||
const result = await apiV1Fetch("/api/v1/databases", {method: "GET"}, apiKey);
|
const result = await apiV1Fetch(
|
||||||
return result.ok ? ok(result.data) : err(result.error);
|
"/api/v1/databases",
|
||||||
}
|
{ method: "GET" },
|
||||||
);
|
apiKey,
|
||||||
|
);
|
||||||
|
return result.ok ? ok(result.data) : err(result.error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
"get_database",
|
"get_database",
|
||||||
"Get details for a specific database",
|
"Get details for a specific database",
|
||||||
{id: z.string().describe("Database ID")},
|
{ id: z.string().describe("Database ID") },
|
||||||
async ({id}) => {
|
async ({ id }) => {
|
||||||
const result = await apiV1Fetch(`/api/v1/databases/${id}`, {method: "GET"}, apiKey);
|
const result = await apiV1Fetch(
|
||||||
return result.ok ? ok(result.data) : err(result.error);
|
`/api/v1/databases/${id}`,
|
||||||
}
|
{ method: "GET" },
|
||||||
);
|
apiKey,
|
||||||
|
);
|
||||||
|
return result.ok ? ok(result.data) : err(result.error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
"get_database_status",
|
"get_database_status",
|
||||||
"Get the current status of a database, including latest backup and restoration state",
|
"Get the current status of a database, including latest backup and restoration state",
|
||||||
{id: z.string().describe("Database ID")},
|
{ id: z.string().describe("Database ID") },
|
||||||
async ({id}) => {
|
async ({ id }) => {
|
||||||
const result = await apiV1Fetch(
|
const result = await apiV1Fetch(
|
||||||
`/api/v1/databases/${id}/status`,
|
`/api/v1/databases/${id}/status`,
|
||||||
{method: "GET"},
|
{ method: "GET" },
|
||||||
apiKey
|
apiKey,
|
||||||
);
|
);
|
||||||
return result.ok ? ok(result.data) : err(result.error);
|
return result.ok ? ok(result.data) : err(result.error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
export function ok(data: unknown) {
|
export function ok(data: unknown) {
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function err(message: string) {
|
export function err(message: string) {
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text" as const, text: message }],
|
content: [{ type: "text" as const, text: message }],
|
||||||
isError: true as const,
|
isError: true as const,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import {createSafeActionClient} from "next-safe-action";
|
import { createSafeActionClient } from "next-safe-action";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
|
|
||||||
export class ActionError extends Error {
|
export class ActionError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "ActionError";
|
this.name = "ActionError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReturnedServerError = (error: Error) => {
|
const handleReturnedServerError = (error: Error) => {
|
||||||
if (error instanceof ActionError) {
|
if (error instanceof ActionError) {
|
||||||
return error.message;
|
return error.message;
|
||||||
} else {
|
} else {
|
||||||
return "An unexpected error occurred.";
|
return "An unexpected error occurred.";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const action = createSafeActionClient({
|
export const action = createSafeActionClient({
|
||||||
handleServerError: handleReturnedServerError,
|
handleServerError: handleReturnedServerError,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userAction = action.use(async ({next, ctx}) => {
|
export const userAction = action.use(async ({ next }) => {
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new ActionError("You must be logged in");
|
throw new ActionError("You must be logged in");
|
||||||
}
|
}
|
||||||
return next({ctx: {user}});
|
return next({ ctx: { user } });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,45 +1,51 @@
|
|||||||
import {db} from "@/db";
|
import { db } from "@/db";
|
||||||
import {eq, lt, and, desc, isNull} from "drizzle-orm";
|
import { eq, lt, and, isNull } from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
|
import { deleteBackupCronAction } from "@/lib/tasks/database/utils/delete";
|
||||||
import {logger} from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
|
|
||||||
const log = logger.child({module: "tasks/database/retention-days"});
|
const log = logger.child({ module: "tasks/database/retention-days" });
|
||||||
|
|
||||||
export async function enforceRetentionDays(databaseId: string, days: number) {
|
export async function enforceRetentionDays(databaseId: string, days: number) {
|
||||||
log.info({ name: "enforceRetentionDays"}, `Enforce Retention Days starting for ${databaseId}`);
|
log.info(
|
||||||
|
{ name: "enforceRetentionDays" },
|
||||||
|
`Enforce Retention Days starting for ${databaseId}`,
|
||||||
|
);
|
||||||
|
|
||||||
const cutoff = new Date(Date.now() - days * 86400000);
|
const cutoff = new Date(Date.now() - days * 86400000);
|
||||||
|
|
||||||
const expiredBackups = await db.query.backup.findMany({
|
const expiredBackups = await db.query.backup.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||||
lt(drizzleDb.schemas.backup.createdAt, cutoff),
|
lt(drizzleDb.schemas.backup.createdAt, cutoff),
|
||||||
isNull(drizzleDb.schemas.backup.deletedAt)
|
isNull(drizzleDb.schemas.backup.deletedAt),
|
||||||
),
|
),
|
||||||
|
with: {
|
||||||
|
database: {
|
||||||
with: {
|
with: {
|
||||||
database: {
|
project: true,
|
||||||
with: {
|
},
|
||||||
project: true
|
},
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
for (const backup of expiredBackups) {
|
||||||
|
const result = await deleteBackupCronAction({
|
||||||
|
backupId: backup.id,
|
||||||
|
databaseId: backup.databaseId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const backup of expiredBackups) {
|
const inner = result?.data;
|
||||||
|
if (inner?.success) {
|
||||||
const result = await deleteBackupCronAction({
|
log.info(
|
||||||
backupId: backup.id,
|
{ name: "enforceRetentionDays" },
|
||||||
databaseId: backup.databaseId,
|
`(databaseId:${backup.databaseId}) - (backupId: ${backup.id}) : successfully deleted`,
|
||||||
});
|
);
|
||||||
|
} else {
|
||||||
const inner = result?.data;
|
log.info(
|
||||||
if (inner?.success) {
|
{ name: "enforceRetentionDays" },
|
||||||
log.info({ name: "enforceRetentionDays"}, `(databaseId:${backup.databaseId}) - (backupId: ${backup.id}) : successfully deleted`);
|
`(databaseId:${backup.databaseId}) - (backupId: ${backup.id}) : an error occurred - ${inner?.actionError?.message}`,
|
||||||
} else {
|
);
|
||||||
log.info({ name: "enforceRetentionDays"}, `(databaseId:${backup.databaseId}) - (backupId: ${backup.id}) : an error occurred - ${inner?.actionError?.message}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,81 +1,106 @@
|
|||||||
import {db} from "@/db";
|
import { db } from "@/db";
|
||||||
import {subDays, subWeeks, subMonths, subYears, startOfWeek, startOfMonth, startOfYear} from "date-fns";
|
import {
|
||||||
import {eq, desc, isNull, and} from "drizzle-orm";
|
subDays,
|
||||||
|
subWeeks,
|
||||||
|
subMonths,
|
||||||
|
subYears,
|
||||||
|
startOfWeek,
|
||||||
|
startOfMonth,
|
||||||
|
startOfYear,
|
||||||
|
} from "date-fns";
|
||||||
|
import { eq, desc, isNull, and } from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
|
import { deleteBackupCronAction } from "@/lib/tasks/database/utils/delete";
|
||||||
import {logger} from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
|
|
||||||
const log = logger.child({module: "tasks/database/retention-gsf"});
|
const log = logger.child({ module: "tasks/database/retention-gsf" });
|
||||||
|
|
||||||
export async function enforceRetentionGFS(databaseId: string, gfsSettings: {
|
export async function enforceRetentionGFS(
|
||||||
|
databaseId: string,
|
||||||
|
gfsSettings: {
|
||||||
daily: number;
|
daily: number;
|
||||||
weekly: number;
|
weekly: number;
|
||||||
monthly: number;
|
monthly: number;
|
||||||
yearly: number;
|
yearly: number;
|
||||||
}) {
|
},
|
||||||
log.info({ name: "enforceRetentionGFS"}, `Retention GFS started for databaseId: ${databaseId}`);
|
) {
|
||||||
|
log.info(
|
||||||
|
{ name: "enforceRetentionGFS" },
|
||||||
|
`Retention GFS started for databaseId: ${databaseId}`,
|
||||||
|
);
|
||||||
|
|
||||||
const backups = await db.query.backup.findMany({
|
const backups = await db.query.backup.findMany({
|
||||||
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseId), isNull(drizzleDb.schemas.backup.deletedAt)),
|
where: and(
|
||||||
orderBy: desc(drizzleDb.schemas.backup.createdAt),
|
eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||||
|
isNull(drizzleDb.schemas.backup.deletedAt),
|
||||||
|
),
|
||||||
|
orderBy: desc(drizzleDb.schemas.backup.createdAt),
|
||||||
|
with: {
|
||||||
|
database: {
|
||||||
with: {
|
with: {
|
||||||
database: {
|
project: true,
|
||||||
with: {
|
},
|
||||||
project: true
|
},
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const toKeep: Set<string> = new Set();
|
const toKeep: Set<string> = new Set();
|
||||||
|
|
||||||
// DAILY
|
backups.forEach((b) => {
|
||||||
backups.forEach((b) => {
|
if (b.createdAt >= subDays(now, gfsSettings.daily)) toKeep.add(b.id);
|
||||||
if (b.createdAt >= subDays(now, gfsSettings.daily)) toKeep.add(b.id);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const weekStartDates = Array.from({length: gfsSettings.weekly}, (_, i) => startOfWeek(subWeeks(now, i), {weekStartsOn: 1}));
|
const weekStartDates = Array.from({ length: gfsSettings.weekly }, (_, i) =>
|
||||||
weekStartDates.forEach((weekStart) => {
|
startOfWeek(subWeeks(now, i), { weekStartsOn: 1 }),
|
||||||
const backupOfWeek = backups.find(
|
);
|
||||||
(b) => b.createdAt >= weekStart && b.createdAt < subWeeks(weekStart, -1)
|
weekStartDates.forEach((weekStart) => {
|
||||||
|
const backupOfWeek = backups.find(
|
||||||
|
(b) => b.createdAt >= weekStart && b.createdAt < subWeeks(weekStart, -1),
|
||||||
|
);
|
||||||
|
if (backupOfWeek) toKeep.add(backupOfWeek.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const monthStartDates = Array.from({ length: gfsSettings.monthly }, (_, i) =>
|
||||||
|
startOfMonth(subMonths(now, i)),
|
||||||
|
);
|
||||||
|
monthStartDates.forEach((monthStart) => {
|
||||||
|
const backupOfMonth = backups.find(
|
||||||
|
(b) =>
|
||||||
|
b.createdAt >= monthStart && b.createdAt < subMonths(monthStart, -1),
|
||||||
|
);
|
||||||
|
if (backupOfMonth) toKeep.add(backupOfMonth.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const yearStartDates = Array.from({ length: gfsSettings.yearly }, (_, i) =>
|
||||||
|
startOfYear(subYears(now, i)),
|
||||||
|
);
|
||||||
|
yearStartDates.forEach((yearStart) => {
|
||||||
|
const backupOfYear = backups.find(
|
||||||
|
(b) => b.createdAt >= yearStart && b.createdAt < subYears(yearStart, -1),
|
||||||
|
);
|
||||||
|
if (backupOfYear) toKeep.add(backupOfYear.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const b of backups) {
|
||||||
|
if (!toKeep.has(b.id)) {
|
||||||
|
const result = await deleteBackupCronAction({
|
||||||
|
backupId: b.id,
|
||||||
|
databaseId: b.databaseId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const inner = result?.data;
|
||||||
|
if (inner?.success) {
|
||||||
|
log.info(
|
||||||
|
{ name: "enforceRetentionGFS" },
|
||||||
|
`(databaseId:${b.databaseId}) - (backupId: ${b.id}) : successfully deleted`,
|
||||||
);
|
);
|
||||||
if (backupOfWeek) toKeep.add(backupOfWeek.id);
|
} else {
|
||||||
});
|
log.info(
|
||||||
|
{ name: "enforceRetentionGFS" },
|
||||||
// MONTHLY
|
`(databaseId:${b.databaseId}) - (backupId: ${b.id}) : an error occurred - ${inner?.actionError?.message}`,
|
||||||
const monthStartDates = Array.from({length: gfsSettings.monthly}, (_, i) => startOfMonth(subMonths(now, i)));
|
|
||||||
monthStartDates.forEach((monthStart) => {
|
|
||||||
const backupOfMonth = backups.find(
|
|
||||||
(b) => b.createdAt >= monthStart && b.createdAt < subMonths(monthStart, -1)
|
|
||||||
);
|
);
|
||||||
if (backupOfMonth) toKeep.add(backupOfMonth.id);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// YEARLY
|
|
||||||
const yearStartDates = Array.from({length: gfsSettings.yearly}, (_, i) => startOfYear(subYears(now, i)));
|
|
||||||
yearStartDates.forEach((yearStart) => {
|
|
||||||
const backupOfYear = backups.find(
|
|
||||||
(b) => b.createdAt >= yearStart && b.createdAt < subYears(yearStart, -1)
|
|
||||||
);
|
|
||||||
if (backupOfYear) toKeep.add(backupOfYear.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete backups not in `toKeep`
|
|
||||||
for (const b of backups) {
|
|
||||||
if (!toKeep.has(b.id)) {
|
|
||||||
|
|
||||||
const result = await deleteBackupCronAction({
|
|
||||||
backupId: b.id,
|
|
||||||
databaseId: b.databaseId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const inner = result?.data;
|
|
||||||
if (inner?.success) {
|
|
||||||
log.info({ name: "enforceRetentionGFS"}, `(databaseId:${b.databaseId}) - (backupId: ${b.id}) : successfully deleted`);
|
|
||||||
} else {
|
|
||||||
log.info({ name: "enforceRetentionGFS"}, `(databaseId:${b.databaseId}) - (backupId: ${b.id}) : an error occurred - ${inner?.actionError?.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use server"
|
"use server"
|
||||||
import {getMasterServerKeyContent} from "@/features/agents/keys.action";
|
import {getMasterServerKeyContent} from "@/features/agents/actions/keys.action";
|
||||||
|
|
||||||
export async function generateEdgeKey(serverUrl: string, agentId: string): Promise<string> {
|
export async function generateEdgeKey(serverUrl: string, agentId: string): Promise<string> {
|
||||||
const masterKey = await getMasterServerKeyContent()
|
const masterKey = await getMasterServerKeyContent()
|
||||||
|
|||||||
Reference in New Issue
Block a user