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 { useLocalStorageWithEffect } from "@/hooks/useLocalStorage";
import { useNostr } from "@/hooks/useNostr";
import {nip19} from "nostr-tools"
import { nip19 } from "nostr-tools"
import { parseEvent } from "@/utils/nostr";
import ContentDropdownItem from "@/components/content/dropdowns/ContentDropdownItem";
import 'primeicons/primeicons.css';
@ -119,12 +119,12 @@ const CourseForm = () => {
if (published) {
// delete the draft
axios.delete(`/api/drafts/${lesson.id}`)
.then((response) => {
console.log('Draft deleted:', response);
})
.catch((error) => {
console.error('Error deleting draft:', error);
});
.then((response) => {
console.log('Draft deleted:', response);
})
.catch((error) => {
console.error('Error deleting draft:', error);
});
}
}
}
@ -178,17 +178,22 @@ const CourseForm = () => {
const signedCourseEvent = await window?.nostr?.signEvent(courseEvent);
console.log('signedCourseEvent:', signedCourseEvent);
// Publish the course event using Nostr
await publish(signedCourseEvent);
const published = await publish(signedCourseEvent);
// Reset the form fields after publishing the course
setTitle('');
setSummary('');
setChecked(false);
setPrice(0);
setCoverImage('');
setLessons([{ id: uuidv4(), title: 'Select a lesson' }]);
setSelectedLessons([]);
setTopics(['']);
if (published) {
// Reset the form fields after publishing the course
setTitle('');
setSummary('');
setChecked(false);
setPrice(0);
setCoverImage('');
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
// Import the PrismaClient class from the @prisma/client package.
import { PrismaClient } from '@prisma/client';
const { PrismaClient } = require('@prisma/client');
// Declare a variable to hold our Prisma client instance.
let prisma;
// Check if the application is running in a production environment.
if (process.env.NODE_ENV === 'production') {
// In production, always create a new instance of PrismaClient.
prisma = new PrismaClient();
} else {
// 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;
// Check if there is already an instance of PrismaClient attached to the global object.
// If not, create a new instance of PrismaClient and attach it to the global object.
// This ensures that the same instance of PrismaClient is reused across multiple invocations.
if (!global.prisma) {
global.prisma = new PrismaClient();
}
// 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 default prisma;
module.exports = prisma;

View File

@ -61,42 +61,46 @@ export const useLogin = () => {
const nostrLogin = useCallback(async () => {
if (!window || !window.nostr) {
showToast('error', 'Nostr Unavailable', 'Nostr is not available');
return;
showToast('error', 'Nostr Unavailable', 'Nostr is not available');
return;
}
const publicKey = await window.nostr.getPublicKey();
if (!publicKey) {
alert('Failed to obtain public key');
return;
showToast('error', 'Public Key Error', 'Failed to obtain public key');
return;
}
try {
const response = await axios.get(`/api/users/${publicKey}`);
if (response.status !== 200) throw new Error('User not found');
;
window.localStorage.setItem('user', JSON.stringify(response.data));
router.push('/').then(() => window.location.reload());
} catch (error) {
const response = await axios.get(`/api/users/${publicKey}`);
let userData;
if (response.status === 204) {
// User not found, create a new user
const kind0 = await fetchKind0(publicKey);
const fields = await findKind0Fields(kind0);
const payload = { pubkey: publicKey, ...fields };
try {
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');
let fields = {};
if (kind0) {
fields = await findKind0Fields(kind0);
}
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(() => {
try {

View File

@ -14,6 +14,8 @@ const defaultRelays = [
"wss://relay.primal.net/"
];
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
export function useNostr() {
const pool = useContext(NostrContext);
const subscriptionQueue = useRef([]);
@ -175,12 +177,12 @@ export function useNostr() {
aTagsAlt.push(`${event.kind}:${event.pubkey}:${event.d}`);
eTags.push(event.id);
});
// Create filters for batch querying
const filterA = { kinds: [9735], '#a': aTags };
const filterE = { kinds: [9735], '#e': eTags };
const filterAAlt = { kinds: [9735], '#a': aTagsAlt };
// Perform batch queries
// const [zapsA, zapsE] = await Promise.all([
// pool.querySync(defaultRelays, filterA),
@ -212,7 +214,7 @@ export function useNostr() {
return [];
}
};
return new Promise((resolve) => {
querySyncQueue.current.push(async () => {
const zaps = await querySyncFn();
@ -222,32 +224,33 @@ export function useNostr() {
});
},
[pool, processQuerySyncQueue]
);
);
const fetchKind0 = useCallback(
async (publicKey) => {
try {
const kind0 = await new Promise((resolve, reject) => {
subscribe(
[{ authors: [publicKey], kinds: [0] }],
{
onevent: (event) => {
resolve(JSON.parse(event.content));
},
onerror: (error) => {
reject(error);
},
}
);
});
return kind0;
} catch (error) {
console.error('Failed to fetch kind 0 for event:', error);
return [];
}
return new Promise((resolve) => {
const timeout = setTimeout(() => {
resolve(null); // Resolve with null if no event is received within the timeout
}, 10000); // 10 seconds timeout
subscribe(
[{ authors: [publicKey], kinds: [0] }],
{
onevent: (event) => {
clearTimeout(timeout);
resolve(JSON.parse(event.content));
},
onerror: (error) => {
clearTimeout(timeout);
console.error('Error fetching kind 0:', error);
resolve(null);
},
}
);
});
},
[subscribe]
);
);
const zapEvent = useCallback(
async (event, amount, comment) => {
@ -334,7 +337,7 @@ export function useNostr() {
);
const fetchResources = useCallback(async () => {
const filter = [{ kinds: [30023, 30402], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
const filter = [{ kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] }];
const hasRequiredTags = (tags) => {
const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
// Check if 'resource' tag exists
@ -374,12 +377,12 @@ export function useNostr() {
}, [subscribe]);
const fetchWorkshops = useCallback(async () => {
const filter = [{ kinds: [30023, 30402], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
const filter = [{ kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] }];
const hasRequiredTags = (tags) => {
const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
const hasWorkshop = tags.some(([tag, value]) => tag === "t" && value === "workshop");
return hasPlebDevs && hasWorkshop;
};
@ -414,7 +417,7 @@ export function useNostr() {
}, [subscribe]);
const fetchCourses = useCallback(async () => {
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }];
const hasRequiredTags = (tags) => {
const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
@ -453,5 +456,94 @@ export function useNostr() {
});
}, [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/"
];
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
export const useNostr = () => {
const [relays, setRelays] = useState(initialRelays);
const [relayStatuses, setRelayStatuses] = useState({});
@ -159,7 +161,7 @@ export const useNostr = () => {
// Fetch resources, workshops, courses, and streams with appropriate filters and update functions
const fetchResources = async () => {
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }];
const hasRequiredTags = async (eventData) => {
const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs");
const hasResource = eventData.some(([tag, value]) => tag === "t" && value === "resource");
@ -181,7 +183,7 @@ export const useNostr = () => {
};
const fetchWorkshops = async () => {
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }];
const hasRequiredTags = async (eventData) => {
const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs");
const hasWorkshop = eventData.some(([tag, value]) => tag === "t" && value === "workshop");
@ -203,7 +205,7 @@ export const useNostr = () => {
};
const fetchCourses = async () => {
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }];
const hasRequiredTags = async (eventData) => {
const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs");
const hasCourse = eventData.some(([tag, value]) => tag === "t" && value === "course");
@ -226,7 +228,7 @@ export const useNostr = () => {
};
// 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");
// 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) {
const { slug } = req.query;
// Determine if slug is a pubkey or an ID
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
const id = parseInt(slug);
if (isNaN(id)) {
return res.status(400).json({ error: "Invalid identifier" });
res.status(400).json({ error: "Invalid identifier" });
return;
}
user = await getUserById(id);
}
if (!user) {
return res.status(204).end();
res.status(204).end();
return;
}
switch (req.method) {
case 'GET':
return res.status(200).json(user);
res.status(200).json(user);
break;
case 'PUT':
if (!isPubkey) {
// Update operation should be done with an ID, not a pubkey
const updatedUser = await updateUser(parseInt(slug), req.body);
return res.status(200).json(updatedUser);
res.status(200).json(updatedUser);
} else {
// 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':
if (!isPubkey) {
// Delete operation should be done with an ID, not a pubkey
await deleteUser(parseInt(slug));
return res.status(204).end();
res.status(204).end();
} else {
// 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:
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) {
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 { publish, fetchSingleEvent } = useNostr();
const { publishCourse, publishResource, fetchSingleEvent } = useNostr();
const [user] = useLocalStorageWithEffect('user', {});
@ -109,18 +109,25 @@ export default function Details() {
return;
}
await publish(signedEvent);
let published;
// check if the event is published
const publishedEvent = await fetchSingleEvent(signedEvent.id);
if (type === 'resource' || type === 'workshop') {
published = await publishResource(signedEvent);
} else if (type === 'course') {
published = await publishCourse(signedEvent);
}
console.log('publishedEvent:', publishedEvent);
if (publishedEvent) {
// show success message
showToast('success', 'Success', `${type} published successfully.`);
// delete the draft
await axios.delete(`/api/drafts/${draft.id}`)
if (published) {
// check if the event is published
const publishedEvent = await fetchSingleEvent(signedEvent.id);
console.log('publishedEvent:', publishedEvent);
if (publishedEvent) {
// show success message
showToast('success', 'Success', `${type} published successfully.`);
// delete the draft
await axios.delete(`/api/drafts/${draft.id}`)
.then(res => {
if (res.status === 204) {
showToast('success', 'Success', 'Draft deleted successfully.');
@ -132,6 +139,7 @@ export default function Details() {
.catch(err => {
console.error(err);
});
}
}
}

View File

@ -1,11 +1,30 @@
import Head from 'next/head'
import React from 'react';
import CoursesCarousel from '@/components/content/carousels/CoursesCarousel'
import WorkshopsCarousel from '@/components/content/carousels/WorkshopsCarousel'
import Head from 'next/head';
import React, { useEffect } from 'react';
import CoursesCarousel from '@/components/content/carousels/CoursesCarousel';
import WorkshopsCarousel from '@/components/content/carousels/WorkshopsCarousel';
import HeroBanner from '@/components/banner/HeroBanner';
import ResourcesCarousel from '@/components/content/carousels/ResourcesCarousel';
import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage';
import axios from 'axios';
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 (
<>
<Head>
@ -21,5 +40,5 @@ export default function Home() {
<ResourcesCarousel />
</main>
</>
)
}
);
}