Skip to content

Commit

Permalink
feat(upload,large-file-upload): add authorization and file schema
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaosen7 committed Aug 5, 2024
1 parent a0eb90f commit 1486001
Show file tree
Hide file tree
Showing 28 changed files with 591 additions and 38 deletions.
5 changes: 4 additions & 1 deletion apps/large-file-upload/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
"version": "0.6.2",
"private": true,
"scripts": {
"dev": "next dev --turbo",
"dev": "next dev",
"build": "next build",
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"prisma-generate": "prisma generate",
"clean": "npcs clean"
},
"dependencies": {
"@clerk/nextjs": "^5.2.6",
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
"@npcs/env": "workspace:^",
"@npcs/log": "workspace:^",
"@npcs/next-config": "workspace:^",
"@npcs/playwright-config": "workspace:^",
Expand Down Expand Up @@ -56,6 +58,7 @@
"socket.io": "^4.7.5",
"socket.io-client": "^4.7.5",
"spark-md5": "^3.0.2",
"svix": "^1.28.0",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"type-fest": "^4.21.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
Warnings:
- You are about to drop the column `email` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `name` on the `User` table. All the data in the column will be lost.
- A unique constraint covering the columns `[clerkId]` on the table `User` will be added. If there are existing duplicate values, this will fail.
- Added the required column `clerkId` to the `User` table without a default value. This is not possible if the table is not empty.
*/
-- DropIndex
DROP INDEX "User_email_key";

-- AlterTable
ALTER TABLE "User" DROP COLUMN "email",
DROP COLUMN "name",
ADD COLUMN "clerkId" TEXT NOT NULL;

-- CreateIndex
CREATE UNIQUE INDEX "User_clerkId_key" ON "User"("clerkId");
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- CreateTable
CREATE TABLE "File" (
"id" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"userId" INTEGER NOT NULL,

CONSTRAINT "File_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "File" ADD CONSTRAINT "File_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
Warnings:
- The primary key for the `File` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The primary key for the `User` table will be changed. If it partially fails, the table could be left without primary key constraint.
*/
-- DropForeignKey
ALTER TABLE "File" DROP CONSTRAINT "File_userId_fkey";

-- AlterTable
ALTER TABLE "File" DROP CONSTRAINT "File_pkey",
ALTER COLUMN "id" SET DATA TYPE TEXT,
ALTER COLUMN "userId" SET DATA TYPE TEXT,
ADD CONSTRAINT "File_pkey" PRIMARY KEY ("id");

-- AlterTable
ALTER TABLE "User" DROP CONSTRAINT "User_pkey",
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ADD CONSTRAINT "User_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "User_id_seq";

-- AddForeignKey
ALTER TABLE "File" ADD CONSTRAINT "File_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
Warnings:
- A unique constraint covering the columns `[hash]` on the table `File` will be added. If there are existing duplicate values, this will fail.
- Added the required column `hash` to the `File` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "File" ADD COLUMN "hash" TEXT NOT NULL;

-- CreateIndex
CREATE UNIQUE INDEX "File_hash_key" ON "File"("hash");
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Warnings:
- You are about to drop the column `userId` on the `File` table. All the data in the column will be lost.
*/
-- DropForeignKey
ALTER TABLE "File" DROP CONSTRAINT "File_userId_fkey";

-- AlterTable
ALTER TABLE "File" DROP COLUMN "userId";

-- CreateTable
CREATE TABLE "_UserFiles" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "_UserFiles_AB_unique" ON "_UserFiles"("A", "B");

-- CreateIndex
CREATE INDEX "_UserFiles_B_index" ON "_UserFiles"("B");

-- AddForeignKey
ALTER TABLE "_UserFiles" ADD CONSTRAINT "_UserFiles_A_fkey" FOREIGN KEY ("A") REFERENCES "File"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "_UserFiles" ADD CONSTRAINT "_UserFiles_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `size` to the `File` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "File" ADD COLUMN "size" INTEGER NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX "File_hash_key";
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE "File" ADD COLUMN "chunkSize" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "concurrency" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "poolElapse" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "state" INTEGER NOT NULL DEFAULT 0,
ALTER COLUMN "size" SET DEFAULT 0;
18 changes: 15 additions & 3 deletions apps/large-file-upload/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,19 @@ datasource db {
}

model User {
id Int @id @default(autoincrement())
email String @unique
name String?
id String @id @default(uuid())
clerkId String @unique
files File[] @relation("UserFiles")
}

model File {
id String @id @default(uuid())
hash String
name String
users User[] @relation("UserFiles")
size Int @default(0)
concurrency Int @default(0)
chunkSize Int @default(0)
poolElapse Int @default(0)
state Int @default(0)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SignIn } from "@clerk/nextjs";
import React from "react";

const SignInPage: React.FC = () => {
return <SignIn />;
};

export default SignInPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SignUp } from "@clerk/nextjs";
import React from "react";

const SignUpPage: React.FC = () => {
return <SignUp />;
};

export default SignUpPage;
90 changes: 90 additions & 0 deletions apps/large-file-upload/src/app/api/webhooks/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { log } from "@/log";
import { prisma } from "@/prisma/client";
import { WebhookEvent } from "@clerk/nextjs/server";
import { env } from "@npcs/env/server";
import { headers } from "next/headers";
import { Webhook } from "svix";

export async function POST(req: Request) {
// You can find this in the Clerk Dashboard -> Webhooks -> choose the endpoint
const WEBHOOK_SECRET = env.WEBHOOK_SECRET;

if (!WEBHOOK_SECRET) {
throw new Error(
"Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local",
);
}

// Get the headers
const headerPayload = headers();
const svix_id = headerPayload.get("svix-id");
const svix_timestamp = headerPayload.get("svix-timestamp");
const svix_signature = headerPayload.get("svix-signature");

// If there are no headers, error out
if (!svix_id || !svix_timestamp || !svix_signature) {
return new Response("Error occured -- no svix headers", {
status: 400,
});
}

// Get the body
const payload = await req.json();
const body = JSON.stringify(payload);

// Create a new Svix instance with your secret.
const wh = new Webhook(WEBHOOK_SECRET);

let evt: WebhookEvent;

// Verify the payload with the headers
try {
evt = wh.verify(body, {
"svix-id": svix_id,
"svix-timestamp": svix_timestamp,
"svix-signature": svix_signature,
}) as WebhookEvent;
} catch (err) {
log.error("Error verifying webhook:", err);
return new Response("Error occured", {
status: 400,
});
}

// Do something with the payload
// For this guide, you simply log the payload to the console
const { id } = evt.data;
const eventType = evt.type;
switch (eventType) {
case "user.created": {
const { id } = evt.data;

await prisma.user.create({
data: {
clerkId: id,
},
});

break;
}

case "user.deleted": {
const { id } = evt.data;

await prisma.user.delete({
where: {
clerkId: id,
},
});

break;
}

default:
break;
}
log.log(`Webhook with and ID of ${id} and type of ${eventType}`);
log.log("Webhook body:", body);

return new Response("", { status: 200 });
}
15 changes: 10 additions & 5 deletions apps/large-file-upload/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ClerkProvider } from "@clerk/nextjs";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
Expand All @@ -15,10 +16,14 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${inter.className} flex h-screen flex-col gap-6 p-4`}>
{children}
</body>
</html>
<ClerkProvider>
<html lang="en">
<body
className={`${inter.className} flex h-screen flex-col items-center justify-center gap-6 p-4`}
>
{children}
</body>
</html>
</ClerkProvider>
);
}
70 changes: 67 additions & 3 deletions apps/large-file-upload/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"use server";
import { getCurrentUser, getCurrentUserOrThrow } from "@/actions/user";
import { log } from "@/log";
import { prisma } from "@/prisma/client";
import { auth } from "@clerk/nextjs/server";
import {
configuration,
FileSystemStorage,
IUploadProps,
startWebsocketServer,
Upload,
} from "@npcs/upload";
Expand All @@ -17,10 +23,68 @@ if (process.env.NEXT_PHASE !== "phase-production-build") {
startWebsocketServer();
}

export default function Home() {
const requireAuth = async () => {
"use server";
const { redirectToSignIn } = auth();
redirectToSignIn();
};

const onComplete: IUploadProps["onComplete"] = async (fileJSON) => {
"use server";
log.log(fileJSON);

const existingFile = await prisma.file.findFirst({
where: {
name: fileJSON.name,
hash: fileJSON.hash,
},
});

const user = await getCurrentUserOrThrow();
if (existingFile) {
await prisma.user.update({
where: user,
data: {
files: {
update: {
where: {
id: existingFile.id,
},
data: fileJSON,
},
},
},
});
} else {
await prisma.file.create({
data: {
...fileJSON,
users: {
connect: {
id: user.id,
},
},
},
});
}
};

export default async function Home() {
const user = await getCurrentUser();

return (
<div className="m-auto w-2/3 min-w-96">
<Upload />
<div className="w-2/3 min-w-96">
<Upload
input={
user
? undefined
: {
onClick: requireAuth,
}
}
maxSize={1024 * 1024 * 1024}
onComplete={onComplete}
/>
</div>
);
}
Loading

0 comments on commit 1486001

Please sign in to comment.