Add aothor pubkey as env var, improve login/signup flow

This commit is contained in:
austinkelsay 2024-07-20 10:51:16 -05:00
parent 43d0c6d321
commit 6b3991d332
10 changed files with 284 additions and 129 deletions

View File

@ -8,7 +8,7 @@ import { Dropdown } from "primereact/dropdown";
import { v4 as uuidv4, v4 } from 'uuid'; import { v4 as uuidv4, v4 } from 'uuid';
import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage"; import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage";
import { useNostr } from "@/hooks/useNostr"; import { useNostr } from "@/hooks/useNostr";
import {nip19} from "nostr-tools" import { nip19 } from "nostr-tools"
import { parseEvent } from "@/utils/nostr"; import { parseEvent } from "@/utils/nostr";
import ContentDropdownItem from "@/components/content/dropdowns/ContentDropdownItem"; import ContentDropdownItem from "@/components/content/dropdowns/ContentDropdownItem";
import 'primeicons/primeicons.css'; import 'primeicons/primeicons.css';
@ -119,12 +119,12 @@ const CourseForm = () => {
if (published) { if (published) {
// delete the draft // delete the draft
axios.delete(`/api/drafts/${lesson.id}`) axios.delete(`/api/drafts/${lesson.id}`)
.then((response) => { .then((response) => {
console.log('Draft deleted:', response); console.log('Draft deleted:', response);
}) })
.catch((error) => { .catch((error) => {
console.error('Error deleting draft:', error); console.error('Error deleting draft:', error);
}); });
} }
} }
} }
@ -178,17 +178,22 @@ const CourseForm = () => {
const signedCourseEvent = await window?.nostr?.signEvent(courseEvent); const signedCourseEvent = await window?.nostr?.signEvent(courseEvent);
console.log('signedCourseEvent:', signedCourseEvent); console.log('signedCourseEvent:', signedCourseEvent);
// Publish the course event using Nostr // Publish the course event using Nostr
await publish(signedCourseEvent); const published = await publish(signedCourseEvent);
// Reset the form fields after publishing the course if (published) {
setTitle('');
setSummary(''); // Reset the form fields after publishing the course
setChecked(false); setTitle('');
setPrice(0); setSummary('');
setCoverImage(''); setChecked(false);
setLessons([{ id: uuidv4(), title: 'Select a lesson' }]); setPrice(0);
setSelectedLessons([]); setCoverImage('');
setTopics(['']); setLessons([{ id: uuidv4(), title: 'Select a lesson' }]);
setSelectedLessons([]);
setTopics(['']);
} else {
// Handle error
}
} }
}; };

View File

@ -0,0 +1,19 @@
import prisma from "../prisma";
export const getAllContentIds = async () => {
const courseIds = await prisma.course.findMany({
select: {
id: true,
},
});
const resourceIds = await prisma.resource.findMany({
select: {
id: true,
},
});
const combinedIds = [...courseIds, ...resourceIds].map((item) => item.id);
return combinedIds;
};

View File

@ -1,28 +1,17 @@
// db/prisma.js const { PrismaClient } = require('@prisma/client');
// Import the PrismaClient class from the @prisma/client package.
import { PrismaClient } from '@prisma/client';
// Declare a variable to hold our Prisma client instance. // Declare a variable to hold our Prisma client instance.
let prisma; let prisma;
// Check if the application is running in a production environment. // Check if there is already an instance of PrismaClient attached to the global object.
if (process.env.NODE_ENV === 'production') { // If not, create a new instance of PrismaClient and attach it to the global object.
// In production, always create a new instance of PrismaClient. // This ensures that the same instance of PrismaClient is reused across multiple invocations.
prisma = new PrismaClient(); if (!global.prisma) {
} else { global.prisma = new PrismaClient();
// In development or other non-production environments...
// Check if there is already an instance of PrismaClient attached to the global object.
if (!global.prisma) {
// If not, create a new instance of PrismaClient...
global.prisma = new PrismaClient();
}
// ...and assign it to the prisma variable. This ensures that in development,
// the same instance of PrismaClient is reused across hot reloads and server restarts,
// which can help prevent the exhaustion of database connections during development.
prisma = global.prisma;
} }
// Assign the global PrismaClient instance to the prisma variable.
prisma = global.prisma;
// Export the prisma client instance, making it available for import in other parts of the application. // Export the prisma client instance, making it available for import in other parts of the application.
export default prisma; module.exports = prisma;

View File

@ -61,42 +61,46 @@ export const useLogin = () => {
const nostrLogin = useCallback(async () => { const nostrLogin = useCallback(async () => {
if (!window || !window.nostr) { if (!window || !window.nostr) {
showToast('error', 'Nostr Unavailable', 'Nostr is not available'); showToast('error', 'Nostr Unavailable', 'Nostr is not available');
return; return;
} }
const publicKey = await window.nostr.getPublicKey(); const publicKey = await window.nostr.getPublicKey();
if (!publicKey) { if (!publicKey) {
alert('Failed to obtain public key'); showToast('error', 'Public Key Error', 'Failed to obtain public key');
return; return;
} }
try { try {
const response = await axios.get(`/api/users/${publicKey}`); const response = await axios.get(`/api/users/${publicKey}`);
if (response.status !== 200) throw new Error('User not found'); let userData;
;
window.localStorage.setItem('user', JSON.stringify(response.data)); if (response.status === 204) {
router.push('/').then(() => window.location.reload());
} catch (error) {
// User not found, create a new user // User not found, create a new user
const kind0 = await fetchKind0(publicKey); const kind0 = await fetchKind0(publicKey);
const fields = await findKind0Fields(kind0);
const payload = { pubkey: publicKey, ...fields }; let fields = {};
if (kind0) {
try { fields = await findKind0Fields(kind0);
const createUserResponse = await axios.post(`/api/users`, payload);
if (createUserResponse.status === 201) {
window.localStorage.setItem('user', JSON.stringify(createUserResponse.data));
router.push('/').then(() => window.location.reload());
} else {
console.error('Error creating user:', createUserResponse);
}
} catch (createError) {
console.error('Error creating user:', createError);
showToast('error', 'Error Creating User', 'Failed to create user');
} }
const payload = { pubkey: publicKey, ...fields };
const createUserResponse = await axios.post(`/api/users`, payload);
if (createUserResponse.status !== 201) {
throw new Error('Failed to create user');
}
userData = createUserResponse.data;
} else {
userData = response.data;
}
window.localStorage.setItem('user', JSON.stringify(userData));
router.push('/').then(() => window.location.reload());
} catch (error) {
console.error('Error during login:', error);
showToast('error', 'Login Error', error.message || 'Failed to log in');
} }
}, [router, showToast, fetchKind0]); }, [router, showToast, fetchKind0]);
const anonymousLogin = useCallback(() => { const anonymousLogin = useCallback(() => {
try { try {

View File

@ -14,6 +14,8 @@ const defaultRelays = [
"wss://relay.primal.net/" "wss://relay.primal.net/"
]; ];
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
export function useNostr() { export function useNostr() {
const pool = useContext(NostrContext); const pool = useContext(NostrContext);
const subscriptionQueue = useRef([]); const subscriptionQueue = useRef([]);
@ -175,12 +177,12 @@ export function useNostr() {
aTagsAlt.push(`${event.kind}:${event.pubkey}:${event.d}`); aTagsAlt.push(`${event.kind}:${event.pubkey}:${event.d}`);
eTags.push(event.id); eTags.push(event.id);
}); });
// Create filters for batch querying // Create filters for batch querying
const filterA = { kinds: [9735], '#a': aTags }; const filterA = { kinds: [9735], '#a': aTags };
const filterE = { kinds: [9735], '#e': eTags }; const filterE = { kinds: [9735], '#e': eTags };
const filterAAlt = { kinds: [9735], '#a': aTagsAlt }; const filterAAlt = { kinds: [9735], '#a': aTagsAlt };
// Perform batch queries // Perform batch queries
// const [zapsA, zapsE] = await Promise.all([ // const [zapsA, zapsE] = await Promise.all([
// pool.querySync(defaultRelays, filterA), // pool.querySync(defaultRelays, filterA),
@ -212,7 +214,7 @@ export function useNostr() {
return []; return [];
} }
}; };
return new Promise((resolve) => { return new Promise((resolve) => {
querySyncQueue.current.push(async () => { querySyncQueue.current.push(async () => {
const zaps = await querySyncFn(); const zaps = await querySyncFn();
@ -222,32 +224,33 @@ export function useNostr() {
}); });
}, },
[pool, processQuerySyncQueue] [pool, processQuerySyncQueue]
); );
const fetchKind0 = useCallback( const fetchKind0 = useCallback(
async (publicKey) => { async (publicKey) => {
try { return new Promise((resolve) => {
const kind0 = await new Promise((resolve, reject) => { const timeout = setTimeout(() => {
subscribe( resolve(null); // Resolve with null if no event is received within the timeout
[{ authors: [publicKey], kinds: [0] }], }, 10000); // 10 seconds timeout
{
onevent: (event) => { subscribe(
resolve(JSON.parse(event.content)); [{ authors: [publicKey], kinds: [0] }],
}, {
onerror: (error) => { onevent: (event) => {
reject(error); clearTimeout(timeout);
}, resolve(JSON.parse(event.content));
} },
); onerror: (error) => {
}); clearTimeout(timeout);
return kind0; console.error('Error fetching kind 0:', error);
} catch (error) { resolve(null);
console.error('Failed to fetch kind 0 for event:', error); },
return []; }
} );
});
}, },
[subscribe] [subscribe]
); );
const zapEvent = useCallback( const zapEvent = useCallback(
async (event, amount, comment) => { async (event, amount, comment) => {
@ -334,7 +337,7 @@ export function useNostr() {
); );
const fetchResources = useCallback(async () => { const fetchResources = useCallback(async () => {
const filter = [{ kinds: [30023, 30402], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; const filter = [{ kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] }];
const hasRequiredTags = (tags) => { const hasRequiredTags = (tags) => {
const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs"); const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
// Check if 'resource' tag exists // Check if 'resource' tag exists
@ -374,12 +377,12 @@ export function useNostr() {
}, [subscribe]); }, [subscribe]);
const fetchWorkshops = useCallback(async () => { const fetchWorkshops = useCallback(async () => {
const filter = [{ kinds: [30023, 30402], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; const filter = [{ kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] }];
const hasRequiredTags = (tags) => { const hasRequiredTags = (tags) => {
const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs"); const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
const hasWorkshop = tags.some(([tag, value]) => tag === "t" && value === "workshop"); const hasWorkshop = tags.some(([tag, value]) => tag === "t" && value === "workshop");
return hasPlebDevs && hasWorkshop; return hasPlebDevs && hasWorkshop;
}; };
@ -414,7 +417,7 @@ export function useNostr() {
}, [subscribe]); }, [subscribe]);
const fetchCourses = useCallback(async () => { const fetchCourses = useCallback(async () => {
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }];
const hasRequiredTags = (tags) => { const hasRequiredTags = (tags) => {
const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs"); const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
@ -453,5 +456,94 @@ export function useNostr() {
}); });
}, [subscribe]); }, [subscribe]);
return { subscribe, publish, fetchSingleEvent, fetchZapsForEvent, fetchKind0, fetchResources, fetchWorkshops, fetchCourses, zapEvent, fetchZapsForEvents }; const publishResource = useCallback(
async (resourceEvent) => {
const published = await publish(resourceEvent);
if (published) {
const { id, kind, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(resourceEvent);
const user = window.localStorage.getItem('user');
const userId = JSON.parse(user).id;
const payload = {
};
if (payload && payload.user) {
try {
const response = await axios.post('/api/resources', payload);
if (response.status === 201) {
try {
const deleteResponse = await axios.delete(`/api/drafts/${resourceEvent.id}`);
if (deleteResponse.status === 204) {
return true;
}
} catch (error) {
console.error('Error deleting draft:', error);
return false;
}
}
} catch (error) {
console.error('Error creating resource:', error);
return false;
}
}
}
return false;
},
[publish]
);
const publishCourse = useCallback(
async (courseEvent) => {
const published = await publish(courseEvent);
if (published) {
const user = window.localStorage.getItem('user');
const pubkey = JSON.parse(user).pubkey;
const payload = {
title: courseEvent.title,
summary: courseEvent.summary,
type: 'course',
content: courseEvent.content,
image: courseEvent.image,
user: pubkey,
topics: [...courseEvent.topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'course']
};
if (payload && payload.user) {
try {
const response = await axios.post('/api/courses', payload);
if (response.status === 201) {
try {
const deleteResponse = await axios.delete(`/api/drafts/${courseEvent.id}`);
if (deleteResponse.status === 204) {
return true;
}
} catch (error) {
console.error('Error deleting draft:', error);
return false;
}
}
} catch (error) {
console.error('Error creating course:', error);
return false;
}
}
}
return false;
},
[publish]
);
return { subscribe, publish, fetchSingleEvent, fetchZapsForEvent, fetchKind0, fetchResources, fetchWorkshops, fetchCourses, zapEvent, fetchZapsForEvents, publishResource, publishCourse };
} }

View File

@ -13,6 +13,8 @@ const initialRelays = [
"wss://relay.primal.net/" "wss://relay.primal.net/"
]; ];
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
export const useNostr = () => { export const useNostr = () => {
const [relays, setRelays] = useState(initialRelays); const [relays, setRelays] = useState(initialRelays);
const [relayStatuses, setRelayStatuses] = useState({}); const [relayStatuses, setRelayStatuses] = useState({});
@ -159,7 +161,7 @@ export const useNostr = () => {
// Fetch resources, workshops, courses, and streams with appropriate filters and update functions // Fetch resources, workshops, courses, and streams with appropriate filters and update functions
const fetchResources = async () => { const fetchResources = async () => {
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }];
const hasRequiredTags = async (eventData) => { const hasRequiredTags = async (eventData) => {
const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs");
const hasResource = eventData.some(([tag, value]) => tag === "t" && value === "resource"); const hasResource = eventData.some(([tag, value]) => tag === "t" && value === "resource");
@ -181,7 +183,7 @@ export const useNostr = () => {
}; };
const fetchWorkshops = async () => { const fetchWorkshops = async () => {
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }];
const hasRequiredTags = async (eventData) => { const hasRequiredTags = async (eventData) => {
const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs");
const hasWorkshop = eventData.some(([tag, value]) => tag === "t" && value === "workshop"); const hasWorkshop = eventData.some(([tag, value]) => tag === "t" && value === "workshop");
@ -203,7 +205,7 @@ export const useNostr = () => {
}; };
const fetchCourses = async () => { const fetchCourses = async () => {
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }];
const hasRequiredTags = async (eventData) => { const hasRequiredTags = async (eventData) => {
const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs");
const hasCourse = eventData.some(([tag, value]) => tag === "t" && value === "course"); const hasCourse = eventData.some(([tag, value]) => tag === "t" && value === "course");
@ -226,7 +228,7 @@ export const useNostr = () => {
}; };
// const fetchStreams = () => { // const fetchStreams = () => {
// const filter = [{kinds: [30311], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"]}]; // const filter = [{kinds: [30311], authors: [AUTHOR_PUBKEY]}];
// const hasRequiredTags = (eventData) => eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); // const hasRequiredTags = (eventData) => eventData.some(([tag, value]) => tag === "t" && value === "plebdevs");
// fetchEvents(filter, 'streams', hasRequiredTags); // fetchEvents(filter, 'streams', hasRequiredTags);
// } // }

View File

@ -0,0 +1,16 @@
import { getAllContentIds } from '../../../db/models/genericModels';
export default async function handler(req, res) {
if (req.method === 'GET') {
try {
const ids = await getAllContentIds();
res.status(200).json(ids);
} catch (error) {
res.status(500).json({ error: error.message });
}
} else {
// Handle any other HTTP method
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}

View File

@ -2,7 +2,6 @@ import { getUserById, getUserByPubkey, updateUser, deleteUser } from "@/db/model
export default async function handler(req, res) { export default async function handler(req, res) {
const { slug } = req.query; const { slug } = req.query;
// Determine if slug is a pubkey or an ID // Determine if slug is a pubkey or an ID
const isPubkey = /^[0-9a-fA-F]{64}$/.test(slug); const isPubkey = /^[0-9a-fA-F]{64}$/.test(slug);
@ -15,44 +14,46 @@ export default async function handler(req, res) {
// Assume slug is an ID // Assume slug is an ID
const id = parseInt(slug); const id = parseInt(slug);
if (isNaN(id)) { if (isNaN(id)) {
return res.status(400).json({ error: "Invalid identifier" }); res.status(400).json({ error: "Invalid identifier" });
return;
} }
user = await getUserById(id); user = await getUserById(id);
} }
if (!user) { if (!user) {
return res.status(204).end(); res.status(204).end();
return;
} }
switch (req.method) { switch (req.method) {
case 'GET': case 'GET':
return res.status(200).json(user); res.status(200).json(user);
break;
case 'PUT': case 'PUT':
if (!isPubkey) { if (!isPubkey) {
// Update operation should be done with an ID, not a pubkey // Update operation should be done with an ID, not a pubkey
const updatedUser = await updateUser(parseInt(slug), req.body); const updatedUser = await updateUser(parseInt(slug), req.body);
return res.status(200).json(updatedUser); res.status(200).json(updatedUser);
} else { } else {
// Handle attempt to update user with pubkey // Handle attempt to update user with pubkey
return res.status(400).json({ error: "Cannot update user with pubkey. Use ID instead." }); res.status(400).json({ error: "Cannot update user with pubkey. Use ID instead." });
} }
break;
case 'DELETE': case 'DELETE':
if (!isPubkey) { if (!isPubkey) {
// Delete operation should be done with an ID, not a pubkey // Delete operation should be done with an ID, not a pubkey
await deleteUser(parseInt(slug)); await deleteUser(parseInt(slug));
return res.status(204).end(); res.status(204).end();
} else { } else {
// Handle attempt to delete user with pubkey // Handle attempt to delete user with pubkey
return res.status(400).json({ error: "Cannot delete user with pubkey. Use ID instead." }); res.status(400).json({ error: "Cannot delete user with pubkey. Use ID instead." });
} }
break;
default: default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']); res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
return res.status(405).end(`Method ${req.method} Not Allowed`); res.status(405).end(`Method ${req.method} Not Allowed`);
} }
} catch (error) { } catch (error) {
return res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
} }

View File

@ -32,7 +32,7 @@ export default function Details() {
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const { publish, fetchSingleEvent } = useNostr(); const { publishCourse, publishResource, fetchSingleEvent } = useNostr();
const [user] = useLocalStorageWithEffect('user', {}); const [user] = useLocalStorageWithEffect('user', {});
@ -109,18 +109,25 @@ export default function Details() {
return; return;
} }
await publish(signedEvent); let published;
// check if the event is published if (type === 'resource' || type === 'workshop') {
const publishedEvent = await fetchSingleEvent(signedEvent.id); published = await publishResource(signedEvent);
} else if (type === 'course') {
published = await publishCourse(signedEvent);
}
console.log('publishedEvent:', publishedEvent); if (published) {
// check if the event is published
if (publishedEvent) { const publishedEvent = await fetchSingleEvent(signedEvent.id);
// show success message
showToast('success', 'Success', `${type} published successfully.`); console.log('publishedEvent:', publishedEvent);
// delete the draft
await axios.delete(`/api/drafts/${draft.id}`) if (publishedEvent) {
// show success message
showToast('success', 'Success', `${type} published successfully.`);
// delete the draft
await axios.delete(`/api/drafts/${draft.id}`)
.then(res => { .then(res => {
if (res.status === 204) { if (res.status === 204) {
showToast('success', 'Success', 'Draft deleted successfully.'); showToast('success', 'Success', 'Draft deleted successfully.');
@ -132,6 +139,7 @@ export default function Details() {
.catch(err => { .catch(err => {
console.error(err); console.error(err);
}); });
}
} }
} }

View File

@ -1,11 +1,30 @@
import Head from 'next/head' import Head from 'next/head';
import React from 'react'; import React, { useEffect } from 'react';
import CoursesCarousel from '@/components/content/carousels/CoursesCarousel' import CoursesCarousel from '@/components/content/carousels/CoursesCarousel';
import WorkshopsCarousel from '@/components/content/carousels/WorkshopsCarousel' import WorkshopsCarousel from '@/components/content/carousels/WorkshopsCarousel';
import HeroBanner from '@/components/banner/HeroBanner'; import HeroBanner from '@/components/banner/HeroBanner';
import ResourcesCarousel from '@/components/content/carousels/ResourcesCarousel'; import ResourcesCarousel from '@/components/content/carousels/ResourcesCarousel';
import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage';
import axios from 'axios';
export default function Home() { export default function Home() {
const [contentIds, setContentIds] = useLocalStorageWithEffect('contentIds', []);
// this is ready so now we can pass all ids into fetch hooks from loacl storage
useEffect(() => {
const fetchContentIds = async () => {
try {
const response = await axios.get('/api/content/all');
const ids = response.data;
setContentIds(ids);
} catch (error) {
console.error('Failed to fetch content IDs:', error);
}
};
fetchContentIds();
}, [setContentIds]);
return ( return (
<> <>
<Head> <Head>
@ -21,5 +40,5 @@ export default function Home() {
<ResourcesCarousel /> <ResourcesCarousel />
</main> </main>
</> </>
) );
} }