From 14b62b3f4ac5272cc96ea97a9233ea142b137e78 Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Mon, 9 Sep 2024 17:35:00 -0500 Subject: [PATCH] Checking for admin, subscribed, or purchased videoId in get video endpoint --- .../20240906171109_email_auth/migration.sql | 71 ------------------- .../migration.sql | 66 ++++++++++++++++- prisma/schema.prisma | 1 + src/components/banner/HeroBanner.js | 23 +++--- src/pages/api/auth/[...nextauth].js | 1 - src/pages/api/get-video-url.js | 44 ++++++++---- src/pages/details/[slug]/index.js | 71 +++++++++++++++---- src/pages/draft/[slug]/index.js | 24 ++++--- 8 files changed, 182 insertions(+), 119 deletions(-) delete mode 100644 prisma/migrations/20240906171109_email_auth/migration.sql rename prisma/migrations/{20240901171426_init => 20240909215121_init}/migration.sql (76%) diff --git a/prisma/migrations/20240906171109_email_auth/migration.sql b/prisma/migrations/20240906171109_email_auth/migration.sql deleted file mode 100644 index ec8cc37..0000000 --- a/prisma/migrations/20240906171109_email_auth/migration.sql +++ /dev/null @@ -1,71 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail. - -*/ --- AlterTable -ALTER TABLE "User" ADD COLUMN "email" TEXT, -ADD COLUMN "emailVerified" TIMESTAMP(3), -ADD COLUMN "image" TEXT, -ADD COLUMN "name" TEXT, -ADD COLUMN "privkey" TEXT, -ALTER COLUMN "pubkey" DROP NOT NULL; - --- CreateTable -CREATE TABLE "Session" ( - "id" TEXT NOT NULL, - "sessionToken" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "expires" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Session_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "VerificationToken" ( - "identifier" TEXT NOT NULL, - "token" TEXT NOT NULL, - "expires" TIMESTAMP(3) NOT NULL -); - --- CreateTable -CREATE TABLE "Account" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "type" TEXT NOT NULL, - "provider" TEXT NOT NULL, - "providerAccountId" TEXT NOT NULL, - "refresh_token" TEXT, - "access_token" TEXT, - "expires_at" INTEGER, - "token_type" TEXT, - "scope" TEXT, - "id_token" TEXT, - "session_state" TEXT, - "oauth_token_secret" TEXT, - "oauth_token" TEXT, - - CONSTRAINT "Account_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); - --- CreateIndex -CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); - --- CreateIndex -CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); - --- CreateIndex -CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- AddForeignKey -ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20240901171426_init/migration.sql b/prisma/migrations/20240909215121_init/migration.sql similarity index 76% rename from prisma/migrations/20240901171426_init/migration.sql rename to prisma/migrations/20240909215121_init/migration.sql index e8ee247..068c101 100644 --- a/prisma/migrations/20240901171426_init/migration.sql +++ b/prisma/migrations/20240909215121_init/migration.sql @@ -1,7 +1,12 @@ -- CreateTable CREATE TABLE "User" ( "id" TEXT NOT NULL, - "pubkey" TEXT NOT NULL, + "pubkey" TEXT, + "privkey" TEXT, + "name" TEXT, + "email" TEXT, + "emailVerified" TIMESTAMP(3), + "image" TEXT, "username" TEXT, "avatar" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -10,6 +15,43 @@ CREATE TABLE "User" ( CONSTRAINT "User_pkey" PRIMARY KEY ("id") ); +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL, + "sessionToken" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VerificationToken" ( + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL +); + +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + "oauth_token_secret" TEXT, + "oauth_token" TEXT, + + CONSTRAINT "Account_pkey" PRIMARY KEY ("id") +); + -- CreateTable CREATE TABLE "Role" ( "id" TEXT NOT NULL, @@ -81,6 +123,7 @@ CREATE TABLE "Resource" ( "userId" TEXT NOT NULL, "price" INTEGER NOT NULL DEFAULT 0, "noteId" TEXT, + "videoId" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, @@ -123,9 +166,24 @@ CREATE TABLE "CourseDraft" ( -- CreateIndex CREATE UNIQUE INDEX "User_pubkey_key" ON "User"("pubkey"); +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + -- CreateIndex CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); +-- CreateIndex +CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); + +-- CreateIndex +CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); + -- CreateIndex CREATE UNIQUE INDEX "Role_userId_key" ON "Role"("userId"); @@ -135,6 +193,12 @@ CREATE UNIQUE INDEX "Course_noteId_key" ON "Course"("noteId"); -- CreateIndex CREATE UNIQUE INDEX "Resource_noteId_key" ON "Resource"("noteId"); +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE "Role" ADD CONSTRAINT "Role_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f3e0f57..af91bb5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -138,6 +138,7 @@ model Resource { price Int @default(0) purchases Purchase[] noteId String? @unique + videoId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } diff --git a/src/components/banner/HeroBanner.js b/src/components/banner/HeroBanner.js index 9638acc..a10d99b 100644 --- a/src/components/banner/HeroBanner.js +++ b/src/components/banner/HeroBanner.js @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import useWindowWidth from '@/hooks/useWindowWidth'; import Image from 'next/image'; const HeroBanner = () => { @@ -6,6 +7,8 @@ const HeroBanner = () => { const [currentOption, setCurrentOption] = useState(0); const [isFlipping, setIsFlipping] = useState(false); + const windowWidth = useWindowWidth(); + const getColorClass = (option) => { switch (option) { case 'Bitcoin': return 'text-orange-400'; @@ -31,15 +34,17 @@ const HeroBanner = () => { return (
- Banner -
+ {windowWidth && ( + Banner + )} +

Learn how to code

Build{' '} diff --git a/src/pages/api/auth/[...nextauth].js b/src/pages/api/auth/[...nextauth].js index b231631..a8752ca 100644 --- a/src/pages/api/auth/[...nextauth].js +++ b/src/pages/api/auth/[...nextauth].js @@ -121,7 +121,6 @@ export const authOptions = { return token; }, async session({ session, token }) { - console.log('SESSION', session); // Add user from token to session session.user = token.user; session.jwt = token; diff --git a/src/pages/api/get-video-url.js b/src/pages/api/get-video-url.js index 90e34b7..b3ae67d 100644 --- a/src/pages/api/get-video-url.js +++ b/src/pages/api/get-video-url.js @@ -12,20 +12,36 @@ const s3Client = new S3Client({ }, }) +const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY + export default async function handler(req, res) { - const session = await getServerSession(req, res, authOptions) - - if (!session) { - return res.status(401).json({ error: "Unauthorized" }) - } - - const { videoKey } = req.query - - if (!videoKey) { - return res.status(400).json({ error: "Video key is required" }) - } - try { + // Check if the request method is GET + if (req.method !== 'GET') { + return res.status(405).json({ error: "Method Not Allowed" }) + } + + const session = await getServerSession(req, res, authOptions) + + if (!session) { + return res.status(401).json({ error: "Unauthorized" }) + } + + const { videoKey } = req.query + + if (!videoKey || typeof videoKey !== 'string') { + return res.status(400).json({ error: "Invalid or missing video key" }) + } + + // Check if the user is authorized to access the video + if (!session.user.role?.subscribed && session.user.pubkey !== AUTHOR_PUBKEY) { + const purchasedVideo = session.user.purchased?.find(purchase => purchase?.resource?.videoId === videoKey) + console.log("purchasedVideo", purchasedVideo) + if (!purchasedVideo) { + return res.status(403).json({ error: "Forbidden: You don't have access to this video" }) + } + } + const command = new GetObjectCommand({ Bucket: "plebdevs-bucket", Key: videoKey, @@ -37,7 +53,7 @@ export default async function handler(req, res) { res.redirect(signedUrl) } catch (error) { - console.error("Error generating signed URL:", error) - res.status(500).json({ error: "Failed to generate video URL" }) + console.error("Error in get-video-url handler:", error) + res.status(500).json({ error: "Internal Server Error" }) } } \ No newline at end of file diff --git a/src/pages/details/[slug]/index.js b/src/pages/details/[slug]/index.js index 2f58653..08a3acc 100644 --- a/src/pages/details/[slug]/index.js +++ b/src/pages/details/[slug]/index.js @@ -10,6 +10,7 @@ import ZapThreadsWrapper from '@/components/ZapThreadsWrapper'; import { useToast } from '@/hooks/useToast'; import { useNDKContext } from '@/context/NDKContext'; import ResourceDetails from '@/components/content/resources/ResourceDetails'; +import {ProgressSpinner} from 'primereact/progressspinner'; import 'primeicons/primeicons.css'; const MDDisplay = dynamic( @@ -30,6 +31,8 @@ export default function Details() { const [paidResource, setPaidResource] = useState(false); const [decryptedContent, setDecryptedContent] = useState(null); const [authorView, setAuthorView] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); const {ndk, addSigner} = useNDKContext(); const { data: session, update } = useSession(); @@ -75,7 +78,9 @@ export default function Details() { if (router.isReady) { const { slug } = router.query; - const fetchEvent = async (slug) => { + const fetchEvent = async (slug, retryCount = 0) => { + setLoading(true); + setError(null); try { await ndk.connect(); @@ -88,13 +93,33 @@ export default function Details() { if (event) { setEvent(event); if (user && user.pubkey === event.pubkey) { + const decryptedContent = await nip04.decrypt(privkey, pubkey, event.content); + setDecryptedContent(decryptedContent); setAuthorView(true); } + } else { + if (retryCount < 1) { + // Wait for 2 seconds before retrying + await new Promise(resolve => setTimeout(resolve, 3000)); + return fetchEvent(slug, retryCount + 1); + } else { + setError("Event not found"); + } } } catch (error) { console.error('Error fetching event:', error); + if (retryCount < 1) { + // Wait for 2 seconds before retrying + await new Promise(resolve => setTimeout(resolve, 3000)); + return fetchEvent(slug, retryCount + 1); + } else { + setError("Failed to fetch event. Please try again."); + } + } finally { + setLoading(false); } }; + if (ndk) { fetchEvent(slug); } @@ -190,21 +215,35 @@ export default function Details() { return null; } + if (loading) { + return

+ +
; + } + + if (error) { + return
+
{error}
+
; + } + return (
- + {processedEvent && ( + + )} {authorView && (
@@ -224,7 +263,9 @@ export default function Details() {
)}
- {renderContent()} + { + processedEvent && processedEvent.content && renderContent() + }
); diff --git a/src/pages/draft/[slug]/index.js b/src/pages/draft/[slug]/index.js index cb8f8b2..17feac7 100644 --- a/src/pages/draft/[slug]/index.js +++ b/src/pages/draft/[slug]/index.js @@ -30,6 +30,7 @@ export default function Draft() { const { returnImageProxy } = useImageProxy(); const { data: session, status } = useSession(); const [user, setUser] = useState(null); + const [videoId, setVideoId] = useState(null); const { width, height } = useResponsiveImageDimensions(); const router = useRouter(); const { showToast } = useToast(); @@ -63,7 +64,7 @@ export default function Draft() { } if (draft) { - const { unsignedEvent, type } = await buildEvent(draft); + const { unsignedEvent, type, videoId } = await buildEvent(draft); const validationResult = validateEvent(unsignedEvent); if (validationResult !== true) { @@ -78,7 +79,7 @@ export default function Draft() { if (unsignedEvent) { const published = await unsignedEvent.publish(); - const saved = await handlePostResource(unsignedEvent); + const saved = await handlePostResource(unsignedEvent, videoId); // if successful, delete the draft, redirect to profile if (published && saved) { axios.delete(`/api/drafts/${draft.id}`) @@ -104,7 +105,7 @@ export default function Draft() { } }; - const handlePostResource = async (resource) => { + const handlePostResource = async (resource, videoId) => { console.log('resourceeeeee:', resource.tags); const dTag = resource.tags.find(tag => tag[0] === 'd')[1]; let price @@ -133,6 +134,7 @@ export default function Draft() { userId: userResponse.data.id, price: Number(price), noteId: nAddress, + videoId: videoId ? videoId : null }; const response = await axios.post(`/api/resources`, payload); @@ -167,6 +169,7 @@ export default function Draft() { const event = new NDKEvent(ndk); let type; let encryptedContent; + let videoId; console.log('Draft:', draft); console.log('NewDTag:', NewDTag); @@ -203,8 +206,10 @@ export default function Draft() { if (draft?.content.includes('.mp4') || draft?.content.includes('.mov') || draft?.content.includes('.avi') || draft?.content.includes('.wmv') || draft?.content.includes('.flv') || draft?.content.includes('.webm')) { // todo update this for dev and prod + const extractedVideoId = draft.content.split('?videoKey=')[1].split('"')[0]; + videoId = extractedVideoId; const baseUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000" - const videoEmbed = `
`; + const videoEmbed = `
`; if (draft?.price) { const encryptedVideoUrl = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, videoEmbed); draft.content = encryptedVideoUrl; @@ -234,7 +239,7 @@ export default function Draft() { return null; } - return { unsignedEvent: event, type }; + return { unsignedEvent: event, type, videoId }; }; return ( @@ -251,10 +256,13 @@ export default function Draft() { ) })}
-

{draft?.title}

-

{draft?.summary}

+

{draft?.title}

+

{draft?.summary}

+ {draft?.price && ( +

Price: {draft.price}

+ )} {draft?.additionalLinks && draft.additionalLinks.length > 0 && ( -
+

External links:

    {draft.additionalLinks.map((link, index) => (