Upgrade Next.js to 16.4.6 and show locks on latest videos
Deploy Vault CMS / deploy (push) Failing after 4m1s
Deploy Vault CMS / deploy (push) Failing after 4m1s
- Upgrade next and eslint-config-next from 16.3.5 to 16.4.6. The older version has critical advisories, which made the Docker build's `pnpm audit --audit-level critical` step fail and blocked deploys. - /api/videos/latest now reports whether the viewer can open each video; the dashboard shows a lock and the reason instead of opening the player. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server';
|
|||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
import { authOptions } from '@/lib/auth-options';
|
import { authOptions } from '@/lib/auth-options';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import { checkVideoAccess } from '@/lib/video-access';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@@ -70,7 +71,22 @@ export async function GET() {
|
|||||||
take: 10,
|
take: 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({ videos });
|
// Tell the dashboard which of these the user can actually open, so locked
|
||||||
|
// ones show a lock instead of leading to a refusal.
|
||||||
|
const role = (session.user as any).role;
|
||||||
|
const viewer = { id: user.id, isAdmin: role === 'admin' || role === 'superadmin' };
|
||||||
|
const withAccess = await Promise.all(
|
||||||
|
videos.map(async (video) => {
|
||||||
|
const access = await checkVideoAccess(viewer, video.id, { peek: true });
|
||||||
|
return {
|
||||||
|
...video,
|
||||||
|
accessible: access.ok,
|
||||||
|
lockReason: access.ok ? null : access.error,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json({ videos: withAccess });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('GET /api/videos/latest error', err);
|
console.error('GET /api/videos/latest error', err);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { SiteHeader } from '@/components/site-header';
|
|||||||
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
|
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
|
||||||
import { Lock } from 'lucide-react';
|
import { Lock } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
type VideoItem = {
|
type VideoItem = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -248,6 +249,10 @@ export default function DashboardClient() {
|
|||||||
<Card
|
<Card
|
||||||
className="@container/card overflow-hidden cursor-pointer"
|
className="@container/card overflow-hidden cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (v.accessible === false) {
|
||||||
|
toast.error(v.lockReason ?? 'This video is locked.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (v.playlist?.id) {
|
if (v.playlist?.id) {
|
||||||
handleOpenVideo(v.playlist.id, v.id);
|
handleOpenVideo(v.playlist.id, v.id);
|
||||||
}
|
}
|
||||||
@@ -261,6 +266,11 @@ export default function DashboardClient() {
|
|||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
{v.accessible === false ? (
|
||||||
|
<div className="absolute inset-0 bg-black/40 flex items-center justify-center">
|
||||||
|
<Lock className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-start pt-2">
|
<div className="flex gap-2 items-start pt-2">
|
||||||
{v.uploader?.image && (
|
{v.uploader?.image && (
|
||||||
|
|||||||
+11
-4
@@ -30,7 +30,12 @@ const allowed = new Map<string, number>();
|
|||||||
* be open to them: first in its playlist, instant access, already unlocked, or
|
* be open to them: first in its playlist, instant access, already unlocked, or
|
||||||
* earned by completing the video before it.
|
* earned by completing the video before it.
|
||||||
*/
|
*/
|
||||||
export async function checkVideoAccess(viewer: Viewer, videoId: string): Promise<AccessResult> {
|
export async function checkVideoAccess(
|
||||||
|
viewer: Viewer,
|
||||||
|
videoId: string,
|
||||||
|
// `peek` only answers the question (for listings); it records nothing.
|
||||||
|
opts: { peek?: boolean } = {}
|
||||||
|
): Promise<AccessResult> {
|
||||||
if (viewer.isAdmin) return { ok: true };
|
if (viewer.isAdmin) return { ok: true };
|
||||||
|
|
||||||
const key = `${viewer.id}:${videoId}`;
|
const key = `${viewer.id}:${videoId}`;
|
||||||
@@ -41,7 +46,7 @@ export async function checkVideoAccess(viewer: Viewer, videoId: string): Promise
|
|||||||
const where = { userId_videoId: { userId: viewer.id, videoId } };
|
const where = { userId_videoId: { userId: viewer.id, videoId } };
|
||||||
|
|
||||||
if (check.ok) {
|
if (check.ok) {
|
||||||
if (!check.free) {
|
if (!check.free && !opts.peek) {
|
||||||
// Earned but never recorded (e.g. completed before unlocks were tracked).
|
// Earned but never recorded (e.g. completed before unlocks were tracked).
|
||||||
await prisma.videoUnlock.upsert({
|
await prisma.videoUnlock.upsert({
|
||||||
where,
|
where,
|
||||||
@@ -56,7 +61,9 @@ export async function checkVideoAccess(viewer: Viewer, videoId: string): Promise
|
|||||||
if (!unlocked) return { ok: false, status: check.status, error: check.error };
|
if (!unlocked) return { ok: false, status: check.status, error: check.error };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowed.size >= MAX_ENTRIES) allowed.clear();
|
if (!opts.peek) {
|
||||||
allowed.set(key, Date.now() + TTL_MS);
|
if (allowed.size >= MAX_ENTRIES) allowed.clear();
|
||||||
|
allowed.set(key, Date.now() + TTL_MS);
|
||||||
|
}
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -45,7 +45,7 @@
|
|||||||
"formidable": "^3.5.4",
|
"formidable": "^3.5.4",
|
||||||
"hls.js": "^1.6.15",
|
"hls.js": "^1.6.15",
|
||||||
"lucide-react": "^0.554.0",
|
"lucide-react": "^0.554.0",
|
||||||
"next": "16.3.5",
|
"next": "16.4.6",
|
||||||
"next-auth": "^4.24.15",
|
"next-auth": "^4.24.15",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "19.3.0",
|
"react": "19.3.0",
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
"@types/unzipper": "^0.10.10",
|
"@types/unzipper": "^0.10.10",
|
||||||
"autoprefixer": "^10.4.22",
|
"autoprefixer": "^10.4.22",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.3.5",
|
"eslint-config-next": "16.4.6",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prisma": "^6.19.0",
|
"prisma": "^6.19.0",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
|
|||||||
Generated
+303
-325
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user