mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-04-19 19:01:19 +00:00
Checking for admin, subscribed, or purchased videoId in get video endpoint
This commit is contained in:
parent
fd7b7567fe
commit
14b62b3f4a
@ -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;
|
@ -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;
|
||||
|
@ -138,6 +138,7 @@ model Resource {
|
||||
price Int @default(0)
|
||||
purchases Purchase[]
|
||||
noteId String? @unique
|
||||
videoId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
@ -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{' '}
|
||||
|
@ -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;
|
||||
|
@ -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" })
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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) => (
|
||||
|
Loading…
x
Reference in New Issue
Block a user