diff --git a/src/components/content/courses/DraftCourseDetails.js b/src/components/content/courses/DraftCourseDetails.js index 11d45f5..b851b11 100644 --- a/src/components/content/courses/DraftCourseDetails.js +++ b/src/components/content/courses/DraftCourseDetails.js @@ -16,6 +16,7 @@ import { useToast } from '@/hooks/useToast'; import { formatDateTime } from '@/utils/time'; import { validateEvent } from '@/utils/nostr'; import appConfig from "@/config/appConfig"; +import { useEncryptContent } from '@/hooks/encryption/useEncryptContent'; import 'primeicons/primeicons.css'; const MDDisplay = dynamic( @@ -30,6 +31,8 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons }) const [user, setUser] = useState(null); const [processedLessons, setProcessedLessons] = useState([]); const hasRunEffect = useRef(false); + const { encryptContent, isLoading: encryptLoading, error: encryptError } = useEncryptContent(); + const { showToast } = useToast(); const { returnImageProxy } = useImageProxy(); @@ -254,8 +257,7 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons }) switch (draft?.type) { case 'document': if (draft?.price) { - // encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY - encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, draft.content); + encryptedContent = await encryptContent(draft.content); } event.kind = draft?.price ? 30402 : 30023; // Determine kind based on if price is present @@ -277,8 +279,7 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons }) break; case 'video': if (draft?.price) { - // encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY - encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, draft.content); + encryptedContent = await encryptContent(draft.content); } event.kind = draft?.price ? 30402 : 30023; diff --git a/src/components/content/courses/VideoLesson.js b/src/components/content/courses/VideoLesson.js index f9f3858..ae25645 100644 --- a/src/components/content/courses/VideoLesson.js +++ b/src/components/content/courses/VideoLesson.js @@ -47,18 +47,10 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid }) => { if (isPaid && decryptionPerformed) { return ( <> -
- {/* Add your video player component here */} - -
); - } - if (isPaid && !decryptionPerformed) { + } else if (isPaid && !decryptionPerformed) { return (
{

); - } - if (lesson?.content) { + } else if (lesson?.content) { return ; } return null; diff --git a/src/components/forms/DocumentForm.js b/src/components/forms/DocumentForm.js index c70e739..b295d22 100644 --- a/src/components/forms/DocumentForm.js +++ b/src/components/forms/DocumentForm.js @@ -11,6 +11,8 @@ import { useToast } from "@/hooks/useToast"; import { useNDKContext } from "@/context/NDKContext"; import { NDKEvent } from "@nostr-dev-kit/ndk"; import dynamic from 'next/dynamic'; +import { useEncryptContent } from '@/hooks/encryption/useEncryptContent'; + const MDEditor = dynamic( () => import("@uiw/react-md-editor"), { @@ -32,6 +34,7 @@ const DocumentForm = ({ draft = null, isPublished = false }) => { const [content, setContent] = useState(draft?.content || ''); const [user, setUser] = useState(null); const [additionalLinks, setAdditionalLinks] = useState(draft?.additionalLinks || ['']); + const { encryptContent, isLoading: encryptLoading, error: encryptError } = useEncryptContent(); const { data: session, status } = useSession(); const { showToast } = useToast(); @@ -72,8 +75,7 @@ const DocumentForm = ({ draft = null, isPublished = false }) => { let encryptedContent; if (draft?.price) { - // encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY - encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, draft.content); + encryptedContent = await encryptContent(draft.content); } event.kind = draft?.price ? 30402 : 30023; // Determine kind based on if price is present diff --git a/src/components/forms/course/embedded/EmbeddedDocumentForm.js b/src/components/forms/course/embedded/EmbeddedDocumentForm.js index a0f59ed..68aafb1 100644 --- a/src/components/forms/course/embedded/EmbeddedDocumentForm.js +++ b/src/components/forms/course/embedded/EmbeddedDocumentForm.js @@ -9,6 +9,8 @@ import { useToast } from "@/hooks/useToast"; import { useNDKContext } from "@/context/NDKContext"; import { NDKEvent } from "@nostr-dev-kit/ndk"; import dynamic from 'next/dynamic'; +import { useEncryptContent } from '@/hooks/encryption/useEncryptContent'; + const MDEditor = dynamic( () => import("@uiw/react-md-editor"), { @@ -29,7 +31,7 @@ const EmbeddedDocumentForm = ({ draft = null, isPublished = false, onSave, isPai const [content, setContent] = useState(draft?.content || ''); const [user, setUser] = useState(null); const [additionalLinks, setAdditionalLinks] = useState(draft?.additionalLinks || ['']); - + const { encryptContent, isLoading: encryptLoading, error: encryptError } = useEncryptContent(); const { data: session, status } = useSession(); const { showToast } = useToast(); const { ndk, addSigner } = useNDKContext(); @@ -69,8 +71,7 @@ const EmbeddedDocumentForm = ({ draft = null, isPublished = false, onSave, isPai let encryptedContent; if (draft?.price) { - // encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY - encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, draft.content); + encryptedContent = await encryptContent(draft.content); } event.kind = draft?.price ? 30402 : 30023; // Determine kind based on if price is present diff --git a/src/hooks/encryption/useDecryptContent.js b/src/hooks/encryption/useDecryptContent.js new file mode 100644 index 0000000..f6fc541 --- /dev/null +++ b/src/hooks/encryption/useDecryptContent.js @@ -0,0 +1,31 @@ +import { useState } from 'react'; +import axios from 'axios'; + +export const useDecryptContent = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const decryptContent = async (encryptedContent) => { + setIsLoading(true); + setError(null); + + try { + const response = await axios.post('/api/decrypt', { encryptedContent }); + console.log('response', response); + + if (response.status !== 200) { + throw new Error('Failed to decrypt content'); + } + + const decryptedContent = response.data.decryptedContent; + setIsLoading(false); + return decryptedContent; + } catch (err) { + setError(err.message); + setIsLoading(false); + return null; + } + }; + + return { decryptContent, isLoading, error }; +}; \ No newline at end of file diff --git a/src/hooks/encryption/useEncryptContent.js b/src/hooks/encryption/useEncryptContent.js new file mode 100644 index 0000000..4122de5 --- /dev/null +++ b/src/hooks/encryption/useEncryptContent.js @@ -0,0 +1,30 @@ +import { useState } from 'react'; +import axios from 'axios'; + +export const useEncryptContent = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const encryptContent = async (content) => { + setIsLoading(true); + setError(null); + + try { + const response = await axios.post('/api/encrypt', { content }); + + if (response.status !== 200) { + throw new Error('Failed to encrypt content'); + } + + const encryptedContent = response.data.encryptedContent; + setIsLoading(false); + return encryptedContent; + } catch (err) { + setError(err.message); + setIsLoading(false); + return null; + } + } + + return { encryptContent, isLoading, error }; +}; \ No newline at end of file diff --git a/src/pages/api/decrypt.js b/src/pages/api/decrypt.js new file mode 100644 index 0000000..bd85185 --- /dev/null +++ b/src/pages/api/decrypt.js @@ -0,0 +1,28 @@ +import { nip04 } from 'nostr-tools'; + +export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method Not Allowed' }); + } + + const { encryptedContent } = req.body; + + if (!encryptedContent) { + return res.status(400).json({ error: 'Encrypted content is required' }); + } + + const APP_PRIV_KEY = process.env.APP_PRIV_KEY; + const APP_PUBLIC_KEY = process.env.APP_PUBLIC_KEY; + + if (!APP_PRIV_KEY || !APP_PUBLIC_KEY) { + return res.status(500).json({ error: 'Server configuration error' }); + } + + try { + const decryptedContent = await nip04.decrypt(APP_PRIV_KEY, APP_PUBLIC_KEY, encryptedContent); + res.status(200).json({ decryptedContent }); + } catch (error) { + console.error('Decryption error:', error); + res.status(500).json({ error: 'Failed to decrypt content' }); + } +} \ No newline at end of file diff --git a/src/pages/api/encrypt.js b/src/pages/api/encrypt.js new file mode 100644 index 0000000..5645062 --- /dev/null +++ b/src/pages/api/encrypt.js @@ -0,0 +1,28 @@ +import { nip04 } from 'nostr-tools'; + +export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method Not Allowed' }); + } + + const { content } = req.body; + + if (!content) { + return res.status(400).json({ error: 'Content is required' }); + } + + const APP_PRIV_KEY = process.env.APP_PRIV_KEY; + const APP_PUBLIC_KEY = process.env.APP_PUBLIC_KEY; + + if (!APP_PRIV_KEY || !APP_PUBLIC_KEY) { + return res.status(500).json({ error: 'Server configuration error' }); + } + + try { + const encryptedContent = await nip04.encrypt(APP_PRIV_KEY, APP_PUBLIC_KEY, content); + res.status(200).json({ encryptedContent }); + } catch (error) { + console.error('Encryption error:', error); + res.status(500).json({ error: 'Failed to encrypt content' }); + } +} \ No newline at end of file diff --git a/src/pages/course/[slug]/index.js b/src/pages/course/[slug]/index.js index d881cbd..7a9efc6 100644 --- a/src/pages/course/[slug]/index.js +++ b/src/pages/course/[slug]/index.js @@ -10,6 +10,7 @@ import { useSession } from 'next-auth/react'; import { nip04, nip19 } from 'nostr-tools'; import { ProgressSpinner } from 'primereact/progressspinner'; import { Accordion, AccordionTab } from 'primereact/accordion'; +import { useDecryptContent } from "@/hooks/encryption/useDecryptContent"; import dynamic from 'next/dynamic'; const MDDisplay = dynamic(() => import("@uiw/react-markdown-preview"), { ssr: false }); @@ -70,7 +71,14 @@ const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => { if (event) { const author = await fetchAuthor(event.pubkey); const parsedLesson = { ...parseEvent(event), author }; - setLessons(prev => [...prev, parsedLesson]); + setLessons(prev => { + // Check if the lesson already exists in the array + const exists = prev.some(lesson => lesson.id === parsedLesson.id); + if (!exists) { + return [...prev, parsedLesson]; + } + return prev; + }); } } catch (error) { console.error('Error fetching event:', error); @@ -78,11 +86,10 @@ const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => { }; lessonIds.forEach(lessonId => fetchLesson(lessonId)); } - }, [lessonIds, ndk, fetchAuthor]); + }, [lessonIds, ndk, fetchAuthor, pubkey]); useEffect(() => { - const uniqueLessonSet = new Set(lessons.map(JSON.stringify)); - const newUniqueLessons = Array.from(uniqueLessonSet).map(JSON.parse); + const newUniqueLessons = Array.from(new Map(lessons.map(lesson => [lesson.id, lesson])).values()); setUniqueLessons(newUniqueLessons); }, [lessons]); @@ -96,11 +103,10 @@ const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => { const useDecryption = (session, paidCourse, course, lessons, setLessons) => { const [decryptionPerformed, setDecryptionPerformed] = useState(false); const [loading, setLoading] = useState(true); - const privkey = process.env.NEXT_PUBLIC_APP_PRIV_KEY; - const pubkey = process.env.NEXT_PUBLIC_APP_PUBLIC_KEY; + const { decryptContent } = useDecryptContent(); useEffect(() => { - const decryptContent = async () => { + const decrypt = async () => { if (session?.user && paidCourse && !decryptionPerformed) { setLoading(true); const canAccess = @@ -111,7 +117,7 @@ const useDecryption = (session, paidCourse, course, lessons, setLessons) => { if (canAccess && lessons.length > 0) { try { const decryptedLessons = await Promise.all(lessons.map(async (lesson) => { - const decryptedContent = await nip04.decrypt(privkey, pubkey, lesson.content); + const decryptedContent = await decryptContent(lesson.content); return { ...lesson, content: decryptedContent }; })); setLessons(decryptedLessons); @@ -124,8 +130,8 @@ const useDecryption = (session, paidCourse, course, lessons, setLessons) => { } setLoading(false); } - decryptContent(); - }, [session, paidCourse, course, lessons, privkey, pubkey, decryptionPerformed, setLessons]); + decrypt(); + }, [session, paidCourse, course, lessons, decryptionPerformed, setLessons]); return { decryptionPerformed, loading }; }; diff --git a/src/pages/details/[slug]/index.js b/src/pages/details/[slug]/index.js index e4cba80..dd27a20 100644 --- a/src/pages/details/[slug]/index.js +++ b/src/pages/details/[slug]/index.js @@ -10,11 +10,10 @@ import VideoDetails from '@/components/content/videos/VideoDetails'; import DocumentDetails from '@/components/content/documents/DocumentDetails'; import { ProgressSpinner } from 'primereact/progressspinner'; import appConfig from "@/config/appConfig"; +import { useDecryptContent } from '@/hooks/encryption/useDecryptContent'; +import { useEncryptContent } from '@/hooks/encryption/useEncryptContent'; import 'primeicons/primeicons.css'; -const privkey = process.env.NEXT_PUBLIC_APP_PRIV_KEY; -const pubkey = process.env.NEXT_PUBLIC_APP_PUBLIC_KEY; - export default function Details() { const [event, setEvent] = useState(null); const [processedEvent, setProcessedEvent] = useState({}); @@ -29,6 +28,8 @@ export default function Details() { const { ndk, addSigner } = useNDKContext(); const { data: session, update } = useSession(); const [user, setUser] = useState(null); + const { decryptContent } = useDecryptContent(); + const { encryptContent } = useEncryptContent(); const { showToast } = useToast(); const router = useRouter(); @@ -46,23 +47,23 @@ export default function Details() { }, [processedEvent]); useEffect(() => { - const decryptContent = async () => { + const decrypt = async () => { if (paidResource && processedEvent.content) { // Check if user is subscribed first if (user?.role?.subscribed) { - const decryptedContent = await nip04.decrypt(privkey, pubkey, processedEvent.content); + const decryptedContent = await decryptContent(processedEvent.content); setDecryptedContent(decryptedContent); } // If not subscribed, check if they have purchased else if (user?.purchased?.some(purchase => purchase.resourceId === processedEvent.d)) { - const decryptedContent = await nip04.decrypt(privkey, pubkey, processedEvent.content); + const decryptedContent = await decryptContent(processedEvent.content); setDecryptedContent(decryptedContent); } // If neither subscribed nor purchased, decryptedContent remains null } }; - decryptContent(); + decrypt(); }, [user, paidResource, processedEvent]); useEffect(() => { @@ -99,7 +100,7 @@ export default function Details() { if (user && user.pubkey === event.pubkey) { setAuthorView(true); if (event.kind === 30402) { - const decryptedContent = await nip04.decrypt(privkey, pubkey, event.content); + const decryptedContent = await decryptContent(event.content); setDecryptedContent(decryptedContent); } } diff --git a/src/pages/draft/[slug]/index.js b/src/pages/draft/[slug]/index.js index 53d9318..e510e9b 100644 --- a/src/pages/draft/[slug]/index.js +++ b/src/pages/draft/[slug]/index.js @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import axios from 'axios'; import { useRouter } from 'next/router'; import { hexToNpub } from '@/utils/nostr'; -import { nip19, nip04 } from 'nostr-tools'; +import { nip19 } from 'nostr-tools'; import { v4 as uuidv4 } from 'uuid'; import { useSession } from 'next-auth/react'; import { useImageProxy } from '@/hooks/useImageProxy'; @@ -19,6 +19,7 @@ import dynamic from 'next/dynamic'; import { validateEvent } from '@/utils/nostr'; import appConfig from "@/config/appConfig"; import { useIsAdmin } from "@/hooks/useIsAdmin"; +import { useEncryptContent } from '@/hooks/encryption/useEncryptContent'; const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), @@ -33,12 +34,12 @@ export default function Draft() { const { data: session, status } = useSession(); const [user, setUser] = useState(null); const [nAddress, setNAddress] = useState(null); - const [videoId, setVideoId] = useState(null); const { width, height } = useResponsiveImageDimensions(); const router = useRouter(); const { showToast } = useToast(); const { ndk, addSigner } = useNDKContext(); const { isAdmin, isLoading } = useIsAdmin(); + const { encryptContent } = useEncryptContent(); useEffect(() => { if (isLoading) return; @@ -190,8 +191,7 @@ export default function Draft() { switch (draft?.type) { case 'document': if (draft?.price) { - // encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY - encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, draft.content); + encryptedContent = await encryptContent(draft.content); } event.kind = draft?.price ? 30402 : 30023; // Determine kind based on if price is present @@ -213,8 +213,7 @@ export default function Draft() { break; case 'video': if (draft?.price) { - // encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY - encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, draft.content); + encryptedContent = await encryptContent(draft.content); } 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')) { @@ -224,7 +223,7 @@ export default function Draft() { const baseUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000" 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); + const encryptedVideoUrl = await encryptContent(videoEmbed); draft.content = encryptedVideoUrl; } else { draft.content = videoEmbed;