09 smart video preview with cloudinary ai - #8
Conversation
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe PR adds a Cloudinary-backed video card, centralizes social format data, and updates video upload authentication, middleware routing, persistence, and request body limits. ChangesVideo upload and display
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant UploadPage
participant ClerkMiddleware
participant UploadRoute
participant Cloudinary
participant Prisma
UploadPage->>ClerkMiddleware: Submit video upload
ClerkMiddleware->>ClerkMiddleware: Await auth()
ClerkMiddleware->>UploadRoute: Pass through /api/upload-video
UploadRoute->>Cloudinary: Upload video
Cloudinary-->>UploadRoute: Return video metadata
UploadRoute->>Prisma: Persist video metadata
Prisma-->>UploadRoute: Return saved video
UploadRoute-->>UploadPage: Return upload response
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (2)
components/videoCard.tsx (1)
61-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReset the preview error in the hover handler.
useEffectcallssetpreviewError(false)synchronously on each hover state change, causing an extra render cycle. Handle the hover reset in theonMouseEntercallback instead.🤖 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 61 - 63, Move the setpreviewError(false) call from the isHover-dependent useEffect into the video card’s onMouseEnter handler, so the preview error resets when hover begins without an extra effect-driven render. Remove the now-unnecessary useEffect while preserving the existing hover behavior.Source: Linters/SAST tools
middleware.ts (1)
16-28: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winUse the same path in the matcher and bypass.
Line 16 matches
/api/uploadvideo, but the client calls/api/upload-video. Line 26 bypasses the actual route separately. Set the matcher to/api/upload-videoand useisPublicApiRoutes(req)for the bypass.Proposed fix
-const isPublicApiRoutes = createRouteMatcher(["/api/uploadvideo"]); +const isPublicApiRoutes = createRouteMatcher(["/api/upload-video"]); -if (req.nextUrl.pathname === "/api/upload-video") { +if (isPublicApiRoutes(req)) { return NextResponse.next(); }🤖 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 16 - 28, Update isPublicApiRoutes to match "/api/upload-video", then replace the direct pathname check in clerkMiddleware with isPublicApiRoutes(req) for the public-route bypass.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@app/api/upload-video/route.ts`:
- 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.
- Around line 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.
- Around line 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.
In `@components/videoCard.tsx`:
- Around line 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.
- Around line 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.
- Around line 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.
In `@middleware.ts`:
- Around line 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.
In `@next.config.ts`:
- Around line 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.
- Around line 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.
In `@utils/constants.ts`:
- Line 18: Correct the user-facing format labels in the constants definition:
update the key containing “Instagram Protrait (4:5)” to “Instagram Portrait
(4:5)” and fix the “Twtter” label to “Twitter,” preserving their existing format
values.
---
Nitpick comments:
In `@components/videoCard.tsx`:
- Around line 61-63: Move the setpreviewError(false) call from the
isHover-dependent useEffect into the video card’s onMouseEnter handler, so the
preview error resets when hover begins without an extra effect-driven render.
Remove the now-unnecessary useEffect while preserving the existing hover
behavior.
In `@middleware.ts`:
- Around line 16-28: Update isPublicApiRoutes to match "/api/upload-video", then
replace the direct pathname check in clerkMiddleware with isPublicApiRoutes(req)
for the public-route bypass.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 7ab87755-c9a2-4905-ab5f-5cc8c87e0b9f
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (10)
app/(app)/uploadvideo/page.tsxapp/api/upload-video/route.tsapp/page.tsxcomponents/videoCard.tsxmiddleware.tsnext.config.tspackage.jsontypes/interfaces.tsutils/constants.tsutils/helpers.ts
💤 Files with no reviewable changes (1)
- utils/helpers.ts
| 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); |
There was a problem hiding this comment.
🩺 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.
| 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; |
There was a problem hiding this comment.
🗄️ 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.
| // Upload to Cloudinary | ||
| const result = await uploadToCloudinary(file, videoOptions); | ||
|
|
||
| // 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 | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🗄️ 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.
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.
| const formatDuration = useCallback((seconds: number) => { | ||
| const minutes = Math.floor(seconds / 60); | ||
| const remainingSeconds = Math.round(seconds % 6); | ||
|
|
||
| return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`; | ||
| }, []); |
There was a problem hiding this comment.
🎯 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.
| 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.
| <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> |
There was a problem hiding this comment.
🎯 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.
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.
| <button className="btn btn-primary btn-sm" onClick={() => onDownload(getFullVideoUrl(video.publicId), video.Title)}> | ||
| <Download size={16}/> | ||
| </button> |
There was a problem hiding this comment.
🎯 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.
| <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.
| // if (userId.role !== "admin") { | ||
| // return NextResponse.redirect(new URL("/sign-in", req.url)); | ||
| // } | ||
| } |
There was a problem hiding this comment.
🔒 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:
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.
; } 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:
- 1: https://clerk.com/docs/reference/backend/types/auth-object
- 2: https://clerk.com/docs/reference/nextjs/app-router/auth
- 3: https://clerk.com/docs/guides/sessions/customize-session-tokens
- 4: https://clerk.com/docs/nextjs/reference/hooks/use-auth
- 5: https://clerk.com/docs/nextjs/guides/organizations/control-access/check-access
- 6: https://clerk.com/docs/nextjs/reference/objects/session
- 7: https://clerk.com/docs/react/reference/hooks/use-auth
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.
| module.exports = { | ||
| experimental: { | ||
| serverActions: { | ||
| bodySizeLimit: '70mb', | ||
| proxyClientMaxBodySize: '70mb', | ||
| middlewareClientMaxBodySize: 100 * 1024 * 1024, | ||
| }, | ||
| }, | ||
| } |
There was a problem hiding this comment.
🎯 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 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
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 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 const socialFormats = { | ||
| "Instagram Square (1:1)": { width: 1080, height: 1080, aspectRatio: "1:1" }, | ||
| "Instagram Protrait (4:5)": { width: 1080, height: 1350, aspectRatio: "4:5" }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Correct the format labels.
Line 18 renders Protrait. Line 20 renders Twtter. These strings appear in the format selector.
Proposed fix
- "Instagram Protrait (4:5)": { width: 1080, height: 1350, aspectRatio: "4:5" },
+ "Instagram Portrait (4:5)": { width: 1080, height: 1350, aspectRatio: "4:5" },
- "Twtter Header (3:1)": { width: 1500, height: 500, aspectRatio: "3:1" },
+ "Twitter Header (3:1)": { width: 1500, height: 500, aspectRatio: "3:1" },Also applies to: 20-20
🤖 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 `@utils/constants.ts` at line 18, Correct the user-facing format labels in the
constants definition: update the key containing “Instagram Protrait (4:5)” to
“Instagram Portrait (4:5)” and fix the “Twtter” label to “Twitter,” preserving
their existing format values.
Introduces server-side logic to request Cloudinary AI-generated previews and thumbnails for uploaded videos.
Adds client-side components to display preview thumbnails and short preview clips in the course/video listing and player UI.
Stores preview metadata (preview URL, thumbnail URL, generation status, timestamps) alongside existing video records.
Adds background job / async task handling so preview generation is retried/resilient and does not block upload flow.
Includes configuration and environment variables for Cloudinary integration and feature toggles.
Adds basic tests for the new preview generation flow and a small set of UI tests for the preview display.