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

248 lines
6.5 KiB
TypeScript

import { NextApiRequest, NextApiResponse } from "next";
import { DocumentStorageType } from "@prisma/client";
import { getServerSession } from "next-auth";
import { deleteDomainRedirectUrl } from "@/lib/api/domains/kv";
import { removeDomainFromVercelProject } from "@/lib/domains";
import { errorhandler } from "@/lib/errorHandler";
import { deleteFiles } from "@/lib/files/delete-team-files-server";
import prisma from "@/lib/prisma";
import { kv } from "@/lib/kv";
import { CustomUser } from "@/lib/types";
import { unsubscribe } from "@/lib/resend";
import { authOptions } from "../../auth/[...nextauth]";
export default async function handle(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method === "GET") {
// GET /api/teams/:teamId
const session = await getServerSession(req, res, authOptions);
if (!session) {
return res.status(401).end("Unauthorized");
}
const { teamId } = req.query as { teamId: string };
try {
const team = await prisma.team.findUnique({
where: {
id: teamId,
},
select: {
id: true,
name: true,
users: {
select: {
role: true,
teamId: true,
userId: true,
status: true,
user: {
select: {
email: true,
name: true,
},
},
},
},
documents: {
select: {
owner: {
select: {
name: true,
id: true,
},
},
},
},
},
});
// check that the user is member of the team, otherwise return 403
const teamUsers = team?.users;
const isUserPartOfTeam = teamUsers?.some(
(user) => user.userId === (session.user as CustomUser).id,
);
if (!isUserPartOfTeam) {
return res.status(403).end("Unauthorized to access this team");
}
return res.status(200).json(team);
} catch (error) {
errorhandler(error, res);
}
} else if (req.method === "DELETE") {
// DELETE /api/teams/:teamId
const session = await getServerSession(req, res, authOptions);
if (!session) {
res.status(401).end("Unauthorized");
return;
}
const { teamId } = req.query as { teamId: string };
try {
// check if the team exists
const team = await prisma.team.findUnique({
where: {
id: teamId,
},
include: {
users: true,
domains: true,
},
});
if (!team) {
return res.status(400).json("Team doesn't exists");
}
// check if current user is admin of the team
const isUserAdmin = team.users.some(
(user) =>
user.role === "ADMIN" &&
user.userId === (session.user as CustomUser).id,
);
if (!isUserAdmin) {
return res
.status(403)
.json({ message: "You are not permitted to perform this action" });
}
// get all documents using Vercel Blob storage
const documentsUsingBlob = await prisma.document.findMany({
where: {
teamId: teamId,
storageType: DocumentStorageType.VERCEL_BLOB,
},
select: {
file: true,
versions: {
select: {
file: true,
pages: {
select: {
file: true,
},
},
},
},
},
});
// get all branding files
const brandingFiles = await prisma.brand.findMany({
where: {
teamId,
},
select: {
logo: true,
},
});
// get all dataroom branding files
const dataroomBrandingFiles = await prisma.dataroomBrand.findMany({
where: {
dataroom: {
teamId: teamId,
},
},
select: {
logo: true,
banner: true,
},
});
let files: string[] = [];
let hasBlobDocuments = false;
if (documentsUsingBlob) {
hasBlobDocuments = true;
files = documentsUsingBlob.flatMap((doc) => [
doc.file,
...doc.versions.flatMap((version) => [
version.file,
...version.pages.map((page) => page.file),
]),
]);
}
if (brandingFiles) {
files = [
...files,
...brandingFiles
.map((brand) => brand.logo)
.filter((logo): logo is string => logo !== null),
];
}
if (dataroomBrandingFiles) {
files = [
...files,
...dataroomBrandingFiles
.flatMap((brand) => [brand.logo, brand.banner])
.filter((item): item is string => item !== null),
];
}
// delete all files from storage
await deleteFiles({ teamId, data: hasBlobDocuments ? files : undefined });
// if user doesn't have other teams, delete the user
const userTeams = await prisma.team.findMany({
where: {
users: {
some: {
userId: (session.user as CustomUser).id,
},
},
},
});
// prepare a list of promises to delete domains and their KV redirect entries
let domainPromises: Promise<unknown>[] = [];
if (team.domains) {
domainPromises = team.domains.flatMap((domain) => [
removeDomainFromVercelProject(domain.slug),
deleteDomainRedirectUrl(domain.slug),
]);
}
await Promise.all([
// delete domains, if exists on team
team.domains && domainPromises,
// delete user from contact book
unsubscribe((session.user as CustomUser).email ?? ""),
// delete user, if no other teams
userTeams.length === 1 &&
prisma.user.delete({
where: {
id: (session.user as CustomUser).id,
},
}),
// delete team branding from kv
kv.del(`brand:logo:${teamId}`),
// delete team
prisma.team.delete({
where: {
id: teamId,
},
}),
]);
return res.status(204).end();
} catch (error) {
return res.status(500).json((error as Error).message);
}
} else {
// We only allow GET and DELETE requests
res.setHeader("Allow", ["GET", "DELETE"]);
return res.status(405).end(`Method ${req.method} Not Allowed`);
}
}