Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions app/(app)/uploadvideo/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client"
import { useRouter } from "next/navigation";
import React, { useState } from "react";
import React, { useState } from "react";
import axios from "axios";

import { formData } from "@/types/interfaces";
Expand All @@ -11,14 +10,11 @@ const UploadVideo = () => {
title: "",
description: "",
});

const [isUploading, setIsUploading] = useState(false);

const router = useRouter();

const MAX_FILE_SIZE = 70 * 1024 * 1024;

const handleSubmit = async (e: React.FormEvent) => {
const handleSubmit = async (e: React.SubmitEvent) => {
e.preventDefault();

if (!formData.file) return;
Expand Down
48 changes: 39 additions & 9 deletions app/api/upload-video/route.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,68 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";

import { prisma } from "@/lib/prisma";
import { videoOptions } from "@/utils/constants";
import { requireUser, uploadToCloudinary } from "@/utils/helpers";
import { uploadToCloudinary } from "@/utils/helpers";

export async function POST(request: NextRequest) {
try {
const userId = await requireUser();
// Authenticate user
const { userId } = await auth();

if (!userId) {
return NextResponse.json(
{
error: "Unauthorized",
message: "You must be logged in to upload videos",
},
{ status: 401 }
);
}

// Parse upload body
const formData = await request.formData();

const file = formData.get("file") as File | null;
const title = formData.get("title") as string;
const description = formData.get("description") as string;
const originalSize = formData.get("originalSize") as string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Derive originalSize from the uploaded file.

Lines 29 and 47 persist a client-controlled size value. An authenticated client can forge it. Store String(file.size) instead and remove originalSize from the client payload.

Also applies to: 47-47

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/upload-video/route.ts` at line 29, Update the upload handling around
the file persistence logic to derive the stored original size from the uploaded
file by using file.size converted to a string, rather than reading the
client-controlled originalSize form field. Remove originalSize from the client
payload and apply this change at both persistence points near the form-data
parsing and storage logic.


if (!file) {
return NextResponse.json({ error: "File not found" }, { status: 400 });
return NextResponse.json(
{ error: "File not found" },
{ status: 400 }
);
}

// Upload to Cloudinary
const result = await uploadToCloudinary(file, videoOptions);
Comment on lines 26 to 39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate the uploaded file on the server.

Line 26 only asserts that the multipart value is a File. A client can send a string, which makes uploadToCloudinary fail with a 500 response. The client-side 70 MiB limit can also be bypassed. Validate file instanceof File and reject files larger than 70 MiB before the Cloudinary call.

Proposed fix
-    const file = formData.get("file") as File | null;
+    const file = formData.get("file");
     const title = formData.get("title") as string;
     const description = formData.get("description") as string;
-    const originalSize = formData.get("originalSize") as string;
+    const MAX_FILE_SIZE = 70 * 1024 * 1024;

-    if (!file) {
+    if (!(file instanceof File)) {
       return NextResponse.json(
         { error: "File not found" },
         { status: 400 }
       );
     }
+    if (file.size > MAX_FILE_SIZE) {
+      return NextResponse.json({ error: "File size too large" }, { status: 413 });
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/upload-video/route.ts` around lines 26 - 39, Update the file
validation in the upload handler before uploadToCloudinary: require file to be
an actual File instance and reject files larger than 70 MiB with the existing
400-style validation response. Keep the Cloudinary call reachable only after
both checks pass.


// Save video
const video = await prisma.video.create({
data: {
Title: title,
description: description,
description,
publicId: result.public_id,
originalSize: originalSize,
originalSize,
compressedSize: String(result.bytes),
duration: result.duration || 0,
duration: result.duration?.toString() || "0",

// If your schema has user relation:
// userId: userId
},
});
Comment on lines +38 to 54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n 'destroy\(' node_modules/cloudinary --glob '*.{ts,d.ts,js}'

Repository: CodingWithTushar/learningsaas

Length of output: 286


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Tracked files matching upload-video route and Cloudinary deps:\n'
git ls-files | rg '(^app/api/upload-video/route\.ts$|package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$' || true

printf '\nUpload-video route excerpt:\n'
if [ -f app/api/upload-video/route.ts ]; then
  nl -ba app/api/upload-video/route.ts | sed -n '1,140p'
fi

printf '\nSearch for uploadToCloudinary and destroy/delete usages:\n'
rg -n 'uploadToCloudinary|cloudinary|destroy\(|public_id|publicId|bytes|duration' . \
  --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' || true

printf '\nPackage manifests / lock references to cloudinary:\n'
( [ -f package.json ] && cat package.json ) || true
( [ -f pnpm-lock.yaml ] && rg -n 'cloudinary' pnpm-lock.yaml || true ) || true
( [ -f package-lock.json ] && cat package-lock.json | jq -r '.. | objects | .dependencies??.cloudinary??.version // empty, .dependencies??.devDependencies??.cloudinary??.version // empty' --argjson file $(mktemp) 2>/dev/null || true ) || true

Repository: CodingWithTushar/learningsaas

Length of output: 360


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Tracked files matching upload-video route and Cloudinary deps:\n'
git ls-files | rg '(^app/api/upload-video/route.ts$|package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$' || true

printf '\nUpload-video route excerpt:\n'
if [ -f app/api/upload-video/route.ts ]; then
  nl -ba app/api/upload-video/route.ts | sed -n '1,140p'
fi

printf '\nSearch for uploadToCloudinary and destroy/delete usages:\n'
rg -n 'uploadToCloudinary|cloudinary|destroy\(|public_id|publicId|bytes|duration' . \
  --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' || true

printf '\nPackage manifests / lock references to cloudinary:\n'
[ -f package.json ] && cat package.json || true
[ -f pnpm-lock.yaml ] && rg -n 'cloudinary' pnpm-lock.yaml || true
[ -f package-lock.json ] && rg -n '"cloudinary"' package-lock.json || true
[ -f yarn.lock ] && rg -n 'cloudinary' yarn.lock || true

Repository: CodingWithTushar/learningsaas

Length of output: 360


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Upload-video route excerpt:\n'
if [ -f app/api/upload-video/route.ts ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' app/api/upload-video/route.ts | sed -n '1,160p'
fi

printf '\nSearch for uploadToCloudinary and delete usages:\n'
rg -n 'uploadToCloudinary|cloudinary|destroy\(|delete\(|public_id|publicId|bytes|duration' . \
  --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' || true

printf '\npackage.json cloudinary entries:\n'
python3 - <<'PY'
import json
from pathlib import Path
p=Path('package.json')
if p.exists():
    data=json.loads(p.read_text())
    for section in ('dependencies','devDependencies'):
        deps=data.get(section,{})
        if 'cloudinary' in deps:
            print(f'[{section}] cloudinary={deps["cloudinary"]}')
PY

printf '\nlockfile cloudinary entries:\n'
rg -n 'cloudinary|node_modules/cloudinary' package-lock.json || true

Repository: CodingWithTushar/learningsaas

Length of output: 10473


Clean up the Cloudinary asset when persistence fails.

uploadToCloudinary completes before prisma.video.create, and the catch block only returns 500. In app/api/upload-video/route.ts, keep result.public_id from the upload and delete that Cloudinary asset when prisma.video.create fails to avoid orphaned assets.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/upload-video/route.ts` around lines 38 - 54, Update the upload flow
around uploadToCloudinary and prisma.video.create to retain result.public_id and
delete the uploaded Cloudinary asset if video persistence fails. Keep the
cleanup within the failure path before returning the existing 500 response,
while preserving the normal successful upload and database-save behavior.


return NextResponse.json(video);

} catch (error) {
console.log("Upload Video failed", error);
return NextResponse.json({ error: "Upload Video failed" }, { status: 500 });
console.error("Upload Video failed", error);

return NextResponse.json(
{
error: "Upload Video failed",
},
{ status: 500 }
);
}
}
}
9 changes: 1 addition & 8 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,10 @@

import React, { useEffect, useRef, useState } from "react";
import { CldImage } from "next-cloudinary";
import { socialFormats } from "@/utils/constants";

type SocialFormat = keyof typeof socialFormats;

const socialFormats = {
"Instagram Square (1:1)": { width: 1080, height: 1080, aspectRatio: "1:1" },
"Instagram Protrait (4:5)": { width: 1080, height: 1350, aspectRatio: "4:5" },
"Twitter Post (16:9)": { width: 1200, height: 675, aspectRatio: "16:9" },
"Twtter Header (3:1)": { width: 1500, height: 500, aspectRatio: "3:1" },
"Facebook Cover (205:78)": { width: 820, height: 312, aspectRatio: "205:78" },
};

export default function Home() {
const [uploadImage, setUploadImage] = useState<string | null>(null);
const [format, setFormat] = useState<SocialFormat>("Instagram Square (1:1)");
Expand Down
145 changes: 145 additions & 0 deletions components/videoCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { getCldImageUrl, getCldVideoUrl } from "next-cloudinary";
import { useCallback, useEffect, useState } from "react";
import { filesize } from "filesize";
import { Clock, Download, FileDown, FileUp } from "lucide-react";

import { VideoProps } from "@/types/interfaces";

dayjs.extend(relativeTime);

const VideoCard: React.FC<VideoProps> = ({ video, onDownload }) => {
const [isHover, setIsHover] = useState(false);
const [previewError, setpreviewError] = useState(false);

const getThumbnailUrl = useCallback((publicId: string) => {
return getCldImageUrl({
src: publicId,
width: 400,
height: 255,
crop: "fill",
gravity: "auto",
format: "jpg",
quality: "auto",
assetType: "video",
});
}, []);

const getFullVideoUrl = useCallback((publicId: string) => {
return getCldVideoUrl({
src: publicId,
width: 1920,
height: 1080,
});
}, []);

const getPreviewVideoUrl = useCallback((publicId: string) => {
return getCldVideoUrl({
src: publicId,
width: 400,
height: 225,
rawTransformations: ["e_preview: duration_15:max_seg_9:min_seg_dur_1"],
});
}, []);

const formatSize = useCallback((size: number) => {
return filesize(size);
}, []);

const formatDuration = useCallback((seconds: number) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.round(seconds % 6);

return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
}, []);
Comment on lines +50 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a 60-second remainder.

Line 52 uses % 6, so most durations display an incorrect seconds value. For example, 78 seconds renders as 1:00 instead of 1:18.

Proposed fix
-    const remainingSeconds = Math.round(seconds % 6);
+    const remainingSeconds = Math.floor(seconds % 60);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const formatDuration = useCallback((seconds: number) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.round(seconds % 6);
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
}, []);
const formatDuration = useCallback((seconds: number) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
}, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/videoCard.tsx` around lines 50 - 55, Update the remainingSeconds
calculation in formatDuration to use a 60-second remainder, so durations convert
correctly into minutes and seconds while preserving the existing formatting.


const compressionPercentage = Math.round(
(1 - Number(video.compressedSize) / Number(video.originalSize)) * 100,
);

useEffect(() => {
setpreviewError(false);
}, [isHover]);

const handlePreviewError = () => {
setpreviewError(true);
};

return (
<>
<div
className="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300"
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
>
<figure className="ascept-video relative">
{isHover ? (
previewError ? (
<div className="w-full h-full flex items-center justify-center bg-grey-200">
<p className="text-red-500">Preview not available</p>
Comment on lines +76 to +80

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -a -t f '^tailwind\.config\..*$' . || true
rg -n -C 2 'ascept-video|bg-grey-200|absoulte|aspect-video|bg-gray-200|absolute' components/videoCard.tsx

Repository: CodingWithTushar/learningsaas

Length of output: 795


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\nNext docs availability:\n'
fd -t f . node_modules/next/dist/docs 2>/dev/null|sed -n '1,20p' || true
printf '\nPackage versions:\n'
if [ -f package.json ]; then
  node - "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); const deps=Object.assign({}, p.dependencies, p.devDependencies); for (const k of ['next','tailwindcss','daisyui']) if (deps[k]) console.log(k+': '+deps[k])"
fi
printf '\nCustom config theme colors/extensions:\n'
for f in $(fd -t f '^tailwind\.config\..*$' . 2>/dev/null); do
  echo "--- $f"
  rg -n -C 3 "aspectVideo|aspect-video|aspect|grey|gray|gray-200|grey-200|absolute|aspect-ratio" "$f" || true
done
printf '\nComponent relevant section:\n'
sed -n '60,115p' components/videoCard.tsx

Repository: CodingWithTushar/learningsaas

Length of output: 874


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Next docs availability:\n'
if [ -d node_modules/next/dist/docs ]; then
  fd -t f . node_modules/next/dist/docs | sed -n '1,20p'
else
  echo 'node_modules/next/dist/docs not present'
fi

printf '\nPackage versions:\n'
if [ -f package.json ]; then
  node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf八','utf8')); const deps=Object.assign({}, p.dependencies, p.devDependencies); for (const k of ['next','tailwindcss','daisyui']) if (deps[k]) console.log(k + ': ' + deps[k]);"
fi

printf '\nTailwind/Daisy UI custom config extensions:\n'
while IFS= read -r f; do
  echo "--- $f"
  rg -n -C 3 "aspectVideo|aspect-video|aspect|grey|gray|gray-200|grey-200|base-100" "$f" || true
done < <(fd -t f '^tailwind\.config\..*$' . 2>/dev/null || true)

if [ -f tailwind.config.js ]; then
  echo "--- tailwind.config.js structure"
  sed -n '1,160p' tailwind.config.js
fi

printf '\nvideoCard relevant section:\n'
sed -n '60,115p' components/videoCard.tsx

printf '\nOccurrences in tracked files:\n'
rg -n 'ascept-video|bg-grey-200|absoulte|absolute bottom-2|aspect-video' . || true

Repository: CodingWithTushar/learningsaas

Length of output: 986


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Package versions:\n'
if [ -f package.json ]; then
  node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); const deps=Object.assign({}, p.dependencies, p.devDependencies); for (const k of ['next','tailwindcss','daisyui']) if (deps[k]) console.log(k + ': ' + deps[k]);"
fi

printf '\nTailwind/Daisy UI custom config extensions:\n'
while IFS= read -r f; do
  echo "--- $f"
  rg -n -C 4 "aspectVideo|aspect-video|aspect|grey|gray|gray-200|grey-200|base-100" "$f" || true
done < <(fd -t f '^tailwind\.config\..*$' . 2>/dev/null || true)

printf '\nvideoCard relevant section:\n'
sed -n '60,115p' components/videoCard.tsx

printf '\nOccurrences in tracked files:\n'
rg -n 'ascept-video|bg-grey-200|absoulte|absolute bottom-2|aspect-video' . || true

Repository: CodingWithTushar/learningsaas

Length of output: 334


Fix the Tailwind class typos.

ascept-video, bg-grey-200, and absoulte are misspelled classes, so Tailwind/DaisyUI will not generate the intended styles. Use aspect-video, bg-gray-200, and absolute.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/videoCard.tsx` around lines 76 - 80, Correct the misspelled
Tailwind classes in the video card markup: update ascept-video to aspect-video,
bg-grey-200 to bg-gray-200, and absoulte to absolute, preserving the surrounding
layout and preview-error behavior.

</div>
) : (
<video
src={getPreviewVideoUrl(video.publicId)}
autoPlay
muted
loop
className="w-full h-full object-cover"
onError={handlePreviewError}
/>
)
) : (
<img
src={getThumbnailUrl(video.publicId)}
alt={video.Title}
className="w-full h-full object-cover"
/>
)}

<div className="absoulte bottom-2 right-2 bg-base-100 bg-opacity-70 px-2 py-1 rounded-lg text-sm flex items-center">
<Clock size={16} className="mr-1" />
{formatDuration(Number(video.duration))}
</div>
</figure>
<div className="card-body p-4">
<h2 className="card-title text-lg font-bold ">{video.Title}</h2>

<p className="text-sm text-base-content opacity-70 mb-4">
Uploaded {dayjs(video.createdAt).fromNow()}
</p>

<div className="grid grid-cols-2 gap-4 text-sm">
<div className="flex items-center">
<FileUp size={18} className="mr-2 text-primary" />
<div>
<div className="font-semibold">Original</div>
<div>{formatSize(Number(video.originalSize))}</div>
</div>
</div>
<div className="flex items-center">
<FileDown size={18} className="mr-2 text-secondary"/>
</div>
<div>
<div className="font-semibold">Compressed</div>
<div>{formatSize(Number(video.compressedSize))}</div>
</div>
</div>
</div>
<div className="flex justify-between items-center mt-4">
<div className="text-sm font-semibold">
Compression: {" "}
<span className="text-accent">{compressionPercentage}%</span>
</div>

<button className="btn btn-primary btn-sm" onClick={() => onDownload(getFullVideoUrl(video.publicId), video.Title)}>
<Download size={16}/>
</button>
Comment on lines +135 to +137

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add an accessible name to the download button.

Line 135 renders an icon-only button. Neither the button nor the icon receives an accessible name. Add an aria-label that identifies the download action and video. Interactive controls require an accessible name. (w3.org)

Proposed fix
-          <button className="btn btn-primary btn-sm" onClick={() => onDownload(getFullVideoUrl(video.publicId), video.Title)}>
+          <button
+            className="btn btn-primary btn-sm"
+            aria-label={`Download ${video.Title}`}
+            onClick={() => onDownload(getFullVideoUrl(video.publicId), video.Title)}
+          >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button className="btn btn-primary btn-sm" onClick={() => onDownload(getFullVideoUrl(video.publicId), video.Title)}>
<Download size={16}/>
</button>
<button
className="btn btn-primary btn-sm"
aria-label={`Download ${video.Title}`}
onClick={() => onDownload(getFullVideoUrl(video.publicId), video.Title)}
>
<Download size={16}/>
</button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/videoCard.tsx` around lines 135 - 137, Add an aria-label to the
icon-only download button in the video card, using the video title to identify
which video will be downloaded. Keep the existing onClick behavior and Download
icon unchanged.


</div>
</div>
</>
);
};

export default VideoCard;
28 changes: 17 additions & 11 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
import { clerkMiddleware, createRouteMatcher,} from "@clerk/nextjs/server";
import { NextResponse } from "next/server";

const isPublicRoutes = createRouteMatcher([
Expand All @@ -13,16 +13,22 @@ const isPublicRoutes = createRouteMatcher([
"/privacy",
]);

const isPublicApiRoutes = createRouteMatcher(["/api/videos"]);
const isPublicApiRoutes = createRouteMatcher(["/api/uploadvideo"]);

const isAdminRoute = createRouteMatcher([
"/admin(.*)",
"api/admin(.*)",
"/dashboard/analytics",
]);

export default clerkMiddleware((auth, req) => {
const { userId } = auth;
export default clerkMiddleware(async (auth, req) => {

if (req.nextUrl.pathname === "/api/upload-video") {
return NextResponse.next();
}

const {userId} = await auth();

const currentUrl = new URL(req.url);
const isHomePage = currentUrl.pathname === "/home";
const isApiReq = currentUrl.pathname.startsWith("/api");
Expand All @@ -34,15 +40,15 @@ export default clerkMiddleware((auth, req) => {
}

//If user is not an admin then redirect to the "/signin" route.
if (userId.role !== "admin") {
return NextResponse.redirect(new URL("/sign-in", req.url));
}
// if (userId.role !== "admin") {
// return NextResponse.redirect(new URL("/sign-in", req.url));
// }
}
Comment on lines +43 to 46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the existing Clerk role-claim convention before adding the admin predicate.
rg -n -C3 'sessionClaims|publicMetadata|privateMetadata|orgRole|role' \
  --glob '*.{ts,tsx}' .

Repository: CodingWithTushar/learningsaas

Length of output: 495


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file outline =="
ast-grep outline middleware.ts || true

echo "== middleware.ts =="
cat -n middleware.ts

echo "== package / clerk docs candidates =="
git ls-files 'node_modules/next/dist/docs/**' | sed -n '1,80p'
ls -1 node_modules/next/dist/docs 2>/dev/null | sed -n '1,80p' || true

echo "== imports/usages middleware.ts =="
rg -n -C2 'userId|isAdminRoute|userId\.role|role|sign-in|analytics|dashboard' middleware.ts

Repository: CodingWithTushar/learningsaas

Length of output: 4842


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== auth/session claim patterns in tracked source =="
rm -f /tmp/claim_search_output.txt
rg -n -C2 'auth\(\)|userId|sessionClaims|publicMetadata|privateMetadata|orgRole|role' \
  --glob '*.{ts,tsx}' --glob '!**/node_modules/**' . > /tmp/claim_search_output.txt || true
cat /tmp/claim_search_output.txt

echo "== Clerk config/package files =="
rg -n '"`@clerk/`(nextjs|react|backend-api)"|clerk' package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null || true

echo "== next version if present =="
node - <<'JS'
try {
  const pkg = JSON.parse(require('path').resolve('package.json'));
  const deps = {...(pkg.dependencies||{}), ...(pkg.devDependencies||{})};
  console.log(JSON.stringify({
    next: deps.next,
    clerkNextjs: deps['`@clerk/nextjs`'],
    clerkReact: deps['`@clerk/react`'],
    clerkBackend: deps['`@clerk/backend-api`'],
  }, null, 2));
} catch (e) {
  console.log(String(e));
}
JS

Repository: CodingWithTushar/learningsaas

Length of output: 4142


🌐 Web query:

Clerk Next.js auth() SessionClaims sessionClaims orgRole documentation

💡 Result:

In Clerk's Next.js integration, the auth() helper (used in the App Router) and the useAuth() hook (used on the client side) provide access to session information, including sessionClaims and orgRole. Auth and SessionClaims The auth() helper returns an Auth object [1][2]. The sessionClaims property within this object contains the user's JWT payload, which includes any custom claims you have configured in your Clerk dashboard or via custom session tokens [1][3]. Example (App Router): import { auth } from '@clerk/nextjs/server'; export default async function Page { const { sessionClaims } = await auth; const customClaim = sessionClaims?.metadata?.someKey; // Access your custom claims //... } OrgRole and Authorization orgRole represents the current user's role in their active organization [1][4]. It is available directly as a property on the object returned by auth() and useAuth() [1][4]. For robust authorization checks, it is recommended to use the has() method rather than checking orgRole directly [2][5]. The has() method is available on the Auth object and can verify roles, permissions, features, or plans [1][2][6]. Example (Authorization Check): import { auth } from '@clerk/nextjs/server'; export default async function Page { const { has } = await auth; // Check if the user has a specific role if (!has({ role: 'org:admin' })) { return

You do not have permission to access this page.

; } return

Welcome, Admin!

; } Key Distinctions - Use auth() for server-side logic (e.g., Server Components, API Routes) [1][3]. - Use useAuth() for client-side components [4][7]. - Prefer the has() helper for authorization to ensure your code remains flexible if you switch from role-based access control (RBAC) to permission-based access control later [2][5].

Citations:


Restore admin authorization before allowing admin routes.

isAdminRoute(req) currently only checks authentication, so any logged-in user can access /admin, api/admin, and /dashboard/analytics. Use the Clerk auth() authorization helper/claim (for example, has({ role: ... }) or the configured custom admin claim) and deny non-admin users with a 403. Do not use userId.role, because userId is the user identifier, not a role object.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@middleware.ts` around lines 43 - 46, Update isAdminRoute(req) to authorize
authenticated users using Clerk’s auth() role helper or configured admin claim,
rather than checking userId.role. Allow admin access to /admin, api/admin, and
/dashboard/analytics; return a 403 response for authenticated non-admin users
while preserving unauthenticated handling.


//If user is LoggedIn and accessing the public route and this route is not a home then user will be go to the home route first then user can change it
if (userId && isPublicRoutes(req) && !isHomePage) {
return NextResponse.redirect(new URL("/home", req.url));
}
// if (userId && isPublicRoutes(req) && !isHomePage) {
// return NextResponse.redirect(new URL("/home", req.url));
// }

//If the User is not LoggedIn
if (!userId) {
Expand All @@ -69,7 +75,7 @@ export default clerkMiddleware((auth, req) => {

export const config = {
matcher: [
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
"/(api|trpc)(.*)",
"/__clerk/(.*)",
],
Expand Down
12 changes: 12 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,16 @@ const nextConfig: NextConfig = {
},
};

module.exports = {
experimental: {
serverActions: {
bodySizeLimit: '70mb',
proxyClientMaxBodySize: '70mb',
middlewareClientMaxBodySize: 100 * 1024 * 1024,
},
},
}
Comment on lines +26 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n 'module\.exports|export default|"type"' next.config.ts package.json

Repository: CodingWithTushar/learningsaas

Length of output: 284


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '--- package metadata ---\n'
sed -n '1,80p' package.json

printf '\n--- next.config.ts ---\n'
cat -n next.config.ts

printf '\n--- node_modules next docs availability ---\n'
if [ -d node_modules/next/dist/docs ]; then
  find node_modules/next/dist/docs -maxdepth 2 -type f | sed -n '1,40p'
else
  echo "node_modules/next/dist/docs not present"
fi

printf '\n--- next source snippets if present ---\n'
if [ -d node_modules/next/dist/build ]; then
  rg -n "serverActions|bodySizeLimit|proxyClientMaxBodySize|middlewareClientMaxBodySize|CONFIG_FILE" node_modules/next/dist/build node_modules/next/dist -g '*.js' -g '*.ts' | sed -n '1,120p'
else
  echo "node_modules/next/dist/build not present"
fi

Repository: CodingWithTushar/learningsaas

Length of output: 258


Keep one Next.js configuration export.

The file already defines nextConfig and exports it later. This module.exports object is separate and does not merge with nextConfig, so the effective config can drop existing images, eslint, and typescript settings. Add the supported options to nextConfig and keep only export default nextConfig.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@next.config.ts` around lines 26 - 34, Update the existing nextConfig object
to include the supported experimental serverActions body-size settings,
preserving its images, eslint, and typescript configuration. Remove the separate
module.exports assignment and retain only export default nextConfig so all
settings are emitted through one configuration object.

Source: Coding guidelines


🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n --glob 'package*.json' '"next"\s*:|proxyClientMaxBodySize|middlewareClientMaxBodySize|serverActions|bodySizeLimit'

if [ -f node_modules/next/package.json ]; then
  rg -n '"version"' node_modules/next/package.json
fi

if [ -f node_modules/next/dist/server/config-schema.js ]; then
  rg -n 'proxyClientMaxBodySize|middlewareClientMaxBodySize|serverActions|bodySizeLimit' \
    node_modules/next/dist/server/config-schema.js
fi

Repository: CodingWithTushar/learningsaas

Length of output: 260


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== candidate config and route files =="
git ls-files | rg '(^|/)next\.config\.(ts|js|mjs|cjs)$' || true
git ls-files | rg '(^|/)app/api/upload-video/route\.(ts|js|mjs|cjs)$' || true

echo
echo "== package manifests and lockfiles =="
git ls-files | rg '(^|/)(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$' || true

echo
echo "== config/route relevant text =="
for f in $(git ls-files | rg '(^|/)next\.config\.(ts|js|mjs|cjs)$' || true); do
  echo "--- $f"
  sed -n '1,120p' "$f"
done
for f in $(git ls-files | rg '(^|/)app/api/upload-video/route\.(ts|js|mjs|cjs)$' || true); do
  echo "--- $f"
  sed -n '1,220p' "$f"
done

echo
echo "== Next manifests in repo =="
for f in $(git ls-files | rg '(^|/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$' || true); do
  echo "--- $f"
  rg -n '"next"|next@|/next@|next:' "$f" | head -80 || true
done

echo
echo "== installed node_modules outline =="
git ls-files node_modules/next/package.json node_modules/next/dist/server/config-schema.js node_modules/next/docs node_modules/next/dist/docs 2>/dev/null || true
if [ -d node_modules/next ]; then
  if [ -f node_modules/next/package.json ]; then
    jq -r '.version' node_modules/next/package.json
  fi
  if [ -f node_modules/next/dist/server/config-schema.js ]; then
    rg -n 'proxyClientMaxBodySize|middlewareClientMaxBodySize|serverActions|bodySizeLimit' node_modules/next/dist/server/config-schema.js || true
  fi
fi

Repository: CodingWithTushar/learningsaas

Length of output: 3178


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== installed next package =="
if [ -f node_modules/next/package.json ]; then
  jq -r '{name, version, engines}' node_modules/next/package.json
else
  echo "node_modules/next/package.json not found"
fi

echo
echo "== installed next schema section =="
if [ -f node_modules/next/dist/server/config-schema.js ]; then
  sed -n '300,370p' node_modules/next/dist/server/config-schema.js
fi

echo
echo "== installed next docs for docs/config/deprecations =="
find node_modules/next/dist/docs node_modules/next/docs node_modules/next/dist -maxdepth 3 -type f \( -iname '*config*' -o -iname '*deprecat*' -o -iname '*server-actions*' \) 2>/dev/null | sort | head -200 || true

echo
echo "== installed next docs text for upload/body/serverActions =="
for f in $(find node_modules/next/dist/docs node_modules/next/docs node_modules/next/dist -maxdepth 3 -type f 2>/dev/null | rg -i 'config|deprecat|document|upload|server-action|route|middleware' | head -100); do
  echo "--- $f"
  rg -n -i 'serverActions|proxyClientMaxBodySize|middlewareClientMaxBodySize|bodySizeLimit|formData|Cloudinary|upload|route handler|request upload|multipart' "$f" | head -50 || true
done

Repository: CodingWithTushar/learningsaas

Length of output: 50385


Remove serverActions config for a Route Handler.

This app uses Next 15.5.22, where proxyClientMaxBodySize and bodySizeLimit are valid Next config keys, but app/api/upload-video/route.ts is not a Server Action. That Route Handler parses request.formData(), so bodySizeLimit does not enforce the upload limit there. Enforce the limit in the route handler or at deployment ingress, and account for multipart formData overhead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@next.config.ts` around lines 26 - 34, Remove the experimental.serverActions
configuration from the Next config, including bodySizeLimit and
proxyClientMaxBodySize, because the upload uses the
app/api/upload-video/route.ts Route Handler rather than a Server Action. Enforce
the upload size in the route handler’s request.formData() flow or deployment
ingress, accounting for multipart form-data overhead.

Source: Coding guidelines




export default nextConfig;
Loading