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

View File

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

View File

@ -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 (
<div className="relative flex justify-center items-center">
<Image
src="/images/plebdevs-banner.png"
alt="Banner"
width={1920}
height={1080}
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">
{windowWidth && (
<Image
src="/images/plebdevs-banner.png"
alt="Banner"
width={windowWidth <= 1920 ? 1920 : windowWidth}
height={1080}
quality={100}
className='opacity-70'
/>
)}
<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'>
Build{' '}

View File

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

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) {
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" })
}
}

View File

@ -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 <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 (
<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}
topics={processedEvent.topics}
title={processedEvent.title}
summary={processedEvent.summary}
image={processedEvent.image}
price={processedEvent.price}
author={author}
paidResource={paidResource}
decryptedContent={decryptedContent}
handlePaymentSuccess={handlePaymentSuccess}
handlePaymentError={handlePaymentError}
/>
{processedEvent && (
<ResourceDetails
processedEvent={processedEvent}
topics={processedEvent.topics}
title={processedEvent.title}
summary={processedEvent.summary}
image={processedEvent.image}
price={processedEvent.price}
author={author}
paidResource={paidResource}
decryptedContent={decryptedContent}
handlePaymentSuccess={handlePaymentSuccess}
handlePaymentError={handlePaymentError}
/>
)}
{authorView && (
<div className='w-[75vw] mx-auto flex flex-row justify-end mt-12'>
<div className='w-fit flex flex-row justify-between'>
@ -224,7 +263,9 @@ export default function Details() {
</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]'>
{renderContent()}
{
processedEvent && processedEvent.content && renderContent()
}
</div>
</div>
);

View File

@ -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 = `<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) {
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() {
)
})}
</div>
<h1 className='text-4xl mt-6'>{draft?.title}</h1>
<p className='text-xl mt-6'>{draft?.summary}</p>
<h1 className='text-4xl mt-4'>{draft?.title}</h1>
<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 && (
<div className='mt-6'>
<div className='mt-4'>
<h3 className='text-lg font-semibold mb-2'>External links:</h3>
<ul className='list-disc list-inside'>
{draft.additionalLinks.map((link, index) => (