mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix: endpoints
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
import {withApiKey} from "@/lib/api-v1/middleware";
|
||||||
|
import {ApiKeyContext} from "@/lib/api-v1/types";
|
||||||
|
import {NextResponse} from "next/server";
|
||||||
|
import {getAgent, resolveAgentAccess} from "@/lib/api-v1/services/agents";
|
||||||
|
import {logger} from "@/lib/logger";
|
||||||
|
import {generateEdgeKey} from "@/utils/edge_key";
|
||||||
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
|
|
||||||
|
const log = logger.child({ module: "api/v1/agents/[id]/key" });
|
||||||
|
|
||||||
|
export const GET = withApiKey(
|
||||||
|
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
|
try {
|
||||||
|
const id = params?.id;
|
||||||
|
if (!id) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
|
||||||
|
const access = await resolveAgentAccess(id, ctx.user);
|
||||||
|
|
||||||
|
if (access === "forbidden") return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||||
|
if (access === "not_found") return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
|
||||||
|
const agent = await getAgent(id);
|
||||||
|
|
||||||
|
if (!agent) return NextResponse.json({ error: "Agent not found" }, { status: 404 });
|
||||||
|
|
||||||
|
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
|
||||||
|
|
||||||
|
return NextResponse.json({ data: edgeKey });
|
||||||
|
} catch (error) {
|
||||||
|
log.error({ error }, "Error in GET /api/v1/agents/[id]");
|
||||||
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,59 +1,38 @@
|
|||||||
import { NextResponse } from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import { withApiKey, ApiKeyContext } from "@/lib/api-v1/middleware";
|
import {withApiKey} from "@/lib/api-v1/middleware";
|
||||||
import { getAccessibleAgentIds } from "@/lib/api-v1/acl";
|
import {db} from "@/db";
|
||||||
import { db } from "@/db";
|
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import { eq, and, or, isNull } from "drizzle-orm";
|
import {eq, and, or, isNull} from "drizzle-orm";
|
||||||
import { withUpdatedAt } from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import {v4 as uuidv4} from "uuid";
|
||||||
import { logger } from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
import {ApiKeyContext} from "@/lib/api-v1/types";
|
||||||
|
import {getAgent, resolveAgentAccess} from "@/lib/api-v1/services/agents";
|
||||||
|
import {deleteAgentService} from "@/features/agents/agent-delete.action";
|
||||||
|
|
||||||
const log = logger.child({ module: "api/v1/agents/[id]" });
|
const log = logger.child({module: "api/v1/agents/[id]"});
|
||||||
|
|
||||||
async function resolveAgentAccess(id: string, userId: string) {
|
|
||||||
const accessibleAgentIds = await getAccessibleAgentIds(userId);
|
|
||||||
if (accessibleAgentIds.includes(id)) return "ok";
|
|
||||||
|
|
||||||
const exists = await db.query.agent.findFirst({
|
|
||||||
where: and(
|
|
||||||
eq(drizzleDb.schemas.agent.id, id),
|
|
||||||
or(
|
|
||||||
eq(drizzleDb.schemas.agent.isArchived, false),
|
|
||||||
isNull(drizzleDb.schemas.agent.isArchived)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
columns: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
return exists ? "forbidden" : "not_found";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GET = withApiKey(
|
export const GET = withApiKey(
|
||||||
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
try {
|
try {
|
||||||
const id = params?.id;
|
const id = params?.id;
|
||||||
if (!id) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
if (!id) return NextResponse.json({error: "Not found"}, {status: 404});
|
||||||
|
|
||||||
const access = await resolveAgentAccess(id, ctx.userId);
|
const access = await resolveAgentAccess(id, ctx.user);
|
||||||
if (access === "forbidden") return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
||||||
if (access === "not_found") return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({
|
if (access === "forbidden") return NextResponse.json({error: "Forbidden"}, {status: 403});
|
||||||
where: and(
|
if (access === "not_found") return NextResponse.json({error: "Not found"}, {status: 404});
|
||||||
eq(drizzleDb.schemas.agent.id, id),
|
|
||||||
or(
|
const agent = await getAgent(id, {
|
||||||
eq(drizzleDb.schemas.agent.isArchived, false),
|
includeDatabases: true,
|
||||||
isNull(drizzleDb.schemas.agent.isArchived)
|
includeOrganizations: false,
|
||||||
)
|
|
||||||
),
|
|
||||||
with: { databases: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!agent) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
if (!agent) return NextResponse.json({error: "Not found"}, {status: 404});
|
||||||
return NextResponse.json({ data: agent });
|
return NextResponse.json({data: agent});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error({ error }, "Error in GET /api/v1/agents/[id]");
|
log.error({error}, "Error in GET /api/v1/agents/[id]");
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
return NextResponse.json({error: "Internal server error"}, {status: 500});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -62,21 +41,31 @@ export const DELETE = withApiKey(
|
|||||||
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
try {
|
try {
|
||||||
const id = params?.id;
|
const id = params?.id;
|
||||||
if (!id) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
if (!id) return NextResponse.json({error: "Not found"}, {status: 404});
|
||||||
|
|
||||||
const access = await resolveAgentAccess(id, ctx.userId);
|
const access = await resolveAgentAccess(id, ctx.user);
|
||||||
if (access === "forbidden") return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
if (access === "forbidden") return NextResponse.json({error: "Forbidden"}, {status: 403});
|
||||||
if (access === "not_found") return NextResponse.json({ error: "Not found" }, { status: 404 });
|
if (access === "not_found") return NextResponse.json({error: "Not found"}, {status: 404});
|
||||||
|
|
||||||
await db
|
const agent = await getAgent(id, {
|
||||||
.update(drizzleDb.schemas.agent)
|
includeDatabases: false,
|
||||||
.set(withUpdatedAt({ isArchived: true, slug: uuidv4(), deletedAt: new Date() }))
|
includeOrganizations: true,
|
||||||
.where(eq(drizzleDb.schemas.agent.id, id));
|
});
|
||||||
|
|
||||||
return new Response(null, { status: 204 });
|
if (!agent) return NextResponse.json({error: "Agent no found"}, {status: 404});
|
||||||
|
|
||||||
|
const organizationIds = agent.organizations.map(org => org.organizationId)
|
||||||
|
|
||||||
|
await deleteAgentService({
|
||||||
|
agentId: agent.id,
|
||||||
|
organizationId: agent.organizationId ?? undefined,
|
||||||
|
organizationIds: organizationIds
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(null, {status: 204});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error({ error }, "Error in DELETE /api/v1/agents/[id]");
|
log.error({error}, "Error in DELETE /api/v1/agents/[id]");
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
return NextResponse.json({error: "Internal server error"}, {status: 500});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,29 +2,29 @@ import { NextResponse } from "next/server";
|
|||||||
import { withApiKey } from "@/lib/api-v1/middleware";
|
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import { eq, and, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import {getAccessibleDatabaseIds} from "@/lib/api-v1/services/databases";
|
import { ApiKeyContext } from "@/lib/api-v1/types";
|
||||||
import {ApiKeyContext} from "@/lib/api-v1/types";
|
import {requireDatabaseAccess} from "@/lib/api-v1/services/databases";
|
||||||
|
|
||||||
const log = logger.child({ module: "api/v1/databases/[id]/backup/[backupId]" });
|
const log = logger.child({
|
||||||
|
module: "api/v1/databases/[id]/backup/[backupId]",
|
||||||
|
});
|
||||||
|
|
||||||
export const GET = withApiKey(
|
export const GET = withApiKey(
|
||||||
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
try {
|
try {
|
||||||
const { id, backupId } = params ?? {};
|
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||||
if (!id || !backupId) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
||||||
|
|
||||||
const accessibleIds = await getAccessibleDatabaseIds(ctx.user);
|
if (!guard.ok) {
|
||||||
if (!accessibleIds.includes(id)) {
|
return guard.response;
|
||||||
const exists = await db.query.database.findFirst({
|
}
|
||||||
where: eq(drizzleDb.schemas.database.id, id),
|
|
||||||
columns: { id: true },
|
const { id } = guard.data;
|
||||||
});
|
const backupId = params?.backupId;
|
||||||
return NextResponse.json(
|
|
||||||
{ error: exists ? "Forbidden" : "Not found" },
|
if (!backupId) {
|
||||||
{ status: exists ? 403 : 404 }
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const backup = await db.query.backup.findFirst({
|
const backup = await db.query.backup.findFirst({
|
||||||
@@ -35,16 +35,27 @@ export const GET = withApiKey(
|
|||||||
),
|
),
|
||||||
with: {
|
with: {
|
||||||
storages: {
|
storages: {
|
||||||
where: (bs, { isNull }) => isNull(bs.deletedAt),
|
where: (backupStorage, { isNull }) =>
|
||||||
|
isNull(backupStorage.deletedAt),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!backup) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
if (!backup) {
|
||||||
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ data: backup });
|
return NextResponse.json({ data: backup });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error({ error }, "Error in GET /api/v1/databases/[id]/backup/[backupId]");
|
log.error(
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
{ error },
|
||||||
|
"Error in GET /api/v1/databases/[id]/backup/[backupId]"
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -2,33 +2,23 @@ import { NextResponse } from "next/server";
|
|||||||
import { withApiKey } from "@/lib/api-v1/middleware";
|
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import { eq, desc, isNull, and } from "drizzle-orm";
|
import {and, desc, eq, inArray, isNull} from "drizzle-orm";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import {getAccessibleDatabaseIds} from "@/lib/api-v1/services/databases";
|
import { ApiKeyContext } from "@/lib/api-v1/types";
|
||||||
import {ApiKeyContext, ApiKeyContextUser} from "@/lib/api-v1/types";
|
import {requireDatabaseAccess} from "@/lib/api-v1/services/databases";
|
||||||
|
|
||||||
const log = logger.child({ module: "api/v1/databases/[id]/backup" });
|
const log = logger.child({ module: "api/v1/databases/[id]/backup" });
|
||||||
|
|
||||||
async function resolveDatabaseAccess(id: string, user: ApiKeyContextUser) {
|
|
||||||
const accessibleIds = await getAccessibleDatabaseIds(user);
|
|
||||||
if (accessibleIds.includes(id)) return "ok";
|
|
||||||
const exists = await db.query.database.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.database.id, id),
|
|
||||||
columns: { id: true },
|
|
||||||
});
|
|
||||||
return exists ? "forbidden" : "not_found";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GET = withApiKey(
|
export const GET = withApiKey(
|
||||||
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
try {
|
try {
|
||||||
const id = params?.id;
|
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||||
|
|
||||||
if (!id) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
if (!guard.ok) {
|
||||||
|
return guard.response;
|
||||||
|
}
|
||||||
|
|
||||||
const access = await resolveDatabaseAccess(id, ctx.user);
|
const { id } = guard.data;
|
||||||
if (access === "forbidden") return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
||||||
if (access === "not_found") return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
||||||
|
|
||||||
const backups = await db.query.backup.findMany({
|
const backups = await db.query.backup.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
@@ -41,7 +31,11 @@ export const GET = withApiKey(
|
|||||||
return NextResponse.json({ data: backups });
|
return NextResponse.json({ data: backups });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error({ error }, "Error in GET /api/v1/databases/[id]/backup");
|
log.error({ error }, "Error in GET /api/v1/databases/[id]/backup");
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -49,26 +43,54 @@ export const GET = withApiKey(
|
|||||||
export const POST = withApiKey(
|
export const POST = withApiKey(
|
||||||
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
try {
|
try {
|
||||||
const id = params?.id;
|
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||||
if (!id) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
||||||
|
|
||||||
const access = await resolveDatabaseAccess(id, ctx.user);
|
if (!guard.ok) {
|
||||||
if (access === "forbidden") return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
return guard.response;
|
||||||
if (access === "not_found") return NextResponse.json({ error: "Not found" }, { status: 404 });
|
}
|
||||||
|
|
||||||
|
const { id } = guard.data;
|
||||||
|
|
||||||
|
const existingBackup = await db.query.backup.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.backup.databaseId, id),
|
||||||
|
inArray(drizzleDb.schemas.backup.status, ["waiting", "ongoing"])
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingBackup) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error:
|
||||||
|
"A backup is already waiting or ongoing for this database",
|
||||||
|
},
|
||||||
|
{ status: 409 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const [createdBackup] = await db
|
const [createdBackup] = await db
|
||||||
.insert(drizzleDb.schemas.backup)
|
.insert(drizzleDb.schemas.backup)
|
||||||
.values({ databaseId: id, status: "waiting" })
|
.values({
|
||||||
|
databaseId: id,
|
||||||
|
status: "waiting",
|
||||||
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (!createdBackup) {
|
if (!createdBackup) {
|
||||||
return NextResponse.json({ error: "Failed to create backup" }, { status: 500 });
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to create backup" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ data: createdBackup }, { status: 201 });
|
return NextResponse.json({ data: createdBackup }, { status: 201 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error({ error }, "Error in POST /api/v1/databases/[id]/backup");
|
log.error({ error }, "Error in POST /api/v1/databases/[id]/backup");
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -2,11 +2,12 @@ import { NextResponse } from "next/server";
|
|||||||
import { withApiKey } from "@/lib/api-v1/middleware";
|
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import { eq, and, isNull } from "drizzle-orm";
|
import {and, eq, inArray, isNull} from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
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 {getAccessibleDatabaseIds} from "@/lib/api-v1/services/databases";
|
import { parseJsonBody } from "@/lib/api-v1/validation/json-body";
|
||||||
|
import {requireDatabaseAccess} from "@/lib/api-v1/services/databases";
|
||||||
|
|
||||||
const log = logger.child({ module: "api/v1/databases/[id]/restore" });
|
const log = logger.child({ module: "api/v1/databases/[id]/restore" });
|
||||||
|
|
||||||
@@ -18,50 +19,38 @@ const RestoreSchema = z.object({
|
|||||||
export const POST = withApiKey(
|
export const POST = withApiKey(
|
||||||
async (req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
async (req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
try {
|
try {
|
||||||
const id = params?.id;
|
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||||
if (!id) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
||||||
|
|
||||||
const accessibleIds = await getAccessibleDatabaseIds(ctx.user);
|
if (!guard.ok) {
|
||||||
if (!accessibleIds.includes(id)) {
|
return guard.response;
|
||||||
const exists = await db.query.database.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.database.id, id),
|
|
||||||
columns: { id: true },
|
|
||||||
});
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: exists ? "Forbidden" : "Not found" },
|
|
||||||
{ status: exists ? 403 : 404 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let body: unknown;
|
const { id } = guard.data;
|
||||||
try {
|
|
||||||
body = await req.json();
|
const body = await parseJsonBody(req, RestoreSchema);
|
||||||
} catch {
|
|
||||||
return NextResponse.json({ error: "Invalid JSON body" }, { status: 422 });
|
if (!body.ok) {
|
||||||
|
return body.response;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = RestoreSchema.safeParse(body);
|
const { backupId, backupStorageId } = body.data;
|
||||||
if (!parsed.success) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: parsed.error.issues[0].message },
|
|
||||||
{ status: 422 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { backupId, backupStorageId } = parsed.data;
|
|
||||||
|
|
||||||
// Validate backup belongs to this database
|
|
||||||
const backupRecord = await db.query.backup.findFirst({
|
const backupRecord = await db.query.backup.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
eq(drizzleDb.schemas.backup.id, backupId),
|
eq(drizzleDb.schemas.backup.id, backupId),
|
||||||
eq(drizzleDb.schemas.backup.databaseId, id),
|
eq(drizzleDb.schemas.backup.databaseId, id),
|
||||||
isNull(drizzleDb.schemas.backup.deletedAt)
|
isNull(drizzleDb.schemas.backup.deletedAt)
|
||||||
),
|
),
|
||||||
columns: { id: true },
|
columns: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!backupRecord) {
|
if (!backupRecord) {
|
||||||
return NextResponse.json({ error: "Backup not found for this database" }, { status: 404 });
|
return NextResponse.json(
|
||||||
|
{ error: "Backup not found for this database" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const backupStorage = await db.query.backupStorage.findFirst({
|
const backupStorage = await db.query.backupStorage.findFirst({
|
||||||
@@ -73,7 +62,10 @@ export const POST = withApiKey(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!backupStorage) {
|
if (!backupStorage) {
|
||||||
return NextResponse.json({ error: "Backup storage not found" }, { status: 404 });
|
return NextResponse.json(
|
||||||
|
{ error: "Backup storage not found" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (backupStorage.status !== "success") {
|
if (backupStorage.status !== "success") {
|
||||||
@@ -83,6 +75,23 @@ export const POST = withApiKey(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingRestorationBackup = await db.query.restoration.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.restoration.databaseId, id),
|
||||||
|
inArray(drizzleDb.schemas.restoration.status, ["waiting", "ongoing"])
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingRestorationBackup) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error:
|
||||||
|
"A restoration is already waiting or ongoing for this database",
|
||||||
|
},
|
||||||
|
{ status: 409 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const [restoration] = await db
|
const [restoration] = await db
|
||||||
.insert(drizzleDb.schemas.restoration)
|
.insert(drizzleDb.schemas.restoration)
|
||||||
.values({
|
.values({
|
||||||
@@ -94,13 +103,20 @@ export const POST = withApiKey(
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (!restoration) {
|
if (!restoration) {
|
||||||
return NextResponse.json({ error: "Failed to create restoration" }, { status: 500 });
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to create restoration" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ data: restoration }, { status: 201 });
|
return NextResponse.json({ data: restoration }, { status: 201 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error({ error }, "Error in POST /api/v1/databases/[id]/restore");
|
log.error({ error }, "Error in POST /api/v1/databases/[id]/restore");
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -2,40 +2,43 @@ import { NextResponse } from "next/server";
|
|||||||
import { withApiKey } from "@/lib/api-v1/middleware";
|
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import {getAccessibleDatabaseIds} from "@/lib/api-v1/services/databases";
|
import { ApiKeyContext } from "@/lib/api-v1/types";
|
||||||
import {ApiKeyContext} from "@/lib/api-v1/types";
|
import {requireDatabaseAccess} from "@/lib/api-v1/services/databases";
|
||||||
|
|
||||||
const log = logger.child({ module: "api/v1/databases/[id]" });
|
const log = logger.child({ module: "api/v1/databases/[id]" });
|
||||||
|
|
||||||
export const GET = withApiKey(
|
export const GET = withApiKey(
|
||||||
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
try {
|
try {
|
||||||
const id = params?.id;
|
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||||
if (!id) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
||||||
|
|
||||||
const accessibleIds = await getAccessibleDatabaseIds(ctx.user);
|
if (!guard.ok) {
|
||||||
if (!accessibleIds.includes(id)) {
|
return guard.response;
|
||||||
const exists = await db.query.database.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.database.id, id),
|
|
||||||
columns: { id: true },
|
|
||||||
});
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: exists ? "Forbidden" : "Not found" },
|
|
||||||
{ status: exists ? 403 : 404 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { id } = guard.data;
|
||||||
|
|
||||||
const database = await db.query.database.findFirst({
|
const database = await db.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.id, id),
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.database.id, id),
|
||||||
|
isNull(drizzleDb.schemas.database.deletedAt)
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!database) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
if (!database) {
|
||||||
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ data: database });
|
return NextResponse.json({ data: database });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error({ error }, "Error in GET /api/v1/databases/[id]");
|
log.error({ error }, "Error in GET /api/v1/databases/[id]");
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -2,38 +2,32 @@ import { NextResponse } from "next/server";
|
|||||||
import { withApiKey } from "@/lib/api-v1/middleware";
|
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import { eq, desc, and, isNull } from "drizzle-orm";
|
import { and, desc, eq, isNull } from "drizzle-orm";
|
||||||
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 {getAccessibleDatabaseIds} from "@/lib/api-v1/services/databases";
|
import {requireDatabaseAccess} from "@/lib/api-v1/services/databases";
|
||||||
|
|
||||||
const log = logger.child({ module: "api/v1/databases/[id]/status" });
|
const log = logger.child({ module: "api/v1/databases/[id]/status" });
|
||||||
|
|
||||||
export const GET = withApiKey(
|
export const GET = withApiKey(
|
||||||
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||||
try {
|
try {
|
||||||
const id = params?.id;
|
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||||
if (!id) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
||||||
|
|
||||||
const accessibleIds = await getAccessibleDatabaseIds(ctx.user);
|
if (!guard.ok) {
|
||||||
if (!accessibleIds.includes(id)) {
|
return guard.response;
|
||||||
const exists = await db.query.database.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.database.id, id),
|
|
||||||
columns: { id: true },
|
|
||||||
});
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: exists ? "Forbidden" : "Not found" },
|
|
||||||
{ status: exists ? 403 : 404 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { id } = guard.data;
|
||||||
|
|
||||||
const [database, latestBackup, latestRestoration] = await Promise.all([
|
const [database, latestBackup, latestRestoration] = await Promise.all([
|
||||||
db.query.database.findFirst({
|
db.query.database.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
eq(drizzleDb.schemas.database.id, id),
|
eq(drizzleDb.schemas.database.id, id),
|
||||||
isNull(drizzleDb.schemas.database.deletedAt)
|
isNull(drizzleDb.schemas.database.deletedAt)
|
||||||
)
|
),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
db.query.backup.findFirst({
|
db.query.backup.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
eq(drizzleDb.schemas.backup.databaseId, id),
|
eq(drizzleDb.schemas.backup.databaseId, id),
|
||||||
@@ -41,6 +35,7 @@ export const GET = withApiKey(
|
|||||||
),
|
),
|
||||||
orderBy: [desc(drizzleDb.schemas.backup.createdAt)],
|
orderBy: [desc(drizzleDb.schemas.backup.createdAt)],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
db.query.restoration.findFirst({
|
db.query.restoration.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
eq(drizzleDb.schemas.restoration.databaseId, id),
|
eq(drizzleDb.schemas.restoration.databaseId, id),
|
||||||
@@ -50,8 +45,11 @@ export const GET = withApiKey(
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!database){
|
if (!database) {
|
||||||
return NextResponse.json({ error: "Database not found" });
|
return NextResponse.json(
|
||||||
|
{ error: "Database not found" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@@ -64,7 +62,11 @@ export const GET = withApiKey(
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error({ error }, "Error in GET /api/v1/databases/[id]/status");
|
log.error({ error }, "Error in GET /api/v1/databases/[id]/status");
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -1,32 +1,24 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { withApiKey } from "@/lib/api-v1/middleware";
|
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||||
import { db } from "@/db";
|
|
||||||
import * as drizzleDb from "@/db";
|
|
||||||
import { inArray, and, isNull } from "drizzle-orm";
|
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import {getAccessibleAgentIds} from "@/lib/api-v1/services/agents";
|
import { ApiKeyContext } from "@/lib/api-v1/types";
|
||||||
import {ApiKeyContext} from "@/lib/api-v1/types";
|
import { getAccessibleDatabases } from "@/lib/api-v1/services/databases";
|
||||||
|
|
||||||
const log = logger.child({ module: "api/v1/databases" });
|
const log = logger.child({ module: "api/v1/databases" });
|
||||||
|
|
||||||
export const GET = withApiKey(async (_req: Request, ctx: ApiKeyContext) => {
|
export const GET = withApiKey(async (_req: Request, ctx: ApiKeyContext) => {
|
||||||
try {
|
try {
|
||||||
const agentIds = await getAccessibleAgentIds(ctx.user);
|
const databases = await getAccessibleDatabases(ctx.user);
|
||||||
|
|
||||||
if (agentIds.length === 0) {
|
return NextResponse.json({
|
||||||
return NextResponse.json({ data: [] });
|
data: databases,
|
||||||
}
|
|
||||||
|
|
||||||
const databases = await db.query.database.findMany({
|
|
||||||
where: and(
|
|
||||||
inArray(drizzleDb.schemas.database.agentId, agentIds),
|
|
||||||
isNull(drizzleDb.schemas.database.deletedAt)
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({ data: databases });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error({ error }, "Error in GET /api/v1/databases");
|
log.error({ error }, "Error in GET /api/v1/databases");
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -76,6 +76,7 @@ function checkRouteExists(pathname: string) {
|
|||||||
// v1 external API
|
// v1 external API
|
||||||
/^\/api\/v1\/agents\/?$/,
|
/^\/api\/v1\/agents\/?$/,
|
||||||
/^\/api\/v1\/agents\/[^/]+\/?$/,
|
/^\/api\/v1\/agents\/[^/]+\/?$/,
|
||||||
|
/^\/api\/v1\/agents\/[^/]+\/key\/?$/,
|
||||||
/^\/api\/v1\/databases\/?$/,
|
/^\/api\/v1\/databases\/?$/,
|
||||||
/^\/api\/v1\/databases\/[^/]+\/?$/,
|
/^\/api\/v1\/databases\/[^/]+\/?$/,
|
||||||
/^\/api\/v1\/databases\/[^/]+\/backup\/?$/,
|
/^\/api\/v1\/databases\/[^/]+\/backup\/?$/,
|
||||||
|
|||||||
@@ -10,22 +10,168 @@ import {Agent} from "@/db/schema/08_agent";
|
|||||||
import {userAction} from "@/lib/safe-actions/actions";
|
import {userAction} from "@/lib/safe-actions/actions";
|
||||||
import {zString} from "@/lib/zod";
|
import {zString} from "@/lib/zod";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
import {AgentSchema} from "@/features/agents/agents.schema";
|
||||||
|
import {slugify} from "@/utils/slugify";
|
||||||
|
|
||||||
export const deleteAgentAction = userAction
|
|
||||||
.schema(
|
|
||||||
z.object({
|
|
||||||
agentId: zString(),
|
|
||||||
organizationId: zString().optional(),
|
|
||||||
organizationIds: z.array(z.string()).optional()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.action(async ({parsedInput}): Promise<ServerActionResult<Agent>> => {
|
|
||||||
const {agentId, organizationId, organizationIds} = parsedInput;
|
|
||||||
|
|
||||||
try {
|
//
|
||||||
|
// type DeleteAgentInput = {
|
||||||
|
// organizationId?: string;
|
||||||
|
// agentId: string;
|
||||||
|
// organizationIds?: string[];
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// export async function deleteAgentService(input: DeleteAgentInput) {
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// export const deleteAgentAction = userAction
|
||||||
|
// .schema(
|
||||||
|
// z.object({
|
||||||
|
// agentId: zString(),
|
||||||
|
// organizationId: zString().optional(),
|
||||||
|
// organizationIds: z.array(z.string()).optional()
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// .action(async ({parsedInput}): Promise<ServerActionResult<Agent>> => {
|
||||||
|
// const {agentId, organizationId, organizationIds} = parsedInput;
|
||||||
|
//
|
||||||
|
// try {
|
||||||
|
// let projectIds: string[] = [];
|
||||||
|
//
|
||||||
|
// const uuid = uuidv4();
|
||||||
|
// if (organizationId) {
|
||||||
|
// await db
|
||||||
|
// .delete(drizzleDb.schemas.organizationAgent)
|
||||||
|
// .where(
|
||||||
|
// and(
|
||||||
|
// eq(drizzleDb.schemas.organizationAgent.organizationId, organizationId),
|
||||||
|
// eq(drizzleDb.schemas.organizationAgent.agentId, agentId)
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// const organization = await db.query.organization.findFirst({
|
||||||
|
// where: eq(drizzleDb.schemas.organization.id, organizationId),
|
||||||
|
// with: {
|
||||||
|
// projects: true,
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// projectIds = organization?.projects?.map(project => project.id) ?? [];
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// } else if (organizationIds) {
|
||||||
|
//
|
||||||
|
// const organizationsToRemoveDetails = await db.query.organization.findMany({
|
||||||
|
// where: inArray(drizzleDb.schemas.organization.id, organizationIds),
|
||||||
|
// with: {
|
||||||
|
// projects: true
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// projectIds = organizationsToRemoveDetails.flatMap(org =>
|
||||||
|
// org.projects.map(project => project.id)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// if (projectIds?.length > 0) {
|
||||||
|
// const databases = await db.query.database.findMany({
|
||||||
|
// where: (db, {inArray}) => inArray(db.projectId, projectIds),
|
||||||
|
// columns: {id: true}
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// const databaseIds = databases.map(d => d.id);
|
||||||
|
//
|
||||||
|
// await db
|
||||||
|
// .update(drizzleDb.schemas.database)
|
||||||
|
// .set(withUpdatedAt({
|
||||||
|
// backupPolicy: null,
|
||||||
|
// projectId: null
|
||||||
|
// }))
|
||||||
|
// .where(inArray(drizzleDb.schemas.database.projectId, projectIds))
|
||||||
|
// .execute();
|
||||||
|
//
|
||||||
|
// await db.delete(drizzleDb.schemas.retentionPolicy)
|
||||||
|
// .where(inArray(drizzleDb.schemas.retentionPolicy.databaseId, databaseIds))
|
||||||
|
// .execute();
|
||||||
|
//
|
||||||
|
// await db.delete(drizzleDb.schemas.alertPolicy)
|
||||||
|
// .where(inArray(drizzleDb.schemas.alertPolicy.databaseId, databaseIds))
|
||||||
|
// .execute();
|
||||||
|
//
|
||||||
|
// await db.delete(drizzleDb.schemas.storagePolicy)
|
||||||
|
// .where(inArray(drizzleDb.schemas.storagePolicy.databaseId, databaseIds))
|
||||||
|
// .execute();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// const updatedAgent = await db
|
||||||
|
// .update(drizzleDb.schemas.agent)
|
||||||
|
// .set(withUpdatedAt({
|
||||||
|
// isArchived: true,
|
||||||
|
// slug: uuid,
|
||||||
|
// deletedAt: new Date()
|
||||||
|
// }))
|
||||||
|
// .where(eq(drizzleDb.schemas.agent.id, agentId))
|
||||||
|
// .returning();
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// if (!updatedAgent[0]) {
|
||||||
|
// return {
|
||||||
|
// success: false,
|
||||||
|
// actionError: {
|
||||||
|
// message: "Agent not found or update failed",
|
||||||
|
// status: 404,
|
||||||
|
// messageParams: {agentId: agentId},
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return {
|
||||||
|
// success: true,
|
||||||
|
// value: updatedAgent[0],
|
||||||
|
// actionSuccess: {
|
||||||
|
// message: "Agent has been successfully deleted.",
|
||||||
|
// messageParams: {projectId: agentId},
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// } catch (error) {
|
||||||
|
// return {
|
||||||
|
// success: false,
|
||||||
|
// actionError: {
|
||||||
|
// message: "Failed to delete agent.",
|
||||||
|
// status: 500,
|
||||||
|
// cause: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
// messageParams: {agentId: agentId},
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
type DeleteAgentInput = {
|
||||||
|
organizationId?: string;
|
||||||
|
agentId: string;
|
||||||
|
organizationIds?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
class AgentNotFoundError extends Error {
|
||||||
|
constructor(agentId: string) {
|
||||||
|
super(`Agent not found or update failed: ${agentId}`);
|
||||||
|
this.name = "AgentNotFoundError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAgentService(input: DeleteAgentInput): Promise<Agent> {
|
||||||
|
const {agentId, organizationId, organizationIds} = input;
|
||||||
|
|
||||||
let projectIds: string[] = [];
|
let projectIds: string[] = [];
|
||||||
|
|
||||||
const uuid = uuidv4();
|
const uuid = uuidv4();
|
||||||
|
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
await db
|
await db
|
||||||
.delete(drizzleDb.schemas.organizationAgent)
|
.delete(drizzleDb.schemas.organizationAgent)
|
||||||
@@ -40,97 +186,141 @@ export const deleteAgentAction = userAction
|
|||||||
where: eq(drizzleDb.schemas.organization.id, organizationId),
|
where: eq(drizzleDb.schemas.organization.id, organizationId),
|
||||||
with: {
|
with: {
|
||||||
projects: true,
|
projects: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
projectIds = organization?.projects?.map(project => project.id) ?? [];
|
projectIds = organization?.projects?.map((project) => project.id) ?? [];
|
||||||
|
} else if (organizationIds?.length) {
|
||||||
|
|
||||||
} else if (organizationIds) {
|
|
||||||
|
|
||||||
const organizationsToRemoveDetails = await db.query.organization.findMany({
|
const organizationsToRemoveDetails = await db.query.organization.findMany({
|
||||||
where: inArray(drizzleDb.schemas.organization.id, organizationIds),
|
where: inArray(drizzleDb.schemas.organization.id, organizationIds),
|
||||||
with: {
|
with: {
|
||||||
projects: true
|
projects: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
projectIds = organizationsToRemoveDetails.flatMap(org =>
|
projectIds = organizationsToRemoveDetails.flatMap((org) =>
|
||||||
org.projects.map(project => project.id)
|
org.projects.map((project) => project.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (projectIds.length > 0) {
|
||||||
if (projectIds?.length > 0) {
|
|
||||||
const databases = await db.query.database.findMany({
|
const databases = await db.query.database.findMany({
|
||||||
where: (db, {inArray}) => inArray(db.projectId, projectIds),
|
where: (database, {inArray}) => inArray(database.projectId, projectIds),
|
||||||
columns: {id: true}
|
columns: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const databaseIds = databases.map(d => d.id);
|
const databaseIds = databases.map((database) => database.id);
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(drizzleDb.schemas.database)
|
.update(drizzleDb.schemas.database)
|
||||||
.set(withUpdatedAt({
|
.set(
|
||||||
|
withUpdatedAt({
|
||||||
backupPolicy: null,
|
backupPolicy: null,
|
||||||
projectId: null
|
projectId: null,
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
.where(inArray(drizzleDb.schemas.database.projectId, projectIds))
|
.where(inArray(drizzleDb.schemas.database.projectId, projectIds))
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.delete(drizzleDb.schemas.retentionPolicy)
|
if (databaseIds.length > 0) {
|
||||||
.where(inArray(drizzleDb.schemas.retentionPolicy.databaseId, databaseIds))
|
await db
|
||||||
|
.delete(drizzleDb.schemas.retentionPolicy)
|
||||||
|
.where(
|
||||||
|
inArray(
|
||||||
|
drizzleDb.schemas.retentionPolicy.databaseId,
|
||||||
|
databaseIds
|
||||||
|
)
|
||||||
|
)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.delete(drizzleDb.schemas.alertPolicy)
|
await db
|
||||||
.where(inArray(drizzleDb.schemas.alertPolicy.databaseId, databaseIds))
|
.delete(drizzleDb.schemas.alertPolicy)
|
||||||
|
.where(
|
||||||
|
inArray(
|
||||||
|
drizzleDb.schemas.alertPolicy.databaseId,
|
||||||
|
databaseIds
|
||||||
|
)
|
||||||
|
)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.delete(drizzleDb.schemas.storagePolicy)
|
await db
|
||||||
.where(inArray(drizzleDb.schemas.storagePolicy.databaseId, databaseIds))
|
.delete(drizzleDb.schemas.storagePolicy)
|
||||||
|
.where(
|
||||||
|
inArray(
|
||||||
|
drizzleDb.schemas.storagePolicy.databaseId,
|
||||||
|
databaseIds
|
||||||
|
)
|
||||||
|
)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updatedAgent] = await db
|
||||||
const updatedAgent = await db
|
|
||||||
.update(drizzleDb.schemas.agent)
|
.update(drizzleDb.schemas.agent)
|
||||||
.set(withUpdatedAt({
|
.set(
|
||||||
|
withUpdatedAt({
|
||||||
isArchived: true,
|
isArchived: true,
|
||||||
slug: uuid,
|
slug: uuid,
|
||||||
deletedAt: new Date()
|
deletedAt: new Date(),
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
.where(eq(drizzleDb.schemas.agent.id, agentId))
|
.where(eq(drizzleDb.schemas.agent.id, agentId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
if (!updatedAgent) {
|
||||||
|
throw new AgentNotFoundError(agentId);
|
||||||
|
}
|
||||||
|
|
||||||
if (!updatedAgent[0]) {
|
return updatedAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteAgentAction = userAction
|
||||||
|
.schema(
|
||||||
|
z.object({
|
||||||
|
agentId: zString(),
|
||||||
|
organizationId: zString().optional(),
|
||||||
|
organizationIds: z.array(zString()).optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.action(async ({parsedInput}): Promise<ServerActionResult<Agent>> => {
|
||||||
|
try {
|
||||||
|
const deletedAgent = await deleteAgentService(parsedInput);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: deletedAgent,
|
||||||
|
actionSuccess: {
|
||||||
|
message: "Agent has been successfully deleted.",
|
||||||
|
messageParams: {
|
||||||
|
agentId: parsedInput.agentId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AgentNotFoundError) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
actionError: {
|
actionError: {
|
||||||
message: "Agent not found or update failed",
|
message: "Agent not found or update failed",
|
||||||
status: 404,
|
status: 404,
|
||||||
messageParams: {agentId: agentId},
|
messageParams: {
|
||||||
|
agentId: parsedInput.agentId,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: updatedAgent[0],
|
|
||||||
actionSuccess: {
|
|
||||||
message: "Agent has been successfully deleted.",
|
|
||||||
messageParams: {projectId: agentId},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
actionError: {
|
actionError: {
|
||||||
message: "Failed to delete agent.",
|
message: "Failed to delete agent.",
|
||||||
status: 500,
|
status: 500,
|
||||||
cause: error instanceof Error ? error.message : "Unknown error",
|
cause: error instanceof Error ? error.message : "Unknown error",
|
||||||
messageParams: {agentId: agentId},
|
messageParams: {
|
||||||
|
agentId: parsedInput.agentId,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,20 +56,7 @@ export const createAgentAction = userAction.schema(
|
|||||||
data: AgentSchema,
|
data: AgentSchema,
|
||||||
})
|
})
|
||||||
).action(async ({parsedInput}) => {
|
).action(async ({parsedInput}) => {
|
||||||
// const slug = slugify(parsedInput.data.name);
|
|
||||||
// await verifySlugUniqueness(slug);
|
|
||||||
//
|
|
||||||
// const [createdAgent] = await db.insert(drizzleDb.schemas.agent).values({...parsedInput.data, slug: slug, organizationId: parsedInput.organizationId}).returning();
|
|
||||||
//
|
|
||||||
// if (createdAgent && parsedInput.organizationId){
|
|
||||||
// await db.insert(drizzleDb.schemas.organizationAgent).values({
|
|
||||||
// organizationId: parsedInput.organizationId,
|
|
||||||
// agentId: createdAgent.id,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
const createdAgent = await createAgentService(parsedInput);
|
const createdAgent = await createAgentService(parsedInput);
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: createdAgent,
|
data: createdAgent,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {ApiKeyContext} from "@/lib/api-v1/types";
|
|||||||
|
|
||||||
const log = logger.child({ module: "api-v1/middleware" });
|
const log = logger.child({ module: "api-v1/middleware" });
|
||||||
|
|
||||||
|
|
||||||
type ApiKeyHandler = (
|
type ApiKeyHandler = (
|
||||||
req: Request,
|
req: Request,
|
||||||
ctx: ApiKeyContext,
|
ctx: ApiKeyContext,
|
||||||
@@ -32,7 +31,7 @@ export function withApiKey(handler: ApiKeyHandler) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore — verifyApiKey is added by the @better-auth/api-key plugin
|
// @ts-ignore — verifyApiKey is added by the @better-auth/api-key plugin
|
||||||
const result = await auth.api.verifyApiKey({ body: { key } });
|
const result = await auth.api.verifyApiKey({ body: { key, configId: "standard" } });
|
||||||
|
|
||||||
if (!result?.valid || !result?.key) {
|
if (!result?.valid || !result?.key) {
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { db } from "@/db";
|
|||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import { eq, inArray, and, or, isNull } from "drizzle-orm";
|
import { eq, inArray, and, or, isNull } from "drizzle-orm";
|
||||||
import {ApiKeyContextUser} from "@/lib/api-v1/types";
|
import {ApiKeyContextUser} from "@/lib/api-v1/types";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
import {AgentWith} from "@/db/schema/08_agent";
|
||||||
|
|
||||||
export async function getAccessibleAgentIds(
|
export async function getAccessibleAgentIds(
|
||||||
user: ApiKeyContextUser
|
user: ApiKeyContextUser
|
||||||
@@ -71,3 +73,58 @@ export async function getAccessibleAgentIds(
|
|||||||
|
|
||||||
return agents.map((a) => a.id);
|
return agents.map((a) => a.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resolveAgentAccess(id: string, user: ApiKeyContextUser) {
|
||||||
|
const accessibleAgentIds = await getAccessibleAgentIds(user);
|
||||||
|
if (accessibleAgentIds.includes(id)) return "ok";
|
||||||
|
|
||||||
|
const exists = await db.query.agent.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.agent.id, id),
|
||||||
|
or(
|
||||||
|
eq(drizzleDb.schemas.agent.isArchived, false),
|
||||||
|
isNull(drizzleDb.schemas.agent.deletedAt)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
columns: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return exists ? "forbidden" : "not_found";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type GetAgentOptions = {
|
||||||
|
includeDatabases?: boolean;
|
||||||
|
includeOrganizations?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getAgent(
|
||||||
|
id: string,
|
||||||
|
options: GetAgentOptions = {}
|
||||||
|
) {
|
||||||
|
const agent = drizzleDb.schemas.agent;
|
||||||
|
|
||||||
|
const withRelations: {
|
||||||
|
databases?: true;
|
||||||
|
organizations?: true;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
if (options.includeDatabases) {
|
||||||
|
withRelations.databases = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.includeOrganizations) {
|
||||||
|
withRelations.organizations = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.query.agent.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(agent.id, id),
|
||||||
|
eq(agent.isArchived, false),
|
||||||
|
isNull(agent.deletedAt)
|
||||||
|
),
|
||||||
|
...(Object.keys(withRelations).length > 0
|
||||||
|
? { with: withRelations }
|
||||||
|
: {}),
|
||||||
|
}) as unknown as AgentWith;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import {ApiKeyContextUser} from "@/lib/api-v1/types";
|
||||||
|
import {getAccessibleDatabaseIds} from "@/lib/api-v1/services/databases";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
|
||||||
|
export async function resolveDatabaseAccess(id: string, user: ApiKeyContextUser) {
|
||||||
|
const accessibleIds = await getAccessibleDatabaseIds(user);
|
||||||
|
if (accessibleIds.includes(id)) return "ok";
|
||||||
|
const exists = await db.query.database.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.database.id, id),
|
||||||
|
columns: { id: true },
|
||||||
|
});
|
||||||
|
return exists ? "forbidden" : "not_found";
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import { eq, inArray, and, or, isNull } from "drizzle-orm";
|
import { eq, inArray, and, isNull } from "drizzle-orm";
|
||||||
import {getAccessibleAgentIds} from "@/lib/api-v1/services/agents";
|
import {getAccessibleAgentIds} from "@/lib/api-v1/services/agents";
|
||||||
import {ApiKeyContextUser} from "@/lib/api-v1/types";
|
import {ApiKeyContext, ApiKeyContextUser} from "@/lib/api-v1/types";
|
||||||
|
import {NextResponse} from "next/server";
|
||||||
|
|
||||||
|
|
||||||
export async function getAccessibleDatabaseIds(user: ApiKeyContextUser): Promise<string[]> {
|
export async function getAccessibleDatabaseIds(user: ApiKeyContextUser): Promise<string[]> {
|
||||||
@@ -19,3 +20,91 @@ export async function getAccessibleDatabaseIds(user: ApiKeyContextUser): Promise
|
|||||||
|
|
||||||
return databases.map((d) => d.id);
|
return databases.map((d) => d.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAccessibleDatabases(user: ApiKeyContext["user"]) {
|
||||||
|
const agentIds = await getAccessibleAgentIds(user);
|
||||||
|
|
||||||
|
if (agentIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.query.database.findMany({
|
||||||
|
where: and(
|
||||||
|
inArray(drizzleDb.schemas.database.agentId, agentIds),
|
||||||
|
isNull(drizzleDb.schemas.database.deletedAt)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type GuardResult<T> =
|
||||||
|
| {
|
||||||
|
ok: true;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
ok: false;
|
||||||
|
response: NextResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
function jsonError(message: string, status: number) {
|
||||||
|
return NextResponse.json({ error: message }, { status });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requireDatabaseAccess(
|
||||||
|
params: Record<string, string> | undefined,
|
||||||
|
user: ApiKeyContext["user"]
|
||||||
|
): Promise<GuardResult<{ id: string }>> {
|
||||||
|
const id = params?.id;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
response: jsonError("Not found", 404),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const access = await resolveDatabaseAccess(id, user);
|
||||||
|
|
||||||
|
if (access === "ok") {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
data: { id },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (access === "forbidden") {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
response: jsonError("Forbidden", 403),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
response: jsonError("Not found", 404),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DatabaseAccessResult = "ok" | "forbidden" | "not_found";
|
||||||
|
|
||||||
|
export async function resolveDatabaseAccess(
|
||||||
|
id: string,
|
||||||
|
user: ApiKeyContext["user"]
|
||||||
|
): Promise<DatabaseAccessResult> {
|
||||||
|
const accessibleIds = await getAccessibleDatabaseIds(user);
|
||||||
|
|
||||||
|
if (accessibleIds.includes(id)) {
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = await db.query.database.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.database.id, id),
|
||||||
|
isNull(drizzleDb.schemas.database.deletedAt)
|
||||||
|
),
|
||||||
|
columns: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return exists ? "forbidden" : "not_found";
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import {SystemPermissions} from "@/lib/acl/system-acl";
|
import {SystemPermissions} from "@/lib/acl/system-acl";
|
||||||
import {OrganizationPermissions} from "@/lib/acl/organization-acl";
|
import {OrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export type ApiKeyContextUser = {
|
export type ApiKeyContextUser = {
|
||||||
id: string;
|
id: string;
|
||||||
permissions: SystemPermissions;
|
permissions: SystemPermissions;
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
type ParseJsonBodyResult<T> =
|
||||||
|
| {
|
||||||
|
ok: true;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
ok: false;
|
||||||
|
response: NextResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function parseJsonBody<TSchema extends z.ZodTypeAny>(
|
||||||
|
req: Request,
|
||||||
|
schema: TSchema
|
||||||
|
): Promise<ParseJsonBodyResult<z.infer<TSchema>>> {
|
||||||
|
let body: unknown;
|
||||||
|
|
||||||
|
try {
|
||||||
|
body = await req.json();
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
response: NextResponse.json(
|
||||||
|
{ error: "Invalid JSON body" },
|
||||||
|
{ status: 422 }
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = schema.safeParse(body);
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
response: NextResponse.json(
|
||||||
|
{
|
||||||
|
error: parsed.error.issues[0]?.message ?? "Invalid body",
|
||||||
|
},
|
||||||
|
{ status: 422 }
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
data: parsed.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
+32
-3
@@ -131,9 +131,38 @@ export const auth = betterAuth({
|
|||||||
allowDifferentEmails: true,
|
allowDifferentEmails: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
apiKey(),
|
apiKey([
|
||||||
|
{
|
||||||
|
configId: "public",
|
||||||
|
defaultPrefix: "pk_",
|
||||||
|
rateLimit: {
|
||||||
|
enabled: true,
|
||||||
|
maxRequests: 100,
|
||||||
|
timeWindow: 1000 * 60 * 60, // 100 requests / hour
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configId: "standard",
|
||||||
|
defaultPrefix: "sk_",
|
||||||
|
enableMetadata: true,
|
||||||
|
rateLimit: {
|
||||||
|
enabled: true,
|
||||||
|
maxRequests: 1000,
|
||||||
|
timeWindow: 1000 * 60 * 60, // 1000 requests / hour
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configId: "internal",
|
||||||
|
defaultPrefix: "int_",
|
||||||
|
enableMetadata: true,
|
||||||
|
rateLimit: {
|
||||||
|
enabled: true,
|
||||||
|
maxRequests: 10_000,
|
||||||
|
timeWindow: 1000 * 60 * 60, // 10k requests / hour
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
sso({
|
sso({
|
||||||
defaultSSO: oidcProviders.map((p) => ({
|
defaultSSO: oidcProviders.map((p) => ({
|
||||||
oidcConfig: {
|
oidcConfig: {
|
||||||
@@ -673,7 +702,7 @@ export const createApiKey = async (name: string) => {
|
|||||||
return await auth.api.createApiKey({
|
return await auth.api.createApiKey({
|
||||||
body: {
|
body: {
|
||||||
name,
|
name,
|
||||||
prefix: "sk_",
|
configId: "standard",
|
||||||
},
|
},
|
||||||
headers: await headers(),
|
headers: await headers(),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user