Removed local storage and using next-auth with session for login and authentication

This commit is contained in:
austinkelsay 2024-08-07 16:02:13 -05:00
parent 658cfe31a9
commit d1eaae6fa1
18 changed files with 273 additions and 112 deletions

10
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ const ZapDisplay = ({ zapAmount, event, zapsLoading }) => {
return (
<>
<span className="text-xs cursor-pointer flex items-center relative shadow-md cursor-pointer hover:opacity-80" onClick={(e) => op.current.toggle(e)}>
<span className="text-xs cursor-pointer flex items-center relative hover:opacity-80" onClick={(e) => op.current.toggle(e)}>
<i className="pi pi-bolt text-yellow-300"></i>
<span className="relative flex items-center min-w-[20px] min-h-[20px]">
{zapsLoading || zapAmount === null || extraLoading ? (

View File

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

View File

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

View File

@ -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 (
<PrimeReactProvider>
<NDKProvider>
<QueryClientProvider client={queryClient}>
<SessionProvider session={session}>
<NDKProvider>
<QueryClientProvider client={queryClient}>
<ToastProvider>
<Layout>
<div className="flex flex-col min-h-screen">
@ -39,6 +41,7 @@ export default function MyApp({
</ToastProvider>
</QueryClientProvider>
</NDKProvider>
</SessionProvider>
</PrimeReactProvider>
);
}

View File

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

75
src/pages/auth/signin.js Normal file
View File

@ -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 (
<div className="w-fit mx-auto mt-24 flex flex-col justify-center">
<h1 className="text-center mb-8">Sign In</h1>
<Button
label={"login with nostr"}
icon="pi pi-user"
className="text-[#f8f8ff] w-[250px] my-4"
rounded
onClick={handleNostrSignIn}
/>
<Button
label={"login anonymously"}
icon="pi pi-user"
className="text-[#f8f8ff] w-[250px] my-4"
rounded
onClick={handleAnonymousSignIn}
/>
<Button
label={"login with email"}
icon="pi pi-envelope"
className="text-[#f8f8ff] w-[250px] my-4"
rounded
onClick={handleEmailSignIn}
/>
</div>
)
}

View File

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

View File

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

View File

@ -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 (
<div className="w-fit mx-auto mt-24 flex flex-col justify-center">
<h1 className="text-center mb-8">Login</h1>
<Button
label={"login with nostr"}
icon="pi pi-user"
className="text-[#f8f8ff] w-[250px] my-4"
rounded
onClick={nostrLogin}
/>
<Button
label={"login anonymously"}
icon="pi pi-user"
className="text-[#f8f8ff] w-[250px] my-4"
rounded
onClick={anonymousLogin}
/>
<Button
label={"login with email"}
icon="pi pi-envelope"
className="text-[#f8f8ff] w-[250px] my-4"
rounded
onClick={anonymousLogin}
/>
</div>
)
}
export default Login;

View File

@ -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 = [];