Files
zeekayandHanzo Dev 6b68c9299e kv: one name for the store, and it is Hanzo KV
The client was already @hanzo/kv; only the vocabulary was still Redis. Every
module, symbol and comment now says what it connects to:

  lib/redis.ts                    -> lib/kv.ts                (exports kv, lockKv)
  lib/redis-job-store.ts          -> lib/kv-job-store.ts      (KvJobStore)
  lib/redis-download-job-store.ts -> lib/kv-download-job-store.ts
  lib/files/tus-redis-locker.ts   -> lib/files/tus-kv-locker.ts (KvLocker, KvLock)
  lib/api/domains/redis.ts        -> lib/api/domains/kv.ts    (getKey)
  lib/redis/                      -> lib/kv-queue/

KV_URL is now the only endpoint variable — the REDIS_URL fallback and the
redis://localhost default are gone, and the default scheme is kv://, which
@hanzo/kv parses natively (utils/index.js). .env.example gained the KV_URL it
never documented, and lost UPSTASH_REDIS_REST_LOCKER_URL/TOKEN: nothing has read
those since the client changed — the locker opens a second connection to the
same KV, so there is no second endpoint to configure.

The second connection is `lockKv`, not `locker`: both tus routes already have a
local `const locker = new KvLocker(...)`, so naming the client `locker` made the
lock take itself as its own client.

Verified against the repo's own gate (npx tsc --noEmit): 100 errors before and
after, 0 introduced, compared set-wise by file:line:code. Behaviour is untouched
— this is naming, one connection string, and dead config.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-09 10:09:34 -07:00

270 lines
7.4 KiB
TypeScript

import { NextApiRequest, NextApiResponse } from "next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth/next";
import { TeamError, errorhandler } from "@/lib/errorHandler";
import { deleteFile } from "@/lib/files/delete-file-server";
import prisma from "@/lib/prisma";
import { ratelimit } from "@/lib/kv";
import { CustomUser } from "@/lib/types";
import { serializeFileSize } from "@/lib/utils";
export default async function handle(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method === "GET") {
// GET /api/teams/:teamId/documents/:id
const session = await getServerSession(req, res, authOptions);
if (!session) {
return res.status(401).json({ message: "Unauthorized" });
}
const { teamId, id: docId } = req.query as { teamId: string; id: string };
const userId = (session.user as CustomUser).id;
try {
// Per-user, per-document rate limit to prevent abuse
// Default: 120 requests per minute per user per document
const { success, limit, remaining, reset } = await ratelimit(
120,
"1 m",
).limit(`doc:${docId}:team:${teamId}:user:${userId}`);
res.setHeader("X-RateLimit-Limit", limit.toString());
res.setHeader("X-RateLimit-Remaining", remaining.toString());
res.setHeader("X-RateLimit-Reset", reset.toString());
if (!success) {
return res.status(429).json({ error: "Too many requests" });
}
// First verify user has access to the team (lightweight query)
const teamAccess = await prisma.userTeam.findUnique({
where: {
userId_teamId: {
userId: userId,
teamId: teamId,
},
},
select: { teamId: true },
});
if (!teamAccess) {
return res.status(401).json({ message: "Unauthorized" });
}
// Then fetch the specific document with its relationships (targeted query)
const document = await prisma.document.findUnique({
where: {
id: docId,
teamId,
},
include: {
// Get the latest primary version of the document
versions: {
where: { isPrimary: true },
orderBy: { createdAt: "desc" },
take: 1,
},
folder: {
select: {
name: true,
path: true,
},
},
datarooms: {
select: {
dataroom: {
select: {
id: true,
name: true,
},
},
folder: {
select: {
id: true,
name: true,
path: true,
},
},
},
},
},
});
if (!document || !document.versions || document.versions.length === 0) {
return res.status(404).json({
error: "Not Found",
message: "The requested document does not exist",
});
}
const pages = await prisma.documentPage.findMany({
where: {
versionId: document.versions[0].id,
},
select: {
pageLinks: true,
},
});
const hasPageLinks = pages.some(
(page) =>
page.pageLinks &&
Array.isArray(page.pageLinks) &&
(page.pageLinks as any[]).length > 0,
);
// Check that the user is owner of the document, otherwise return 401
// if (document.ownerId !== (session.user as CustomUser).id) {
// return res.status(401).end("Unauthorized to access this document");
// }
return res
.status(200)
.json(serializeFileSize({ ...document, hasPageLinks }));
} catch (error) {
if (error instanceof TeamError) {
return res.status(404).json({
error: "Not Found",
message: error.message,
});
}
errorhandler(error, res);
}
} else if (req.method === "PUT") {
// PUT /api/teams/:teamId/document/:id
const session = await getServerSession(req, res, authOptions);
if (!session) {
return res.status(401).json({ message: "Unauthorized" });
}
const userId = (session.user as CustomUser).id;
const { teamId, id: docId } = req.query as { teamId: string; id: string };
const { folderId, currentPathName } = req.body as {
folderId: string;
currentPathName: string;
};
const document = await prisma.document.update({
where: {
id: docId,
teamId: teamId,
team: {
users: {
some: {
role: "ADMIN",
userId: userId,
},
},
},
},
data: {
folderId: folderId,
},
select: {
folder: {
select: {
path: true,
},
},
},
});
if (!document) {
return res.status(404).json({ message: "Document not found" });
}
return res.status(200).json({
message: "Document moved successfully",
newPath: document.folder?.path,
oldPath: currentPathName,
});
} else if (req.method === "DELETE") {
// DELETE /api/teams/:teamId/document/:id
const session = await getServerSession(req, res, authOptions);
if (!session) {
return res.status(401).json({ message: "Unauthorized" });
}
const { teamId, id: docId } = req.query as { teamId: string; id: string };
const userId = (session.user as CustomUser).id;
try {
const teamAccess = await prisma.userTeam.findUnique({
where: {
userId_teamId: {
userId: userId,
teamId: teamId,
},
},
select: {
role: true,
},
});
if (!teamAccess) {
return res.status(401).json({ message: "Unauthorized" });
}
if (teamAccess.role !== "ADMIN" && teamAccess.role !== "MANAGER") {
return res.status(403).json({
message:
"You are not permitted to perform this action. Only admin and managers can delete documents.",
});
}
const documentVersions = await prisma.document.findUnique({
where: {
id: docId,
teamId: teamId,
},
include: {
versions: {
select: {
id: true,
file: true,
type: true,
storageType: true,
},
},
},
});
if (!documentVersions) {
return res.status(404).json({ message: "Document not found" });
}
//if it is not notion document then only delete the document from storage
if (documentVersions.type !== "notion") {
// delete the files from storage
for (const version of documentVersions.versions) {
await deleteFile({
type: version.storageType,
data: version.file,
teamId,
});
}
}
// delete the document from database
await prisma.document.delete({
where: {
id: docId,
},
});
return res.status(204).end(); // 204 No Content response for successful deletes
} catch (error) {
errorhandler(error, res);
}
} else {
// We only allow GET, PUT, PATCH and DELETE requests
res.setHeader("Allow", ["GET", "PUT", "DELETE"]);
return res
.status(405)
.json({ message: `Method ${req.method} Not Allowed` });
}
}