From d1eaae6fa127ac6ca7a3c9eb06326bfb22185b81 Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Wed, 7 Aug 2024 16:02:13 -0500 Subject: [PATCH] Removed local storage and using next-auth with session for login and authentication --- package-lock.json | 10 +++ package.json | 1 + src/components/course/CourseDetails.js | 12 +++- src/components/forms/CourseForm.js | 13 +++- src/components/forms/ResourceForm.js | 11 ++- src/components/forms/WorkshopForm.js | 11 ++- src/components/navbar/user/UserAvatar.js | 16 +++-- src/components/profile/UserContent.js | 11 ++- src/components/zaps/ZapDisplay.js | 2 +- src/hooks/apiQueries/useDraftsQuery.js | 11 ++- src/hooks/useLocalStorage.js | 46 ------------- src/pages/_app.js | 9 ++- src/pages/api/auth/[...nextauth].js | 86 ++++++++++++++++++++++++ src/pages/auth/signin.js | 75 +++++++++++++++++++++ src/pages/details/[slug].js | 11 ++- src/pages/draft/[slug]/index.js | 12 +++- src/pages/login.js | 35 ---------- src/pages/profile.js | 13 ++-- 18 files changed, 273 insertions(+), 112 deletions(-) delete mode 100644 src/hooks/useLocalStorage.js create mode 100644 src/pages/api/auth/[...nextauth].js create mode 100644 src/pages/auth/signin.js delete mode 100644 src/pages/login.js diff --git a/package-lock.json b/package-lock.json index 0f6f0ec..c07d41b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "next": "14.2.5", "next-auth": "^4.24.7", "next-remove-imports": "^1.0.12", + "nodemailer": "^6.9.14", "nostr-tools": "^2.7.1", "primeicons": "^7.0.0", "primereact": "^10.7.0", @@ -8595,6 +8596,15 @@ "integrity": "sha512-Ww6ZlOiEQfPfXM45v17oabk77Z7mg5bOt7AjDyzy7RjK9OrLrLC8dyZQoAPEOtFX9SaNf1Tdvr5gRJWdTJj7GA==", "license": "MIT" }, + "node_modules/nodemailer": { + "version": "6.9.14", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", + "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/package.json b/package.json index e1bf431..907315d 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "next": "14.2.5", "next-auth": "^4.24.7", "next-remove-imports": "^1.0.12", + "nodemailer": "^6.9.14", "nostr-tools": "^2.7.1", "primeicons": "^7.0.0", "primereact": "^10.7.0", diff --git a/src/components/course/CourseDetails.js b/src/components/course/CourseDetails.js index 0a94b08..6ec2102 100644 --- a/src/components/course/CourseDetails.js +++ b/src/components/course/CourseDetails.js @@ -5,7 +5,7 @@ import ZapDisplay from '@/components/zaps/ZapDisplay'; import { getSatAmountFromInvoice } from '@/utils/lightning'; import { Tag } from 'primereact/tag'; import { nip19 } from 'nostr-tools'; -import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage'; +import { useSession } from 'next-auth/react'; import Image from 'next/image'; import dynamic from 'next/dynamic'; import ZapThreadsWrapper from '@/components/ZapThreadsWrapper'; @@ -33,13 +33,19 @@ export default function CourseDetails({ processedEvent }) { const [bitcoinConnect, setBitcoinConnect] = useState(false); const [nAddress, setNAddress] = useState(null); const [zapAmount, setZapAmount] = useState(0); - - const [user] = useLocalStorageWithEffect('user', {}); + const [user, setUser] = useState(null); const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: processedEvent }); const { returnImageProxy } = useImageProxy(); + const { data: session, status } = useSession(); const router = useRouter(); const ndk = useNDKContext(); + useEffect(() => { + if (session) { + setUser(session.user); + } + }, [session]); + const handleZapEvent = async () => { if (!processedEvent) return; diff --git a/src/components/forms/CourseForm.js b/src/components/forms/CourseForm.js index 7c303d2..ad00e42 100644 --- a/src/components/forms/CourseForm.js +++ b/src/components/forms/CourseForm.js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import axios from "axios"; import { InputText } from "primereact/inputtext"; import { InputNumber } from "primereact/inputnumber"; @@ -7,7 +7,7 @@ import { Button } from "primereact/button"; import { Dropdown } from "primereact/dropdown"; import { ProgressSpinner } from "primereact/progressspinner"; import { v4 as uuidv4, v4 } from 'uuid'; -import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage"; +import { useSession } from 'next-auth/react'; import { useNDKContext } from "@/context/NDKContext"; import { useRouter } from "next/router"; import { useToast } from "@/hooks/useToast"; @@ -32,11 +32,18 @@ const CourseForm = () => { const { resources, resourcesLoading, resourcesError } = useResourcesQuery(); const { workshops, workshopsLoading, workshopsError } = useWorkshopsQuery(); const { drafts, draftsLoading, draftsError } = useDraftsQuery(); - const [user, setUser] = useLocalStorageWithEffect('user', {}); + const { data: session, status } = useSession(); + const [user, setUser] = useState(null); const ndk = useNDKContext(); const router = useRouter(); const { showToast } = useToast(); + useEffect(() => { + if (session) { + setUser(session.user); + } + }, [session]); + /** * Course Creation Flow: * 1. Generate a new course ID diff --git a/src/components/forms/ResourceForm.js b/src/components/forms/ResourceForm.js index 1746875..50bbff0 100644 --- a/src/components/forms/ResourceForm.js +++ b/src/components/forms/ResourceForm.js @@ -5,7 +5,7 @@ import { InputNumber } from "primereact/inputnumber"; import { InputSwitch } from "primereact/inputswitch"; import { Button } from "primereact/button"; import { useRouter } from "next/router";; -import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage"; +import { useSession } from "next-auth/react"; import { useToast } from "@/hooks/useToast"; import dynamic from 'next/dynamic'; const MDEditor = dynamic( @@ -24,11 +24,18 @@ const ResourceForm = ({ draft = null }) => { const [coverImage, setCoverImage] = useState(draft?.image || ''); const [topics, setTopics] = useState(draft?.topics || ['']); const [content, setContent] = useState(draft?.content || ''); + const [user, setUser] = useState(null); - const [user] = useLocalStorageWithEffect('user', {}); + const { data: session, status } = useSession(); const { showToast } = useToast(); const router = useRouter(); + useEffect(() => { + if (session) { + setUser(session.user); + } + }, [session]); + const handleContentChange = useCallback((value) => { setContent(value || ''); }, []); diff --git a/src/components/forms/WorkshopForm.js b/src/components/forms/WorkshopForm.js index 8331d69..29976c1 100644 --- a/src/components/forms/WorkshopForm.js +++ b/src/components/forms/WorkshopForm.js @@ -6,7 +6,7 @@ import { InputNumber } from 'primereact/inputnumber'; import { InputSwitch } from 'primereact/inputswitch'; import { Button } from 'primereact/button'; import { useToast } from '@/hooks/useToast'; -import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage'; +import { useSession } from 'next-auth/react'; import 'primeicons/primeicons.css'; const WorkshopForm = ({ draft = null }) => { @@ -19,9 +19,16 @@ const WorkshopForm = ({ draft = null }) => { const [topics, setTopics] = useState(draft?.topics || ['']); const router = useRouter(); - const [user] = useLocalStorageWithEffect('user', {}); + const { data: session, status } = useSession(); + const [user, setUser] = useState(null); const { showToast } = useToast(); + useEffect(() => { + if (session) { + setUser(session.user); + } + }, [session]); + useEffect(() => { if (draft) { setTitle(draft.title); diff --git a/src/components/navbar/user/UserAvatar.js b/src/components/navbar/user/UserAvatar.js index 25f6975..93fb9ce 100644 --- a/src/components/navbar/user/UserAvatar.js +++ b/src/components/navbar/user/UserAvatar.js @@ -1,4 +1,3 @@ -"use client"; import React, { useRef, useState, useEffect } from 'react'; import Image from 'next/image'; import { useRouter } from 'next/router'; @@ -6,22 +5,31 @@ import { useImageProxy } from '@/hooks/useImageProxy'; import { Button } from 'primereact/button'; import { Menu } from 'primereact/menu'; import useWindowWidth from '@/hooks/useWindowWidth'; -import {useLocalStorageWithEffect} from '@/hooks/useLocalStorage'; +import {useSession, signOut} from 'next-auth/react'; import 'primereact/resources/primereact.min.css'; import 'primeicons/primeicons.css'; import styles from '../navbar.module.css'; const UserAvatar = () => { const router = useRouter(); - const [user, setUser] = useLocalStorageWithEffect('user', {}); const [isClient, setIsClient] = useState(false); + const [user, setUser] = useState(null); const { returnImageProxy } = useImageProxy(); const windowWidth = useWindowWidth(); + const { data: session, status } = useSession(); + + useEffect(() => { + if (session) { + console.log(session); + setUser(session.user); + } + }, [session]); + const menu = useRef(null); const handleLogout = () => { - window.localStorage.removeItem('user'); + signOut(); router.push('/').then(() => window.location.reload()); } diff --git a/src/components/profile/UserContent.js b/src/components/profile/UserContent.js index a8e5128..6950ed8 100644 --- a/src/components/profile/UserContent.js +++ b/src/components/profile/UserContent.js @@ -2,12 +2,12 @@ import React, { useState, useEffect } from "react"; import { useRouter } from "next/router"; import { Button } from "primereact/button"; import MenuTab from "@/components/menutab/MenuTab"; -import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage"; import { useCoursesQuery } from "@/hooks/nostrQueries/content/useCoursesQuery"; import { useResourcesQuery } from "@/hooks/nostrQueries/content/useResourcesQuery"; import { useWorkshopsQuery } from "@/hooks/nostrQueries/content/useWorkshopsQuery"; import { useDraftsQuery } from "@/hooks/apiQueries/useDraftsQuery"; import { useContentIdsQuery } from "@/hooks/apiQueries/useContentIdsQuery"; +import { useSession } from "next-auth/react"; import { useToast } from "@/hooks/useToast"; import ContentList from "@/components/content/lists/ContentList"; import { parseEvent } from "@/utils/nostr"; @@ -21,7 +21,8 @@ const UserContent = () => { const [content, setContent] = useState([]); const [publishedContent, setPublishedContent] = useState([]); - const [user] = useLocalStorageWithEffect("user", {}); + const { data: session, status } = useSession(); + const [user, setUser] = useState(null); const router = useRouter(); const { showToast } = useToast(); const ndk = useNDKContext(); @@ -31,6 +32,12 @@ const UserContent = () => { const { drafts, draftsLoading, draftsError } = useDraftsQuery(); const { contentIds, contentIdsLoading, contentIdsError, refetchContentIds } = useContentIdsQuery(); + useEffect(() => { + if (session) { + setUser(session.user); + } + }, [session]); + useEffect(() => { setIsClient(true); }, []); diff --git a/src/components/zaps/ZapDisplay.js b/src/components/zaps/ZapDisplay.js index 90290c2..5770528 100644 --- a/src/components/zaps/ZapDisplay.js +++ b/src/components/zaps/ZapDisplay.js @@ -18,7 +18,7 @@ const ZapDisplay = ({ zapAmount, event, zapsLoading }) => { return ( <> - op.current.toggle(e)}> + op.current.toggle(e)}> {zapsLoading || zapAmount === null || extraLoading ? ( diff --git a/src/hooks/apiQueries/useDraftsQuery.js b/src/hooks/apiQueries/useDraftsQuery.js index f657d2f..c7b2d39 100644 --- a/src/hooks/apiQueries/useDraftsQuery.js +++ b/src/hooks/apiQueries/useDraftsQuery.js @@ -1,11 +1,18 @@ import { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; -import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage'; +import { useSession } from 'next-auth/react'; export function useDraftsQuery() { const [isClient, setIsClient] = useState(false); - const [user] = useLocalStorageWithEffect('user', {}); + const { data: session, status } = useSession(); + const [user, setUser] = useState(null); + + useEffect(() => { + if (session) { + setUser(session.user); + } + }, [session]); useEffect(() => { setIsClient(true); diff --git a/src/hooks/useLocalStorage.js b/src/hooks/useLocalStorage.js deleted file mode 100644 index b0c4277..0000000 --- a/src/hooks/useLocalStorage.js +++ /dev/null @@ -1,46 +0,0 @@ -"use client"; -import { useState, useEffect } from 'react'; - -// This version of the hook initializes state without immediately attempting to read from localStorage -function useLocalStorage(key, initialValue) { - const [storedValue, setStoredValue] = useState(initialValue); - - // Function to update localStorage and state - const setValue = value => { - try { - const valueToStore = value instanceof Function ? value(storedValue) : value; - setStoredValue(valueToStore); // Update state - if (typeof window !== 'undefined') { - window.localStorage.setItem(key, JSON.stringify(valueToStore)); // Update localStorage - } - } catch (error) { - console.log(error); - } - }; - - return [storedValue, setValue]; -} - -// Custom hook to handle fetching and setting data from localStorage -export function useLocalStorageWithEffect(key, initialValue) { - const [storedValue, setStoredValue] = useLocalStorage(key, initialValue); - - useEffect(() => { - if (typeof window === 'undefined') { - return; - } - try { - const item = window.localStorage.getItem(key); - // Only update if the item exists to prevent overwriting the initial value with null - if (item !== null) { - setStoredValue(JSON.parse(item)); - } - } catch (error) { - console.log(error); - } - }, [key]); // Dependencies array ensures this runs once on mount - - return [storedValue, setStoredValue]; -} - -export default useLocalStorage; diff --git a/src/pages/_app.js b/src/pages/_app.js index bbe0d1d..d7bd31c 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -2,6 +2,7 @@ import { PrimeReactProvider } from 'primereact/api'; import { useEffect } from 'react'; import Navbar from '@/components/navbar/Navbar'; import { ToastProvider } from '@/hooks/useToast'; +import { SessionProvider } from "next-auth/react" import Layout from '@/components/Layout'; import '@/styles/globals.css' import 'primereact/resources/themes/lara-dark-indigo/theme.css'; @@ -17,12 +18,13 @@ import { const queryClient = new QueryClient() export default function MyApp({ - Component, pageProps: { ...pageProps } + Component, pageProps: { session, ...pageProps } }) { return ( - - + + +
@@ -39,6 +41,7 @@ export default function MyApp({ + ); } \ No newline at end of file diff --git a/src/pages/api/auth/[...nextauth].js b/src/pages/api/auth/[...nextauth].js new file mode 100644 index 0000000..4af992e --- /dev/null +++ b/src/pages/api/auth/[...nextauth].js @@ -0,0 +1,86 @@ +import NextAuth from "next-auth"; +import CredentialsProvider from "next-auth/providers/credentials"; +import NDK from "@nostr-dev-kit/ndk"; +import axios from "axios"; +import { findKind0Fields } from "@/utils/nostr"; + +const relayUrls = [ + "wss://nos.lol/", + "wss://relay.damus.io/", + "wss://relay.snort.social/", + "wss://relay.nostr.band/", + "wss://nostr.mutinywallet.com/", + "wss://relay.mutinywallet.com/", + "wss://relay.primal.net/" +]; + +const BASE_URL = process.env.BASE_URL; + +// Initialize NDK +const ndk = new NDK({ + explicitRelayUrls: relayUrls, +}); + +export default NextAuth({ + providers: [ + CredentialsProvider({ + id: "nostr", + name: "Nostr", + credentials: { + pubkey: { label: "Public Key", type: "text" }, + }, + authorize: async (credentials) => { + if (credentials?.pubkey) { + await ndk.connect(); + + const user = ndk.getUser({ pubkey: credentials.pubkey }); + + try { + const profile = await user.fetchProfile(); + + // Check if user exists, create if not + const response = await axios.get(`${BASE_URL}/api/users/${credentials.pubkey}`); + if (response.status === 200 && response.data) { + return response.data; + } else if (response.status === 204) { + // Create user + if (profile) { + const fields = await findKind0Fields(profile); + const payload = { pubkey: credentials.pubkey, ...fields }; + + const createUserResponse = await axios.post(`${BASE_URL}/api/users`, payload); + return createUserResponse.data; + } + } + } catch (error) { + console.error("Nostr login error:", error); + } + } + return null; + }, + }), + ], + callbacks: { + async jwt({ token, user }) { + if (user) { + token.user = user; // Add user to token + } + return token; + }, + async session({ session, token }) { + session.user = token.user; // Add user to session + return session; + }, + async redirect({ url, baseUrl }) { + return url.split("/signin"); + }, + }, + secret: process.env.NEXTAUTH_SECRET, + session: { jwt: true }, + jwt: { + signingKey: process.env.JWT_SECRET, + }, + pages: { + signIn: "/auth/signin", + }, +}); \ No newline at end of file diff --git a/src/pages/auth/signin.js b/src/pages/auth/signin.js new file mode 100644 index 0000000..668a894 --- /dev/null +++ b/src/pages/auth/signin.js @@ -0,0 +1,75 @@ +import { signIn, useSession } from "next-auth/react" +import { useState, useEffect } from "react" +import { useNDKContext } from "@/context/NDKContext"; +import { Button } from 'primereact/button'; +import NDK, { NDKEvent, NDKNip07Signer } from "@nostr-dev-kit/ndk"; + +export default function SignIn() { + const [email, setEmail] = useState("") + const [nostrPubkey, setNostrPubkey] = useState("") + const [nostrPrivkey, setNostrPrivkey] = useState("") + + const { data: session, status } = useSession(); // Get the current session's data and status + + // const ndk = useNDKContext() + + useEffect(() => { + console.log("session", session) + }, [session]) + + const handleEmailSignIn = (e) => { + e.preventDefault() + signIn("email", { email }) + } + + const handleNostrSignIn = async (e) => { + e.preventDefault() + + const nip07signer = new NDKNip07Signer(); + const ndk = new NDK({ signer: nip07signer }); + + await ndk.connect() + + try { + const user = await nip07signer.user() + + console.log("user in signin", user) + const pubkey = user?._pubkey + signIn("nostr", { pubkey }) + } catch (error) { + console.error("Error signing Nostr event:", error) + } + } + + const handleAnonymousSignIn = (e) => { + e.preventDefault() + signIn("anonymous") + } + + return ( +
+

Sign In

+
+ ) +} \ No newline at end of file diff --git a/src/pages/details/[slug].js b/src/pages/details/[slug].js index 13464e3..092b2c1 100644 --- a/src/pages/details/[slug].js +++ b/src/pages/details/[slug].js @@ -6,7 +6,7 @@ import { getSatAmountFromInvoice } from '@/utils/lightning'; import ZapDisplay from '@/components/zaps/ZapDisplay'; import { Tag } from 'primereact/tag'; import { nip19, nip04 } from 'nostr-tools'; -import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage'; +import { useSession } from 'next-auth/react'; import Image from 'next/image'; import dynamic from 'next/dynamic'; import ZapThreadsWrapper from '@/components/ZapThreadsWrapper'; @@ -40,12 +40,19 @@ export default function Details() { const [decryptedContent, setDecryptedContent] = useState(null); const ndk = useNDKContext(); - const [user] = useLocalStorageWithEffect('user', {}); + const { data: session, status } = useSession(); + const [user, setUser] = useState(null); const { returnImageProxy } = useImageProxy(); const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: processedEvent }); const router = useRouter(); + useEffect(() => { + if (session) { + setUser(session.user); + } + }, [session]); + useEffect(() => { if (processedEvent.price) { setPaidResource(true); diff --git a/src/pages/draft/[slug]/index.js b/src/pages/draft/[slug]/index.js index 51b21d2..bb83ef5 100644 --- a/src/pages/draft/[slug]/index.js +++ b/src/pages/draft/[slug]/index.js @@ -4,7 +4,7 @@ import { useRouter } from 'next/router'; import { hexToNpub } from '@/utils/nostr'; import { nip19, nip04 } from 'nostr-tools'; import { v4 as uuidv4 } from 'uuid'; -import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage'; +import { useSession } from 'next-auth/react'; import { useImageProxy } from '@/hooks/useImageProxy'; import { Button } from 'primereact/button'; import { useToast } from '@/hooks/useToast'; @@ -44,12 +44,19 @@ function validateEvent(event) { export default function Draft() { const [draft, setDraft] = useState(null); const { returnImageProxy } = useImageProxy(); - const [user] = useLocalStorageWithEffect('user', {}); + const { data: session, status } = useSession(); + const [user, setUser] = useState(null); const { width, height } = useResponsiveImageDimensions(); const router = useRouter(); const { showToast } = useToast(); const ndk = useNDKContext(); + useEffect(() => { + if (session) { + setUser(session.user); + } + }, [session]); + useEffect(() => { if (router.isReady) { const { slug } = router.query; @@ -117,7 +124,6 @@ export default function Draft() { try { price = resource.tags.find(tag => tag[0] === 'price')[1]; } catch (err) { - console.error(err); price = 0; } diff --git a/src/pages/login.js b/src/pages/login.js deleted file mode 100644 index 096d8a5..0000000 --- a/src/pages/login.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from "react"; -import { Button } from 'primereact/button'; -import { useLogin } from "@/hooks/useLogin"; - -const Login = () => { - const { nostrLogin, anonymousLogin } = useLogin(); - return ( -
-

Login

-
- ) -} - -export default Login; \ No newline at end of file diff --git a/src/pages/profile.js b/src/pages/profile.js index 8fb8589..01742fe 100644 --- a/src/pages/profile.js +++ b/src/pages/profile.js @@ -1,21 +1,26 @@ -import React, { useRef, useState, useEffect } from "react"; +import React, { useRef, useState, useEffect, use } from "react"; import { Button } from "primereact/button"; import { DataTable } from "primereact/datatable"; import { Menu } from "primereact/menu"; import { Column } from "primereact/column"; import { useImageProxy } from "@/hooks/useImageProxy"; import { useRouter } from "next/router"; -import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage"; +import { useSession } from 'next-auth/react'; import UserContent from "@/components/profile/UserContent"; import Image from "next/image"; import BitcoinConnectButton from "@/components/profile/BitcoinConnect"; const Profile = () => { - const [user] = useLocalStorageWithEffect("user", {}); + const { data: session, status } = useSession(); + const [user, setUser] = useState(null); const { returnImageProxy } = useImageProxy(); const menu = useRef(null); - console.log('user:', user); + useEffect(() => { + if (session) { + setUser(session.user); + } + }, [session]); const purchases = [];