-
Notifications
You must be signed in to change notification settings - Fork 0
09 smart video preview with cloudinary ai #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 |
||
|
|
||
| // 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ) || trueRepository: 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 || trueRepository: 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 || trueRepository: CodingWithTushar/learningsaas Length of output: 10473 Clean up the Cloudinary asset when persistence fails.
🤖 Prompt for AI Agents |
||
|
|
||
| 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 } | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Proposed fix- const remainingSeconds = Math.round(seconds % 6);
+ const remainingSeconds = Math.floor(seconds % 60);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.tsxRepository: 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.tsxRepository: 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' . || trueRepository: 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' . || trueRepository: CodingWithTushar/learningsaas Length of output: 334 Fix the Tailwind class typos.
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| </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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export default VideoCard; | ||||||||||||||||||||||||||
| 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([ | ||
|
|
@@ -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"); | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.tsRepository: 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));
}
JSRepository: CodingWithTushar/learningsaas Length of output: 4142 🌐 Web query:
💡 Result: In Clerk's Next.js integration, the You do not have permission to access this page. ; } returnWelcome, Admin! ; } Key Distinctions - Useauth() 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.
🤖 Prompt for AI Agents |
||
|
|
||
| //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) { | ||
|
|
@@ -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/(.*)", | ||
| ], | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.jsonRepository: 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"
fiRepository: CodingWithTushar/learningsaas Length of output: 258 Keep one Next.js configuration export. The file already defines 🤖 Prompt for AI AgentsSource: 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
fiRepository: 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
fiRepository: 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
doneRepository: CodingWithTushar/learningsaas Length of output: 50385 Remove This app uses Next 15.5.22, where 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
|
|
||
|
|
||
| export default nextConfig; | ||
There was a problem hiding this comment.
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
originalSizefrom 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 removeoriginalSizefrom the client payload.Also applies to: 47-47
🤖 Prompt for AI Agents