mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-04-19 19:01:19 +00:00
Add encrypt and decrypt endpoint and replaced encryption/decryption in app to use these endpoints
This commit is contained in:
parent
b800ab3b88
commit
09e0ba026a
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
31
src/hooks/encryption/useDecryptContent.js
Normal file
31
src/hooks/encryption/useDecryptContent.js
Normal 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 };
|
||||
};
|
30
src/hooks/encryption/useEncryptContent.js
Normal file
30
src/hooks/encryption/useEncryptContent.js
Normal 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
28
src/pages/api/decrypt.js
Normal 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
28
src/pages/api/encrypt.js
Normal 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' });
|
||||
}
|
||||
}
|
@ -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 };
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user