Add encrypt and decrypt endpoint and replaced encryption/decryption in app to use these endpoints

This commit is contained in:
austinkelsay 2024-09-17 16:00:00 -05:00
parent b800ab3b88
commit 09e0ba026a
11 changed files with 163 additions and 45 deletions

View File

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

View File

@ -47,18 +47,10 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
if (isPaid && decryptionPerformed) {
return (
<>
<div className="w-full aspect-video rounded-lg mb-4">
{/* Add your video player component here */}
<video controls className="w-full h-full">
<source src={lesson.videoUrl} type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
<MDDisplay className='p-4 rounded-lg w-full' source={lesson.content} />
</>
);
}
if (isPaid && !decryptionPerformed) {
} else if (isPaid && !decryptionPerformed) {
return (
<div className="w-full aspect-video rounded-lg flex flex-col items-center justify-center relative overflow-hidden">
<div
@ -78,8 +70,7 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
</p>
</div>
);
}
if (lesson?.content) {
} else if (lesson?.content) {
return <MDDisplay className='p-4 rounded-lg w-full' source={lesson.content} />;
}
return null;

View File

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

View File

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

View File

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

View File

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

28
src/pages/api/decrypt.js Normal file
View File

@ -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' });
}
}

28
src/pages/api/encrypt.js Normal file
View File

@ -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' });
}
}

View File

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

View File

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

View File

@ -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 = `<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);
const encryptedVideoUrl = await encryptContent(videoEmbed);
draft.content = encryptedVideoUrl;
} else {
draft.content = videoEmbed;