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