From c7f98fcf5d18f4a6384f6cf659c507e52118cbee Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Wed, 11 Sep 2024 16:48:56 -0500 Subject: [PATCH] Added glow on profile, added isAdmin hook and blocked components / pages that nly admins should access --- src/components/BottomBar.js | 2 +- src/components/navbar/navbar.module.css | 6 ++-- src/components/navbar/user/UserAvatar.js | 25 +++++++++++++--- src/components/sidebar/Sidebar.js | 11 +++++--- src/db/models/roleModels.js | 16 +++++++++++ src/hooks/useIsAdmin.js | 17 +++++++++++ src/pages/api/auth/[...nextauth].js | 23 +++++++++++++++ src/pages/api/get-video-url.js | 2 +- src/pages/api/roles/index.js | 27 ++++++++++++++++++ src/pages/create.js | 19 ++++++++++++- src/pages/profile.js | 36 ++++++++++++++++++++---- 11 files changed, 165 insertions(+), 19 deletions(-) create mode 100644 src/db/models/roleModels.js create mode 100644 src/hooks/useIsAdmin.js create mode 100644 src/pages/api/roles/index.js diff --git a/src/components/BottomBar.js b/src/components/BottomBar.js index bbf4b49..a01d525 100644 --- a/src/components/BottomBar.js +++ b/src/components/BottomBar.js @@ -14,7 +14,7 @@ const BottomBar = () => {
router.push('/')} className={`hover:bg-gray-700 cursor-pointer px-4 py-3 rounded-lg ${isActive('/') ? 'bg-gray-700' : ''}`}>
-
router.push('/content')} className={`hover:bg-gray-700 cursor-pointer px-4 py-3 rounded-lg ${isActive('/content') ? 'bg-gray-700' : ''}`}> +
router.push('/content?tag=all')} className={`hover:bg-gray-700 cursor-pointer px-4 py-3 rounded-lg ${isActive('/content') ? 'bg-gray-700' : ''}`}>
router.push('/feed?channel=global')} className={`hover:bg-gray-700 cursor-pointer px-4 py-3 rounded-lg ${isActive('/feed') ? 'bg-gray-700' : ''}`}> diff --git a/src/components/navbar/navbar.module.css b/src/components/navbar/navbar.module.css index 6aa32f7..af80b05 100644 --- a/src/components/navbar/navbar.module.css +++ b/src/components/navbar/navbar.module.css @@ -9,10 +9,10 @@ } .logo { - border-radius: 25px; + border-radius: 50%; margin-right: 8px; - max-height: 60px; - max-width: 60px; + height: 50px; + width: 50px; } .title { diff --git a/src/components/navbar/user/UserAvatar.js b/src/components/navbar/user/UserAvatar.js index 55f540c..e4af999 100644 --- a/src/components/navbar/user/UserAvatar.js +++ b/src/components/navbar/user/UserAvatar.js @@ -1,5 +1,6 @@ import React, { useRef, useState, useEffect } from 'react'; import Image from 'next/image'; +import { Avatar } from 'primereact/avatar'; import { useRouter } from 'next/router'; import { useImageProxy } from '@/hooks/useImageProxy'; import GenericButton from '@/components/buttons/GenericButton'; @@ -7,6 +8,8 @@ import { Menu } from 'primereact/menu'; import useWindowWidth from '@/hooks/useWindowWidth'; import { useSession, signOut } from 'next-auth/react'; import { Dialog } from 'primereact/dialog'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import { useIsAdmin } from '@/hooks/useIsAdmin'; import 'primereact/resources/primereact.min.css'; import 'primeicons/primeicons.css'; import styles from '../navbar.module.css'; @@ -16,9 +19,10 @@ const UserAvatar = () => { const [isClient, setIsClient] = useState(false); const [user, setUser] = useState(null); const [visible, setVisible] = useState(false); + const [isProfile, setIsProfile] = useState(false); const { returnImageProxy } = useImageProxy(); const windowWidth = useWindowWidth(); - + const { isAdmin, isLoading } = useIsAdmin(); const { data: session, status } = useSession(); useEffect(() => { @@ -28,6 +32,14 @@ const UserAvatar = () => { } }, [session]); + useEffect(() => { + if (router.asPath === '/profile?tab=profile') { + setIsProfile(true); + } else { + setIsProfile(false); + } + }, [router.asPath]); + const menu = useRef(null); const handleLogout = async () => { @@ -57,11 +69,12 @@ const UserAvatar = () => { icon: 'pi pi-user', command: () => router.push('/profile?tab=profile') }, - { + // Only show the "Create" option for admin users + ...(isAdmin ? [{ label: 'Create', icon: 'pi pi-book', command: () => router.push('/create') - }, + }] : []), { label: 'Logout', icon: 'pi pi-power-off', @@ -72,13 +85,17 @@ const UserAvatar = () => { ]; userAvatar = ( <> -
menu.current.toggle(event)} className='flex flex-row items-center justify-between cursor-pointer hover:opacity-75'> +
menu.current.toggle(event)} className={`flex flex-row items-center justify-between cursor-pointer hover:opacity-75`}> logo
diff --git a/src/components/sidebar/Sidebar.js b/src/components/sidebar/Sidebar.js index 91d6474..3b3842d 100644 --- a/src/components/sidebar/Sidebar.js +++ b/src/components/sidebar/Sidebar.js @@ -2,13 +2,14 @@ import React, { useState, useEffect } from 'react'; import { Accordion, AccordionTab } from 'primereact/accordion'; import { useRouter } from 'next/router'; import { useSession, signOut } from 'next-auth/react'; -import { Button } from 'primereact/button'; +import { useIsAdmin } from '@/hooks/useIsAdmin'; import 'primeicons/primeicons.css'; import styles from "./sidebar.module.css"; import { Divider } from 'primereact/divider'; const Sidebar = () => { const [isExpanded, setIsExpanded] = useState(true); + const { isAdmin } = useIsAdmin(); const router = useRouter(); // Helper function to determine if the path matches the current route @@ -61,9 +62,11 @@ const Sidebar = () => {
-
router.push('/create')} className={`w-full flex flex-row items-center cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/create') ? 'bg-gray-700' : ''}`}> -

Create

-
+ {isAdmin && ( +
router.push('/create')} className={`w-full flex flex-row items-center cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/create') ? 'bg-gray-700' : ''}`}> +

Create

+
+ )}
session ? router.push('/profile?tab=subscribe') : router.push('/auth/signin')} className={`w-full flex flex-row items-center cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/profile?tab=subscribe') ? 'bg-gray-700' : ''}`}>

Subscribe

diff --git a/src/db/models/roleModels.js b/src/db/models/roleModels.js new file mode 100644 index 0000000..a367250 --- /dev/null +++ b/src/db/models/roleModels.js @@ -0,0 +1,16 @@ +import prisma from "../prisma"; + +export const createRole = async (data) => { + return await prisma.role.create({ + data: { + user: { connect: { id: data.userId } }, + admin: data.admin, + subscribed: data.subscribed, + // Add other fields as needed, with default values or null if not provided + subscriptionStartDate: null, + lastPaymentAt: null, + subscriptionExpiredAt: null, + nwc: null, + } + }); +}; \ No newline at end of file diff --git a/src/hooks/useIsAdmin.js b/src/hooks/useIsAdmin.js new file mode 100644 index 0000000..8019947 --- /dev/null +++ b/src/hooks/useIsAdmin.js @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react'; +import { useSession } from 'next-auth/react'; + +export function useIsAdmin() { + const { data: session, status } = useSession(); + const [isAdmin, setIsAdmin] = useState(false); + + useEffect(() => { + if (status === 'authenticated') { + setIsAdmin(session?.user?.role?.admin || false); + } else if (status === 'unauthenticated') { + setIsAdmin(false); + } + }, [session, status]); + + return { isAdmin, isLoading: status === 'loading' }; +} \ No newline at end of file diff --git a/src/pages/api/auth/[...nextauth].js b/src/pages/api/auth/[...nextauth].js index a8752ca..8322a4d 100644 --- a/src/pages/api/auth/[...nextauth].js +++ b/src/pages/api/auth/[...nextauth].js @@ -9,6 +9,7 @@ import { findKind0Fields } from "@/utils/nostr"; import { generateSecretKey, getPublicKey } from 'nostr-tools/pure' import { bytesToHex } from '@noble/hashes/utils' import { updateUser } from "@/db/models/userModels"; +import { createRole } from "@/db/models/roleModels"; const relayUrls = [ "wss://nos.lol/", @@ -21,6 +22,7 @@ const relayUrls = [ ]; const BASE_URL = process.env.BASE_URL; +const AUTHOR_PUBKEY = process.env.AUTHOR_PUBKEY; const ndk = new NDK({ explicitRelayUrls: relayUrls, @@ -114,6 +116,27 @@ export const authOptions = { } } + if (user && user?.pubkey === AUTHOR_PUBKEY && !user?.role) { + // create a new author role for this user + const role = await createRole({ + userId: user.id, + admin: true, + subscribed: false, + }); + + if (!role) { + console.error("Failed to create role"); + return null; + } + + const updatedUser = await updateUser(user.id, {role: role.id}); + if (!updatedUser) { + console.error("Failed to update user"); + return null; + } + token.user = updatedUser; + } + // Add combined user object to the token if (user) { token.user = user; diff --git a/src/pages/api/get-video-url.js b/src/pages/api/get-video-url.js index b3ae67d..0a0bc99 100644 --- a/src/pages/api/get-video-url.js +++ b/src/pages/api/get-video-url.js @@ -12,7 +12,7 @@ const s3Client = new S3Client({ }, }) -const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY +const AUTHOR_PUBKEY = process.env.AUTHOR_PUBKEY export default async function handler(req, res) { try { diff --git a/src/pages/api/roles/index.js b/src/pages/api/roles/index.js new file mode 100644 index 0000000..6be0435 --- /dev/null +++ b/src/pages/api/roles/index.js @@ -0,0 +1,27 @@ +import { createRole } from "@/db/models/roleModels"; + +export default async function handler(req, res) { + if (req.method === "POST") { + if (!req.body || !req.body.userId) { + res.status(400).json({ error: "Missing required fields" }); + return; + } + + try { + const roleData = { + userId: req.body.userId, + admin: req.body.admin || false, + subscribed: req.body.subscribed || false, + // Add other fields as needed + }; + + const role = await createRole(roleData); + res.status(201).json(role); + } catch (error) { + console.error("Error creating role:", error); + res.status(500).json({ error: "Error creating role" }); + } + } else { + res.status(405).json({ error: "Method not allowed" }); + } +} \ No newline at end of file diff --git a/src/pages/create.js b/src/pages/create.js index 8c27876..ff609f0 100644 --- a/src/pages/create.js +++ b/src/pages/create.js @@ -1,17 +1,30 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import MenuTab from "@/components/menutab/MenuTab"; import ResourceForm from "@/components/forms/ResourceForm"; import WorkshopForm from "@/components/forms/WorkshopForm"; import CourseForm from "@/components/forms/course/CourseForm"; +import { useIsAdmin } from "@/hooks/useIsAdmin"; +import { useRouter } from "next/router"; +import { ProgressSpinner } from "primereact/progressspinner"; const Create = () => { const [activeIndex, setActiveIndex] = useState(0); // State to track the active tab index + const { isAdmin, isLoading } = useIsAdmin(); + const router = useRouter(); const homeItems = [ { label: 'Resource', icon: 'pi pi-book' }, { label: 'Workshop', icon: 'pi pi-video' }, { label: 'Course', icon: 'pi pi-desktop' } ]; + useEffect(() => { + if (isLoading) return; + + if (!isAdmin) { + router.push('/'); + } + }, [isAdmin, router, isLoading]); + // Function to render the correct form based on the active tab const renderForm = () => { switch (homeItems[activeIndex].label) { @@ -26,6 +39,10 @@ const Create = () => { } }; + if (!isAdmin) return null; + + if (isLoading) return ; + return (

Create a {homeItems[activeIndex].label}

diff --git a/src/pages/profile.js b/src/pages/profile.js index 82f04e4..6be3763 100644 --- a/src/pages/profile.js +++ b/src/pages/profile.js @@ -5,11 +5,16 @@ import UserSettings from "@/components/profile/UserSettings"; import UserContent from "@/components/profile/UserContent"; import UserSubscription from "@/components/profile/subscription/UserSubscription"; import { useRouter } from "next/router"; +import { useSession } from "next-auth/react"; +import { useIsAdmin } from "@/hooks/useIsAdmin"; +import { ProgressSpinner } from "primereact/progressspinner"; const Profile = () => { const router = useRouter(); + const { data: session, status } = useSession(); const [activeTab, setActiveTab] = useState(0); - + const {isAdmin, isLoading} = useIsAdmin(); + const tabs = ["profile", "settings", "content", "subscribe"]; useEffect(() => { @@ -22,12 +27,31 @@ const Profile = () => { } }, [router.query]); + useEffect(() => { + if (status === 'unauthenticated') { + router.push('/auth/signin'); + } + }, [status, router]); + const onTabChange = (e) => { const newIndex = e.index; setActiveTab(newIndex); router.push(`/profile?tab=${tabs[newIndex]}`, undefined, { shallow: true }); }; + if (status === 'loading' || isLoading) { + return ( + + ); + } + + if (status === 'unauthenticated') { + router.push('/auth/signin'); + return null; + } + + if (!session) return null; + return (
{ }}> - - - + + + )}