This commit is contained in:
asabizanjo
2025-12-11 01:05:24 +00:00
parent c713d58f98
commit 423ce1bc6d
88 changed files with 4081 additions and 122 deletions

View File

@@ -0,0 +1,39 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import {
createSessionToken,
setSessionCookie,
verifyPassword,
} from "@/lib/auth";
export async function POST(req: Request) {
const body = await req.json().catch(() => null);
const email = (body?.email as string | undefined)?.toLowerCase()?.trim();
const password = body?.password as string | undefined;
if (!email || !password) {
return NextResponse.json(
{ error: "Email and password are required." },
{ status: 400 }
);
}
const user = await prisma.user.findUnique({ where: { email } });
if (!user) {
return NextResponse.json({ error: "Invalid credentials." }, { status: 401 });
}
const valid = await verifyPassword(password, user.passwordHash);
if (!valid) {
return NextResponse.json({ error: "Invalid credentials." }, { status: 401 });
}
const token = await createSessionToken({ userId: user.id, email: user.email });
await setSessionCookie(token);
return NextResponse.json({
ok: true,
user: { id: user.id, email: user.email },
});
}

View File

@@ -0,0 +1,8 @@
import { NextResponse } from "next/server";
import { clearSessionCookie } from "@/lib/auth";
export async function POST() {
await clearSessionCookie();
return NextResponse.json({ ok: true });
}

View File

@@ -0,0 +1,42 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import {
createSessionToken,
hashPassword,
setSessionCookie,
} from "@/lib/auth";
export async function POST(req: Request) {
const body = await req.json().catch(() => null);
const email = (body?.email as string | undefined)?.toLowerCase()?.trim();
const password = body?.password as string | undefined;
if (!email || !password || password.length < 6) {
return NextResponse.json(
{ error: "Email and password (min 6 chars) are required." },
{ status: 400 }
);
}
const existing = await prisma.user.findUnique({ where: { email } });
if (existing) {
return NextResponse.json(
{ error: "Email is already registered." },
{ status: 400 }
);
}
const passwordHash = await hashPassword(password);
const user = await prisma.user.create({
data: { email, passwordHash },
});
const token = await createSessionToken({ userId: user.id, email: user.email });
await setSessionCookie(token);
return NextResponse.json({
ok: true,
user: { id: user.id, email: user.email },
});
}

View File

@@ -0,0 +1,31 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import { getSessionUser } from "@/lib/auth";
import { deleteFromR2 } from "@/lib/r2";
type Props = {
params: Promise<{ id: string }>;
};
export async function DELETE(_req: Request, { params }: Props) {
const session = await getSessionUser();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { id } = await params;
const file = await prisma.file.findFirst({
where: { id, userId: session.userId },
});
if (!file) {
return NextResponse.json({ error: "File not found" }, { status: 404 });
}
await deleteFromR2(file.key);
await prisma.file.delete({ where: { id: file.id } });
return NextResponse.json({ ok: true });
}

View File

@@ -0,0 +1,29 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import { getSessionUser } from "@/lib/auth";
import { getSignedDownloadUrl } from "@/lib/r2";
type Props = {
params: Promise<{ id: string }>;
};
export async function GET(_req: Request, { params }: Props) {
const session = await getSessionUser();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { id } = await params;
const file = await prisma.file.findFirst({
where: { id, userId: session.userId },
});
if (!file) {
return NextResponse.json({ error: "File not found" }, { status: 404 });
}
const url = await getSignedDownloadUrl(file.key);
return NextResponse.json({ url, name: file.name });
}

View File

@@ -0,0 +1,33 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import { getSessionUser } from "@/lib/auth";
import { deleteFromR2 } from "@/lib/r2";
export async function DELETE() {
const session = await getSessionUser();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Get all files for the user
const files = await prisma.file.findMany({
where: { userId: session.userId },
select: { id: true, key: true },
});
if (files.length === 0) {
return NextResponse.json({ ok: true, deleted: 0 });
}
// Delete all files from R2
const deletePromises = files.map((file) => deleteFromR2(file.key));
await Promise.all(deletePromises);
// Delete all file records from database
await prisma.file.deleteMany({
where: { userId: session.userId },
});
return NextResponse.json({ ok: true, deleted: files.length });
}

View File

@@ -0,0 +1,28 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import { getSessionUser } from "@/lib/auth";
export async function GET() {
const session = await getSessionUser();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const files = await prisma.file.findMany({
where: { userId: session.userId },
orderBy: { createdAt: "desc" },
});
return NextResponse.json({
files: files.map((f) => ({
id: f.id,
key: f.key,
name: f.name,
relativePath: f.relativePath,
contentType: f.contentType,
sizeBytes: Number(f.sizeBytes),
createdAt: f.createdAt,
})),
});
}

View File

@@ -0,0 +1,81 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import { getSessionUser } from "@/lib/auth";
import { uploadToR2 } from "@/lib/r2";
const MAX_UPLOAD_SIZE = 10 * 1024 * 1024 * 1024; // 10GB in bytes
function serializeFile(file: Awaited<ReturnType<typeof prisma.file.create>>) {
return {
id: file.id,
key: file.key,
name: file.name,
relativePath: file.relativePath,
contentType: file.contentType,
sizeBytes: Number(file.sizeBytes),
createdAt: file.createdAt,
};
}
export async function POST(req: Request) {
const session = await getSessionUser();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const formData = await req.formData();
const files = formData.getAll("files") as File[];
if (!files.length) {
return NextResponse.json(
{ error: "No files received in 'files' field." },
{ status: 400 }
);
}
// Check total upload size
const totalSize = files.reduce((acc, file) => acc + (file instanceof File ? file.size : 0), 0);
if (totalSize > MAX_UPLOAD_SIZE) {
return NextResponse.json(
{ error: "Total upload size exceeds 10GB limit." },
{ status: 413 }
);
}
const uploaded = [];
for (const file of files) {
if (!(file instanceof File)) continue;
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const fileWithPath = file as File & { webkitRelativePath?: string };
const relativePath =
fileWithPath.webkitRelativePath ?? fileWithPath.name ?? "unnamed";
const normalizedPath = relativePath.replace(/\\/g, "/");
const name = normalizedPath.split("/").pop() ?? normalizedPath;
const key = `${session.userId}/${Date.now()}-${normalizedPath}`;
const contentType = file.type || "application/octet-stream";
await uploadToR2({
key,
contentType,
body: buffer,
});
const record = await prisma.file.create({
data: {
key,
name,
relativePath: normalizedPath,
contentType,
sizeBytes: BigInt(file.size),
userId: session.userId,
},
});
uploaded.push(serializeFile(record));
}
return NextResponse.json({ ok: true, files: uploaded });
}