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,82 +1,71 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { withApiKey, ApiKeyContext } from "@/lib/api-v1/middleware";
|
||||
import { getAccessibleAgentIds } from "@/lib/api-v1/acl";
|
||||
import { db } from "@/db";
|
||||
import {NextResponse} from "next/server";
|
||||
import {withApiKey} from "@/lib/api-v1/middleware";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import { eq, and, or, isNull } from "drizzle-orm";
|
||||
import { withUpdatedAt } from "@/db/utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { logger } from "@/lib/logger";
|
||||
import {eq, and, or, isNull} from "drizzle-orm";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
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]" });
|
||||
|
||||
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";
|
||||
}
|
||||
const log = logger.child({module: "api/v1/agents/[id]"});
|
||||
|
||||
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 });
|
||||
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.userId);
|
||||
if (access === "forbidden") return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
if (access === "not_found") return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
const access = await resolveAgentAccess(id, ctx.user);
|
||||
|
||||
const agent = 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)
|
||||
)
|
||||
),
|
||||
with: { databases: true },
|
||||
});
|
||||
if (access === "forbidden") return NextResponse.json({error: "Forbidden"}, {status: 403});
|
||||
if (access === "not_found") return NextResponse.json({error: "Not found"}, {status: 404});
|
||||
|
||||
if (!agent) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
return NextResponse.json({ data: agent });
|
||||
} catch (error) {
|
||||
log.error({ error }, "Error in GET /api/v1/agents/[id]");
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
const agent = await getAgent(id, {
|
||||
includeDatabases: true,
|
||||
includeOrganizations: false,
|
||||
});
|
||||
|
||||
if (!agent) return NextResponse.json({error: "Not found"}, {status: 404});
|
||||
return NextResponse.json({data: agent});
|
||||
} catch (error) {
|
||||
log.error({error}, "Error in GET /api/v1/agents/[id]");
|
||||
return NextResponse.json({error: "Internal server error"}, {status: 500});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const DELETE = 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 });
|
||||
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.userId);
|
||||
if (access === "forbidden") return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
if (access === "not_found") 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});
|
||||
|
||||
await db
|
||||
.update(drizzleDb.schemas.agent)
|
||||
.set(withUpdatedAt({ isArchived: true, slug: uuidv4(), deletedAt: new Date() }))
|
||||
.where(eq(drizzleDb.schemas.agent.id, id));
|
||||
const agent = await getAgent(id, {
|
||||
includeDatabases: false,
|
||||
includeOrganizations: true,
|
||||
});
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
} catch (error) {
|
||||
log.error({ error }, "Error in DELETE /api/v1/agents/[id]");
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
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) {
|
||||
log.error({error}, "Error in DELETE /api/v1/agents/[id]");
|
||||
return NextResponse.json({error: "Internal server error"}, {status: 500});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2,49 +2,60 @@ import { NextResponse } from "next/server";
|
||||
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||
import { db } 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 {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(
|
||||
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||
try {
|
||||
const { id, backupId } = params ?? {};
|
||||
if (!id || !backupId) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||
try {
|
||||
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||
|
||||
const accessibleIds = await getAccessibleDatabaseIds(ctx.user);
|
||||
if (!accessibleIds.includes(id)) {
|
||||
const exists = await db.query.database.findFirst({
|
||||
where: eq(drizzleDb.schemas.database.id, id),
|
||||
columns: { id: true },
|
||||
if (!guard.ok) {
|
||||
return guard.response;
|
||||
}
|
||||
|
||||
const { id } = guard.data;
|
||||
const backupId = params?.backupId;
|
||||
|
||||
if (!backupId) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const backup = await db.query.backup.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.id, backupId),
|
||||
eq(drizzleDb.schemas.backup.databaseId, id),
|
||||
isNull(drizzleDb.schemas.backup.deletedAt)
|
||||
),
|
||||
with: {
|
||||
storages: {
|
||||
where: (backupStorage, { isNull }) =>
|
||||
isNull(backupStorage.deletedAt),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!backup) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ data: backup });
|
||||
} catch (error) {
|
||||
log.error(
|
||||
{ error },
|
||||
"Error in GET /api/v1/databases/[id]/backup/[backupId]"
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: exists ? "Forbidden" : "Not found" },
|
||||
{ status: exists ? 403 : 404 }
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const backup = await db.query.backup.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.id, backupId),
|
||||
eq(drizzleDb.schemas.backup.databaseId, id),
|
||||
isNull(drizzleDb.schemas.backup.deletedAt)
|
||||
),
|
||||
with: {
|
||||
storages: {
|
||||
where: (bs, { isNull }) => isNull(bs.deletedAt),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!backup) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
return NextResponse.json({ data: backup });
|
||||
} catch (error) {
|
||||
log.error({ error }, "Error in GET /api/v1/databases/[id]/backup/[backupId]");
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
);
|
||||
);
|
||||
@@ -2,73 +2,95 @@ import { NextResponse } from "next/server";
|
||||
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||
import { db } 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 {getAccessibleDatabaseIds} from "@/lib/api-v1/services/databases";
|
||||
import {ApiKeyContext, ApiKeyContextUser} 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" });
|
||||
|
||||
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(
|
||||
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||
try {
|
||||
const id = params?.id;
|
||||
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||
try {
|
||||
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);
|
||||
if (access === "forbidden") return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
if (access === "not_found") return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
const { id } = guard.data;
|
||||
|
||||
const backups = await db.query.backup.findMany({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, id),
|
||||
isNull(drizzleDb.schemas.backup.deletedAt)
|
||||
),
|
||||
orderBy: [desc(drizzleDb.schemas.backup.createdAt)],
|
||||
});
|
||||
const backups = await db.query.backup.findMany({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, id),
|
||||
isNull(drizzleDb.schemas.backup.deletedAt)
|
||||
),
|
||||
orderBy: [desc(drizzleDb.schemas.backup.createdAt)],
|
||||
});
|
||||
|
||||
return NextResponse.json({ data: backups });
|
||||
} catch (error) {
|
||||
log.error({ error }, "Error in GET /api/v1/databases/[id]/backup");
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
return NextResponse.json({ data: backups });
|
||||
} catch (error) {
|
||||
log.error({ error }, "Error in GET /api/v1/databases/[id]/backup");
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const POST = 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 });
|
||||
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||
try {
|
||||
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||
|
||||
const access = await resolveDatabaseAccess(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 });
|
||||
if (!guard.ok) {
|
||||
return guard.response;
|
||||
}
|
||||
|
||||
const [createdBackup] = await db
|
||||
.insert(drizzleDb.schemas.backup)
|
||||
.values({ databaseId: id, status: "waiting" })
|
||||
.returning();
|
||||
const { id } = guard.data;
|
||||
|
||||
if (!createdBackup) {
|
||||
return NextResponse.json({ error: "Failed to create backup" }, { status: 500 });
|
||||
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
|
||||
.insert(drizzleDb.schemas.backup)
|
||||
.values({
|
||||
databaseId: id,
|
||||
status: "waiting",
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!createdBackup) {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to create backup" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ data: createdBackup }, { status: 201 });
|
||||
} catch (error) {
|
||||
log.error({ error }, "Error in POST /api/v1/databases/[id]/backup");
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ data: createdBackup }, { status: 201 });
|
||||
} catch (error) {
|
||||
log.error({ error }, "Error in POST /api/v1/databases/[id]/backup");
|
||||
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 { db } 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 { logger } from "@/lib/logger";
|
||||
import {ApiKeyContext} from "@/lib/api-v1/types";
|
||||
import {getAccessibleDatabaseIds} from "@/lib/api-v1/services/databases";
|
||||
import { ApiKeyContext } from "@/lib/api-v1/types";
|
||||
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" });
|
||||
|
||||
@@ -16,91 +17,106 @@ const RestoreSchema = z.object({
|
||||
});
|
||||
|
||||
export const POST = 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 accessibleIds = await getAccessibleDatabaseIds(ctx.user);
|
||||
if (!accessibleIds.includes(id)) {
|
||||
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;
|
||||
async (req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid JSON body" }, { status: 422 });
|
||||
}
|
||||
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||
|
||||
if (!guard.ok) {
|
||||
return guard.response;
|
||||
}
|
||||
|
||||
const { id } = guard.data;
|
||||
|
||||
const body = await parseJsonBody(req, RestoreSchema);
|
||||
|
||||
if (!body.ok) {
|
||||
return body.response;
|
||||
}
|
||||
|
||||
const { backupId, backupStorageId } = body.data;
|
||||
|
||||
const backupRecord = await db.query.backup.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.id, backupId),
|
||||
eq(drizzleDb.schemas.backup.databaseId, id),
|
||||
isNull(drizzleDb.schemas.backup.deletedAt)
|
||||
),
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!backupRecord) {
|
||||
return NextResponse.json(
|
||||
{ error: "Backup not found for this database" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const backupStorage = await db.query.backupStorage.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backupStorage.id, backupStorageId),
|
||||
eq(drizzleDb.schemas.backupStorage.backupId, backupId),
|
||||
isNull(drizzleDb.schemas.backupStorage.deletedAt)
|
||||
),
|
||||
});
|
||||
|
||||
if (!backupStorage) {
|
||||
return NextResponse.json(
|
||||
{ error: "Backup storage not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (backupStorage.status !== "success") {
|
||||
return NextResponse.json(
|
||||
{ error: "Backup storage is not in a successful state" },
|
||||
{ status: 422 }
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
.insert(drizzleDb.schemas.restoration)
|
||||
.values({
|
||||
databaseId: id,
|
||||
backupId,
|
||||
backupStorageId,
|
||||
status: "waiting",
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!restoration) {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to create restoration" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ data: restoration }, { status: 201 });
|
||||
} catch (error) {
|
||||
log.error({ error }, "Error in POST /api/v1/databases/[id]/restore");
|
||||
|
||||
const parsed = RestoreSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{ error: parsed.error.issues[0].message },
|
||||
{ status: 422 }
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const { backupId, backupStorageId } = parsed.data;
|
||||
|
||||
// Validate backup belongs to this database
|
||||
const backupRecord = await db.query.backup.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.id, backupId),
|
||||
eq(drizzleDb.schemas.backup.databaseId, id),
|
||||
isNull(drizzleDb.schemas.backup.deletedAt)
|
||||
),
|
||||
columns: { id: true },
|
||||
});
|
||||
|
||||
if (!backupRecord) {
|
||||
return NextResponse.json({ error: "Backup not found for this database" }, { status: 404 });
|
||||
}
|
||||
|
||||
const backupStorage = await db.query.backupStorage.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backupStorage.id, backupStorageId),
|
||||
eq(drizzleDb.schemas.backupStorage.backupId, backupId),
|
||||
isNull(drizzleDb.schemas.backupStorage.deletedAt)
|
||||
),
|
||||
});
|
||||
|
||||
if (!backupStorage) {
|
||||
return NextResponse.json({ error: "Backup storage not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (backupStorage.status !== "success") {
|
||||
return NextResponse.json(
|
||||
{ error: "Backup storage is not in a successful state" },
|
||||
{ status: 422 }
|
||||
);
|
||||
}
|
||||
|
||||
const [restoration] = await db
|
||||
.insert(drizzleDb.schemas.restoration)
|
||||
.values({
|
||||
databaseId: id,
|
||||
backupId,
|
||||
backupStorageId,
|
||||
status: "waiting",
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!restoration) {
|
||||
return NextResponse.json({ error: "Failed to create restoration" }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ data: restoration }, { status: 201 });
|
||||
} catch (error) {
|
||||
log.error({ error }, "Error in POST /api/v1/databases/[id]/restore");
|
||||
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 { db } 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 {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]" });
|
||||
|
||||
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 });
|
||||
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||
try {
|
||||
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||
|
||||
const accessibleIds = await getAccessibleDatabaseIds(ctx.user);
|
||||
if (!accessibleIds.includes(id)) {
|
||||
const exists = await db.query.database.findFirst({
|
||||
where: eq(drizzleDb.schemas.database.id, id),
|
||||
columns: { id: true },
|
||||
if (!guard.ok) {
|
||||
return guard.response;
|
||||
}
|
||||
|
||||
const { id } = guard.data;
|
||||
|
||||
const database = await db.query.database.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.database.id, id),
|
||||
isNull(drizzleDb.schemas.database.deletedAt)
|
||||
),
|
||||
});
|
||||
|
||||
if (!database) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ data: database });
|
||||
} catch (error) {
|
||||
log.error({ error }, "Error in GET /api/v1/databases/[id]");
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: exists ? "Forbidden" : "Not found" },
|
||||
{ status: exists ? 403 : 404 }
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const database = await db.query.database.findFirst({
|
||||
where: eq(drizzleDb.schemas.database.id, id),
|
||||
});
|
||||
|
||||
if (!database) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
return NextResponse.json({ data: database });
|
||||
} catch (error) {
|
||||
log.error({ error }, "Error in GET /api/v1/databases/[id]");
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
);
|
||||
);
|
||||
@@ -2,69 +2,71 @@ import { NextResponse } from "next/server";
|
||||
import { withApiKey } from "@/lib/api-v1/middleware";
|
||||
import { db } 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 {ApiKeyContext} from "@/lib/api-v1/types";
|
||||
import {getAccessibleDatabaseIds} from "@/lib/api-v1/services/databases";
|
||||
import { ApiKeyContext } from "@/lib/api-v1/types";
|
||||
import {requireDatabaseAccess} from "@/lib/api-v1/services/databases";
|
||||
|
||||
const log = logger.child({ module: "api/v1/databases/[id]/status" });
|
||||
|
||||
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 });
|
||||
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
|
||||
try {
|
||||
const guard = await requireDatabaseAccess(params, ctx.user);
|
||||
|
||||
const accessibleIds = await getAccessibleDatabaseIds(ctx.user);
|
||||
if (!accessibleIds.includes(id)) {
|
||||
const exists = await db.query.database.findFirst({
|
||||
where: eq(drizzleDb.schemas.database.id, id),
|
||||
columns: { id: true },
|
||||
if (!guard.ok) {
|
||||
return guard.response;
|
||||
}
|
||||
|
||||
const { id } = guard.data;
|
||||
|
||||
const [database, latestBackup, latestRestoration] = await Promise.all([
|
||||
db.query.database.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.database.id, id),
|
||||
isNull(drizzleDb.schemas.database.deletedAt)
|
||||
),
|
||||
}),
|
||||
|
||||
db.query.backup.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, id),
|
||||
isNull(drizzleDb.schemas.backup.deletedAt)
|
||||
),
|
||||
orderBy: [desc(drizzleDb.schemas.backup.createdAt)],
|
||||
}),
|
||||
|
||||
db.query.restoration.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.restoration.databaseId, id),
|
||||
isNull(drizzleDb.schemas.restoration.deletedAt)
|
||||
),
|
||||
orderBy: [desc(drizzleDb.schemas.restoration.createdAt)],
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!database) {
|
||||
return NextResponse.json(
|
||||
{ error: "Database not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
data: {
|
||||
isWaitingForBackup: database.isWaitingForBackup,
|
||||
lastContact: database.lastContact,
|
||||
latestBackup: latestBackup ?? null,
|
||||
latestRestoration: latestRestoration ?? null,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
log.error({ error }, "Error in GET /api/v1/databases/[id]/status");
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: exists ? "Forbidden" : "Not found" },
|
||||
{ status: exists ? 403 : 404 }
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const [database, latestBackup, latestRestoration] = await Promise.all([
|
||||
db.query.database.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.database.id, id),
|
||||
isNull(drizzleDb.schemas.database.deletedAt)
|
||||
)
|
||||
}),
|
||||
db.query.backup.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, id),
|
||||
isNull(drizzleDb.schemas.backup.deletedAt)
|
||||
),
|
||||
orderBy: [desc(drizzleDb.schemas.backup.createdAt)],
|
||||
}),
|
||||
db.query.restoration.findFirst({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.restoration.databaseId, id),
|
||||
isNull(drizzleDb.schemas.restoration.deletedAt)
|
||||
),
|
||||
orderBy: [desc(drizzleDb.schemas.restoration.createdAt)],
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!database){
|
||||
return NextResponse.json({ error: "Database not found" });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
data: {
|
||||
isWaitingForBackup: database.isWaitingForBackup,
|
||||
lastContact: database.lastContact,
|
||||
latestBackup: latestBackup ?? null,
|
||||
latestRestoration: latestRestoration ?? null,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
log.error({ error }, "Error in GET /api/v1/databases/[id]/status");
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
);
|
||||
);
|
||||
@@ -1,32 +1,24 @@
|
||||
import { NextResponse } from "next/server";
|
||||
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 {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" });
|
||||
|
||||
export const GET = withApiKey(async (_req: Request, ctx: ApiKeyContext) => {
|
||||
try {
|
||||
const agentIds = await getAccessibleAgentIds(ctx.user);
|
||||
const databases = await getAccessibleDatabases(ctx.user);
|
||||
|
||||
if (agentIds.length === 0) {
|
||||
return NextResponse.json({ data: [] });
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
return NextResponse.json({ data: databases });
|
||||
} catch (error) {
|
||||
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
|
||||
/^\/api\/v1\/agents\/?$/,
|
||||
/^\/api\/v1\/agents\/[^/]+\/?$/,
|
||||
/^\/api\/v1\/agents\/[^/]+\/key\/?$/,
|
||||
/^\/api\/v1\/databases\/?$/,
|
||||
/^\/api\/v1\/databases\/[^/]+\/?$/,
|
||||
/^\/api\/v1\/databases\/[^/]+\/backup\/?$/,
|
||||
|
||||
@@ -10,128 +10,318 @@ import {Agent} from "@/db/schema/08_agent";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {zString} from "@/lib/zod";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {AgentSchema} from "@/features/agents/agents.schema";
|
||||
import {slugify} from "@/utils/slugify";
|
||||
|
||||
|
||||
//
|
||||
// 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[] = [];
|
||||
|
||||
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?.length) {
|
||||
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: (database, {inArray}) => inArray(database.projectId, projectIds),
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
const databaseIds = databases.map((database) => database.id);
|
||||
|
||||
await db
|
||||
.update(drizzleDb.schemas.database)
|
||||
.set(
|
||||
withUpdatedAt({
|
||||
backupPolicy: null,
|
||||
projectId: null,
|
||||
})
|
||||
)
|
||||
.where(inArray(drizzleDb.schemas.database.projectId, projectIds))
|
||||
.execute();
|
||||
|
||||
if (databaseIds.length > 0) {
|
||||
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) {
|
||||
throw new AgentNotFoundError(agentId);
|
||||
}
|
||||
|
||||
return updatedAgent;
|
||||
}
|
||||
|
||||
export const deleteAgentAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
agentId: zString(),
|
||||
organizationId: zString().optional(),
|
||||
organizationIds: z.array(z.string()).optional()
|
||||
organizationIds: z.array(zString()).optional(),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<Agent>> => {
|
||||
const {agentId, organizationId, organizationIds} = parsedInput;
|
||||
|
||||
try {
|
||||
let projectIds: string[] = [];
|
||||
const deletedAgent = await deleteAgentService(parsedInput);
|
||||
|
||||
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: true,
|
||||
value: deletedAgent,
|
||||
actionSuccess: {
|
||||
message: "Agent has been successfully deleted.",
|
||||
messageParams: {
|
||||
agentId: parsedInput.agentId,
|
||||
},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AgentNotFoundError) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Agent not found or update failed",
|
||||
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 {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to delete agent.",
|
||||
status: 500,
|
||||
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,
|
||||
})
|
||||
).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);
|
||||
|
||||
|
||||
return {
|
||||
data: createdAgent,
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ import {ApiKeyContext} from "@/lib/api-v1/types";
|
||||
|
||||
const log = logger.child({ module: "api-v1/middleware" });
|
||||
|
||||
|
||||
type ApiKeyHandler = (
|
||||
req: Request,
|
||||
ctx: ApiKeyContext,
|
||||
@@ -32,7 +31,7 @@ export function withApiKey(handler: ApiKeyHandler) {
|
||||
}
|
||||
|
||||
// @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) {
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import { db } from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import { eq, inArray, and, or, isNull } from "drizzle-orm";
|
||||
import {ApiKeyContextUser} from "@/lib/api-v1/types";
|
||||
import {notFound} from "next/navigation";
|
||||
import {AgentWith} from "@/db/schema/08_agent";
|
||||
|
||||
export async function getAccessibleAgentIds(
|
||||
user: ApiKeyContextUser
|
||||
@@ -70,4 +72,59 @@ export async function getAccessibleAgentIds(
|
||||
});
|
||||
|
||||
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 * 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 {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[]> {
|
||||
@@ -19,3 +20,91 @@ export async function getAccessibleDatabaseIds(user: ApiKeyContextUser): Promise
|
||||
|
||||
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 {OrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||
|
||||
|
||||
|
||||
export type ApiKeyContextUser = {
|
||||
id: string;
|
||||
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,
|
||||
},
|
||||
},
|
||||
|
||||
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({
|
||||
defaultSSO: oidcProviders.map((p) => ({
|
||||
oidcConfig: {
|
||||
@@ -673,7 +702,7 @@ export const createApiKey = async (name: string) => {
|
||||
return await auth.api.createApiKey({
|
||||
body: {
|
||||
name,
|
||||
prefix: "sk_",
|
||||
configId: "standard",
|
||||
},
|
||||
headers: await headers(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user