mirror of
https://github.com/khairul169/code-share.git
synced 2025-04-29 00:59:37 +07:00
292 lines
7.3 KiB
TypeScript
292 lines
7.3 KiB
TypeScript
import { and, desc, eq, isNull, sql } from "drizzle-orm";
|
|
import db from "../db";
|
|
import { procedure, router } from "../api/trpc";
|
|
import {
|
|
project,
|
|
insertProjectSchema,
|
|
selectProjectSchema,
|
|
ProjectSchema,
|
|
} from "../db/schema/project";
|
|
import { z } from "zod";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { uid } from "../lib/utils";
|
|
import { insertUserSchema, user } from "../db/schema/user";
|
|
import { faker } from "@faker-js/faker";
|
|
import { ucwords } from "~/lib/utils";
|
|
import { hashPassword } from "../lib/crypto";
|
|
import { createToken } from "../lib/jwt";
|
|
import { file } from "../db/schema/file";
|
|
import { Context } from "../api/trpc/context";
|
|
|
|
const projectRouter = router({
|
|
getAll: procedure
|
|
.input(z.object({ owned: z.boolean() }).partial().optional())
|
|
.query(async ({ ctx, input: opt }) => {
|
|
if (opt?.owned && !ctx.user) {
|
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
}
|
|
|
|
const where = [
|
|
!opt?.owned
|
|
? eq(project.visibility, "public")
|
|
: eq(project.userId, ctx.user!.id),
|
|
isNull(project.deletedAt),
|
|
];
|
|
|
|
const projects = await db.query.project.findMany({
|
|
where: and(...(where.filter((i) => i != null) as any)),
|
|
columns: { settings: false },
|
|
with: {
|
|
user: { columns: { password: false } },
|
|
},
|
|
orderBy: [desc(project.id)],
|
|
});
|
|
|
|
return projects;
|
|
}),
|
|
|
|
getById: procedure
|
|
.input(z.number().or(z.string()))
|
|
.query(async ({ ctx, input }) => {
|
|
const where = and(
|
|
typeof input === "string"
|
|
? eq(project.slug, input)
|
|
: eq(project.id, input),
|
|
isNull(project.deletedAt)
|
|
);
|
|
|
|
const projectData = await db.query.project.findFirst({
|
|
where,
|
|
with: {
|
|
user: { columns: { password: false } },
|
|
},
|
|
});
|
|
|
|
if (!projectData || !hasPermission(ctx, projectData, "r")) {
|
|
return null;
|
|
}
|
|
|
|
const isMutable = hasPermission(ctx, projectData, "w");
|
|
|
|
return { ...projectData!, isMutable };
|
|
}),
|
|
|
|
create: procedure
|
|
.input(
|
|
insertProjectSchema
|
|
.pick({
|
|
title: true,
|
|
})
|
|
.merge(
|
|
z.object({
|
|
forkFromId: z.number().optional(),
|
|
user: insertUserSchema
|
|
.pick({
|
|
name: true,
|
|
email: true,
|
|
password: true,
|
|
})
|
|
.optional(),
|
|
})
|
|
)
|
|
)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const title =
|
|
input.title.length > 0 ? input.title : ucwords(faker.lorem.words(2));
|
|
let userId = ctx.user?.id;
|
|
|
|
return db.transaction(async (tx) => {
|
|
if (input.user && !userId) {
|
|
const [usr] = await tx
|
|
.insert(user)
|
|
.values({
|
|
...input.user,
|
|
password: await hashPassword(input.user.password),
|
|
})
|
|
.returning();
|
|
|
|
userId = usr.id;
|
|
|
|
// set user token
|
|
const token = await createToken({ id: userId });
|
|
ctx.res.cookie("auth-token", token, { httpOnly: true });
|
|
}
|
|
|
|
if (!userId) {
|
|
throw new Error("Invalid userId!");
|
|
}
|
|
|
|
const data: z.infer<typeof insertProjectSchema> = {
|
|
userId,
|
|
title,
|
|
slug: uid(),
|
|
};
|
|
|
|
const [projectData] = await tx.insert(project).values(data).returning();
|
|
const projectId = projectData.id;
|
|
|
|
if (input.forkFromId) {
|
|
const forkProject = await tx.query.project.findFirst({
|
|
where: and(
|
|
eq(project.id, input.forkFromId),
|
|
isNull(project.deletedAt)
|
|
),
|
|
});
|
|
|
|
if (!forkProject) {
|
|
throw new Error("Fork Project not found!");
|
|
}
|
|
|
|
if (!hasPermission(ctx, forkProject, "r")) {
|
|
throw new TRPCError({ code: "FORBIDDEN" });
|
|
}
|
|
|
|
const forkFiles = await tx.query.file.findMany({
|
|
where: and(
|
|
eq(file.projectId, input.forkFromId),
|
|
isNull(file.deletedAt)
|
|
),
|
|
columns: {
|
|
id: false,
|
|
projectId: false,
|
|
createdAt: false,
|
|
deletedAt: false,
|
|
},
|
|
});
|
|
|
|
await tx
|
|
.insert(file)
|
|
.values(forkFiles.map((file) => ({ ...file, projectId })));
|
|
|
|
await tx
|
|
.update(project)
|
|
.set({ settings: forkProject.settings, forkId: forkProject.id })
|
|
.where(eq(project.id, projectData.id));
|
|
} else {
|
|
await tx.insert(file).values([
|
|
{
|
|
projectId,
|
|
path: "index.html",
|
|
filename: "index.html",
|
|
content: "<p>Open index.html to edit this file.</p>",
|
|
isPinned: true,
|
|
},
|
|
]);
|
|
}
|
|
|
|
return projectData;
|
|
});
|
|
}),
|
|
|
|
update: procedure
|
|
.input(
|
|
selectProjectSchema
|
|
.partial()
|
|
.omit({ slug: true, userId: true })
|
|
.required({ id: true })
|
|
)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const data = { ...input };
|
|
|
|
const projectData = await db.query.project.findFirst({
|
|
where: and(eq(project.id, input.id), isNull(project.deletedAt)),
|
|
});
|
|
if (!projectData) {
|
|
throw new TRPCError({ code: "NOT_FOUND" });
|
|
}
|
|
|
|
if (!hasPermission(ctx, projectData, "w")) {
|
|
throw new TRPCError({ code: "FORBIDDEN" });
|
|
}
|
|
|
|
if (data.settings) {
|
|
data.settings = Object.assign(
|
|
projectData.settings || {},
|
|
data.settings
|
|
);
|
|
}
|
|
|
|
const [result] = await db
|
|
.update(project)
|
|
.set(data)
|
|
.where(and(eq(project.id, input.id), isNull(project.deletedAt)))
|
|
.returning();
|
|
|
|
return result;
|
|
}),
|
|
|
|
delete: procedure.input(z.number()).mutation(async ({ ctx, input }) => {
|
|
const projectData = await db.query.project.findFirst({
|
|
where: and(eq(project.id, input), isNull(project.deletedAt)),
|
|
});
|
|
if (!projectData) {
|
|
throw new TRPCError({ code: "NOT_FOUND" });
|
|
}
|
|
|
|
if (!hasPermission(ctx, projectData, "r")) {
|
|
throw new TRPCError({ code: "FORBIDDEN" });
|
|
}
|
|
|
|
const [result] = await db
|
|
.update(project)
|
|
.set({ deletedAt: sql`CURRENT_TIMESTAMP` })
|
|
.where(eq(project.id, projectData.id))
|
|
.returning();
|
|
|
|
return result;
|
|
}),
|
|
|
|
getTemplates: procedure.query(() => {
|
|
return [
|
|
{
|
|
title: "Empty Project",
|
|
projectId: 0,
|
|
},
|
|
{
|
|
title: "Vanilla HTML+CSS+JS",
|
|
projectId: 1,
|
|
},
|
|
{
|
|
title: "React + Tailwindcss",
|
|
projectId: 2,
|
|
},
|
|
];
|
|
}),
|
|
});
|
|
|
|
export function hasPermission(
|
|
ctx: Context,
|
|
project: Pick<ProjectSchema, "userId" | "visibility"> | undefined,
|
|
permission: "r" | "w"
|
|
) {
|
|
if (!project) {
|
|
return false;
|
|
}
|
|
|
|
let read = false,
|
|
write = false;
|
|
|
|
if (ctx.user?.id === project.userId) {
|
|
read = true;
|
|
write = true;
|
|
}
|
|
|
|
if (["public", "unlisted"].includes(project.visibility as never)) {
|
|
read = true;
|
|
}
|
|
|
|
return permission === "r" ? read : write;
|
|
}
|
|
|
|
export async function getProjectById(id: number) {
|
|
const projectData = await db.query.project.findFirst({
|
|
where: and(eq(project.id, id), isNull(project.deletedAt)),
|
|
});
|
|
if (!projectData) {
|
|
throw new TRPCError({ code: "NOT_FOUND" });
|
|
}
|
|
return projectData;
|
|
}
|
|
|
|
export default projectRouter;
|