Checking for admin, subscribed, or purchased videoId in get video endpoint

This commit is contained in:
austinkelsay 2024-09-09 17:35:00 -05:00
parent fd7b7567fe
commit 14b62b3f4a
8 changed files with 182 additions and 119 deletions

View File

@ -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;

View File

@ -1,7 +1,12 @@
-- CreateTable -- CreateTable
CREATE TABLE "User" ( CREATE TABLE "User" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"pubkey" TEXT NOT NULL, "pubkey" TEXT,
"privkey" TEXT,
"name" TEXT,
"email" TEXT,
"emailVerified" TIMESTAMP(3),
"image" TEXT,
"username" TEXT, "username" TEXT,
"avatar" TEXT, "avatar" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
@ -10,6 +15,43 @@ CREATE TABLE "User" (
CONSTRAINT "User_pkey" PRIMARY KEY ("id") 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 -- CreateTable
CREATE TABLE "Role" ( CREATE TABLE "Role" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
@ -81,6 +123,7 @@ CREATE TABLE "Resource" (
"userId" TEXT NOT NULL, "userId" TEXT NOT NULL,
"price" INTEGER NOT NULL DEFAULT 0, "price" INTEGER NOT NULL DEFAULT 0,
"noteId" TEXT, "noteId" TEXT,
"videoId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
@ -123,9 +166,24 @@ CREATE TABLE "CourseDraft" (
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "User_pubkey_key" ON "User"("pubkey"); CREATE UNIQUE INDEX "User_pubkey_key" ON "User"("pubkey");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); 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 -- CreateIndex
CREATE UNIQUE INDEX "Role_userId_key" ON "Role"("userId"); CREATE UNIQUE INDEX "Role_userId_key" ON "Role"("userId");
@ -135,6 +193,12 @@ CREATE UNIQUE INDEX "Course_noteId_key" ON "Course"("noteId");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Resource_noteId_key" ON "Resource"("noteId"); 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 -- AddForeignKey
ALTER TABLE "Role" ADD CONSTRAINT "Role_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "Role" ADD CONSTRAINT "Role_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -138,6 +138,7 @@ model Resource {
price Int @default(0) price Int @default(0)
purchases Purchase[] purchases Purchase[]
noteId String? @unique noteId String? @unique
videoId String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }

View File

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import useWindowWidth from '@/hooks/useWindowWidth';
import Image from 'next/image'; import Image from 'next/image';
const HeroBanner = () => { const HeroBanner = () => {
@ -6,6 +7,8 @@ const HeroBanner = () => {
const [currentOption, setCurrentOption] = useState(0); const [currentOption, setCurrentOption] = useState(0);
const [isFlipping, setIsFlipping] = useState(false); const [isFlipping, setIsFlipping] = useState(false);
const windowWidth = useWindowWidth();
const getColorClass = (option) => { const getColorClass = (option) => {
switch (option) { switch (option) {
case 'Bitcoin': return 'text-orange-400'; case 'Bitcoin': return 'text-orange-400';
@ -31,15 +34,17 @@ const HeroBanner = () => {
return ( return (
<div className="relative flex justify-center items-center"> <div className="relative flex justify-center items-center">
<Image {windowWidth && (
src="/images/plebdevs-banner.png" <Image
alt="Banner" src="/images/plebdevs-banner.png"
width={1920} alt="Banner"
height={1080} width={windowWidth <= 1920 ? 1920 : windowWidth}
quality={100} height={1080}
className='opacity-70' quality={100}
/> className='opacity-70'
<div className="w-[75vw] sm:w-[65vw] md:w-[45vw] lg:w-[45vw] xl:w-[45vw] absolute text-center text-white text-xl h-full flex flex-col justify-evenly"> />
)}
<div className="w-[75vw] sm:w-[65vw] md:w-[45vw] lg:w-[45vw] xl:w-[37vw] absolute text-center text-white text-xl h-full flex flex-col justify-evenly">
<p className='text-2xl md:text-4xl lg:text-5xl xl:text-5xl'>Learn how to code</p> <p className='text-2xl md:text-4xl lg:text-5xl xl:text-5xl'>Learn how to code</p>
<p className='text-2xl md:text-4xl lg:text-5xl xl:text-5xl'> <p className='text-2xl md:text-4xl lg:text-5xl xl:text-5xl'>
Build{' '} Build{' '}

View File

@ -121,7 +121,6 @@ export const authOptions = {
return token; return token;
}, },
async session({ session, token }) { async session({ session, token }) {
console.log('SESSION', session);
// Add user from token to session // Add user from token to session
session.user = token.user; session.user = token.user;
session.jwt = token; session.jwt = token;

View File

@ -12,20 +12,36 @@ const s3Client = new S3Client({
}, },
}) })
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY
export default async function handler(req, res) { 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 { 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({ const command = new GetObjectCommand({
Bucket: "plebdevs-bucket", Bucket: "plebdevs-bucket",
Key: videoKey, Key: videoKey,
@ -37,7 +53,7 @@ export default async function handler(req, res) {
res.redirect(signedUrl) res.redirect(signedUrl)
} catch (error) { } catch (error) {
console.error("Error generating signed URL:", error) console.error("Error in get-video-url handler:", error)
res.status(500).json({ error: "Failed to generate video URL" }) res.status(500).json({ error: "Internal Server Error" })
} }
} }

View File

@ -10,6 +10,7 @@ import ZapThreadsWrapper from '@/components/ZapThreadsWrapper';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { useNDKContext } from '@/context/NDKContext'; import { useNDKContext } from '@/context/NDKContext';
import ResourceDetails from '@/components/content/resources/ResourceDetails'; import ResourceDetails from '@/components/content/resources/ResourceDetails';
import {ProgressSpinner} from 'primereact/progressspinner';
import 'primeicons/primeicons.css'; import 'primeicons/primeicons.css';
const MDDisplay = dynamic( const MDDisplay = dynamic(
@ -30,6 +31,8 @@ export default function Details() {
const [paidResource, setPaidResource] = useState(false); const [paidResource, setPaidResource] = useState(false);
const [decryptedContent, setDecryptedContent] = useState(null); const [decryptedContent, setDecryptedContent] = useState(null);
const [authorView, setAuthorView] = useState(false); const [authorView, setAuthorView] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const {ndk, addSigner} = useNDKContext(); const {ndk, addSigner} = useNDKContext();
const { data: session, update } = useSession(); const { data: session, update } = useSession();
@ -75,7 +78,9 @@ export default function Details() {
if (router.isReady) { if (router.isReady) {
const { slug } = router.query; const { slug } = router.query;
const fetchEvent = async (slug) => { const fetchEvent = async (slug, retryCount = 0) => {
setLoading(true);
setError(null);
try { try {
await ndk.connect(); await ndk.connect();
@ -88,13 +93,33 @@ export default function Details() {
if (event) { if (event) {
setEvent(event); setEvent(event);
if (user && user.pubkey === event.pubkey) { if (user && user.pubkey === event.pubkey) {
const decryptedContent = await nip04.decrypt(privkey, pubkey, event.content);
setDecryptedContent(decryptedContent);
setAuthorView(true); 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) { } catch (error) {
console.error('Error fetching event:', 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) { if (ndk) {
fetchEvent(slug); fetchEvent(slug);
} }
@ -190,21 +215,35 @@ export default function Details() {
return null; return null;
} }
if (loading) {
return <div className="mx-auto">
<ProgressSpinner />
</div>;
}
if (error) {
return <div className="w-full mx-auto h-screen">
<div className="text-red-500 text-xl">{error}</div>
</div>;
}
return ( return (
<div className='w-full px-24 pt-12 mx-auto mt-4 max-tab:px-0 max-mob:px-0 max-tab:pt-2 max-mob:pt-2'> <div className='w-full px-24 pt-12 mx-auto mt-4 max-tab:px-0 max-mob:px-0 max-tab:pt-2 max-mob:pt-2'>
<ResourceDetails {processedEvent && (
processedEvent={processedEvent} <ResourceDetails
topics={processedEvent.topics} processedEvent={processedEvent}
title={processedEvent.title} topics={processedEvent.topics}
summary={processedEvent.summary} title={processedEvent.title}
image={processedEvent.image} summary={processedEvent.summary}
price={processedEvent.price} image={processedEvent.image}
author={author} price={processedEvent.price}
paidResource={paidResource} author={author}
decryptedContent={decryptedContent} paidResource={paidResource}
handlePaymentSuccess={handlePaymentSuccess} decryptedContent={decryptedContent}
handlePaymentError={handlePaymentError} handlePaymentSuccess={handlePaymentSuccess}
/> handlePaymentError={handlePaymentError}
/>
)}
{authorView && ( {authorView && (
<div className='w-[75vw] mx-auto flex flex-row justify-end mt-12'> <div className='w-[75vw] mx-auto flex flex-row justify-end mt-12'>
<div className='w-fit flex flex-row justify-between'> <div className='w-fit flex flex-row justify-between'>
@ -224,7 +263,9 @@ export default function Details() {
</div> </div>
)} )}
<div className='w-[75vw] mx-auto mt-12 p-12 border-t-2 border-gray-300 max-tab:p-0 max-mob:p-0 max-tab:max-w-[100vw] max-mob:max-w-[100vw]'> <div className='w-[75vw] mx-auto mt-12 p-12 border-t-2 border-gray-300 max-tab:p-0 max-mob:p-0 max-tab:max-w-[100vw] max-mob:max-w-[100vw]'>
{renderContent()} {
processedEvent && processedEvent.content && renderContent()
}
</div> </div>
</div> </div>
); );

View File

@ -30,6 +30,7 @@ export default function Draft() {
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [videoId, setVideoId] = useState(null);
const { width, height } = useResponsiveImageDimensions(); const { width, height } = useResponsiveImageDimensions();
const router = useRouter(); const router = useRouter();
const { showToast } = useToast(); const { showToast } = useToast();
@ -63,7 +64,7 @@ export default function Draft() {
} }
if (draft) { if (draft) {
const { unsignedEvent, type } = await buildEvent(draft); const { unsignedEvent, type, videoId } = await buildEvent(draft);
const validationResult = validateEvent(unsignedEvent); const validationResult = validateEvent(unsignedEvent);
if (validationResult !== true) { if (validationResult !== true) {
@ -78,7 +79,7 @@ export default function Draft() {
if (unsignedEvent) { if (unsignedEvent) {
const published = await unsignedEvent.publish(); 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 successful, delete the draft, redirect to profile
if (published && saved) { if (published && saved) {
axios.delete(`/api/drafts/${draft.id}`) 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); console.log('resourceeeeee:', resource.tags);
const dTag = resource.tags.find(tag => tag[0] === 'd')[1]; const dTag = resource.tags.find(tag => tag[0] === 'd')[1];
let price let price
@ -133,6 +134,7 @@ export default function Draft() {
userId: userResponse.data.id, userId: userResponse.data.id,
price: Number(price), price: Number(price),
noteId: nAddress, noteId: nAddress,
videoId: videoId ? videoId : null
}; };
const response = await axios.post(`/api/resources`, payload); const response = await axios.post(`/api/resources`, payload);
@ -167,6 +169,7 @@ export default function Draft() {
const event = new NDKEvent(ndk); const event = new NDKEvent(ndk);
let type; let type;
let encryptedContent; let encryptedContent;
let videoId;
console.log('Draft:', draft); console.log('Draft:', draft);
console.log('NewDTag:', NewDTag); 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')) { 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 // 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 baseUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"
const videoEmbed = `<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><video src="${baseUrl}/api/get-video-url?videoKey=${encodeURIComponent(draft.content)}" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" controls></video></div>`; const videoEmbed = `<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><video src="${baseUrl}/api/get-video-url?videoKey=${encodeURIComponent(extractedVideoId)}" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" controls></video></div>`;
if (draft?.price) { if (draft?.price) {
const encryptedVideoUrl = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, videoEmbed); const encryptedVideoUrl = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, videoEmbed);
draft.content = encryptedVideoUrl; draft.content = encryptedVideoUrl;
@ -234,7 +239,7 @@ export default function Draft() {
return null; return null;
} }
return { unsignedEvent: event, type }; return { unsignedEvent: event, type, videoId };
}; };
return ( return (
@ -251,10 +256,13 @@ export default function Draft() {
) )
})} })}
</div> </div>
<h1 className='text-4xl mt-6'>{draft?.title}</h1> <h1 className='text-4xl mt-4'>{draft?.title}</h1>
<p className='text-xl mt-6'>{draft?.summary}</p> <p className='text-xl mt-4'>{draft?.summary}</p>
{draft?.price && (
<p className='text-lg mt-4'>Price: {draft.price}</p>
)}
{draft?.additionalLinks && draft.additionalLinks.length > 0 && ( {draft?.additionalLinks && draft.additionalLinks.length > 0 && (
<div className='mt-6'> <div className='mt-4'>
<h3 className='text-lg font-semibold mb-2'>External links:</h3> <h3 className='text-lg font-semibold mb-2'>External links:</h3>
<ul className='list-disc list-inside'> <ul className='list-disc list-inside'>
{draft.additionalLinks.map((link, index) => ( {draft.additionalLinks.map((link, index) => (