From a564665861faf93fb8274e65ab1e18b0756f6499 Mon Sep 17 00:00:00 2001 From: Presa <124523559+zenWai@users.noreply.github.com> Date: Wed, 2 Oct 2024 18:32:44 +0200 Subject: [PATCH] API per current docs, name changes, server-only package, stuff (#21) - /api/admin is per `auth.js` docs. - SocialButtons.tsx renamed and address `callbackUrl deprecated` - hooks with 'use client' - remove `` from root layout, usage for client components in app router - `SessionProvider` is not used on root layout, navbar refactored to have UserAvatarMenu serverside for session, clientPage refactored and created client component for demo - Settings page is now server-side and grab user session server-side. `Now there is no flickering on form load`. - install package `'server-only'` - introduction of `'server-only'` package for auth-utils.ts - /actions/register checks for 'production' since dev ip is proxied - login action fix bug where user with 2FA on and with wrong password, would still move to 2fa logic and send email. - middleware.ts no need to check isApiAuthRoute anymore, fixed rate limit - rename UserInfo.tsx AuthFormHeader.tsx - UserAvatarMenu.tsx moved, renamed and now server side auth - Moved some components for better organization --- actions/login.ts | 18 +- actions/register.ts | 19 +- app/(protected)/_components/Navbar.tsx | 30 --- app/(protected)/admin/page.tsx | 4 +- app/(protected)/client/client-component.tsx | 14 ++ app/(protected)/client/page.tsx | 13 +- app/(protected)/layout.tsx | 9 +- app/(protected)/server/page.tsx | 2 +- app/(protected)/settings/SettingsForm.tsx | 189 +++++++++++++++++ app/(protected)/settings/page.tsx | 194 +----------------- app/api/admin/route.ts | 23 +-- app/layout.tsx | 15 +- app/page.tsx | 2 +- components/{auth => }/LoginButton.tsx | 0 components/LogoutButton.tsx | 10 + components/{auth => }/RoleGate.tsx | 0 components/{user-info.tsx => UserInfo.tsx} | 0 .../admin-only-rh-and-sa.tsx | 0 .../auth/{Header.tsx => AuthFormHeader.tsx} | 2 +- components/auth/CardWrapper.tsx | 8 +- components/auth/LogoutButton.tsx | 13 -- .../auth/{Social.tsx => SocialButtons.tsx} | 4 +- components/navbar/Navbar.tsx | 5 + components/navbar/NavigationMenu.tsx | 33 +++ .../UserAvatarMenu.tsx} | 10 +- hooks/use-current-role.ts | 4 + hooks/use-current-user.ts | 4 + lib/auth-utils.ts | 11 +- middleware.ts | 17 +- package-lock.json | 6 + package.json | 1 + 31 files changed, 358 insertions(+), 302 deletions(-) delete mode 100644 app/(protected)/_components/Navbar.tsx create mode 100644 app/(protected)/client/client-component.tsx create mode 100644 app/(protected)/settings/SettingsForm.tsx rename components/{auth => }/LoginButton.tsx (100%) create mode 100644 components/LogoutButton.tsx rename components/{auth => }/RoleGate.tsx (100%) rename components/{user-info.tsx => UserInfo.tsx} (100%) rename {app/(protected)/_components => components}/admin-only-rh-and-sa.tsx (100%) rename components/auth/{Header.tsx => AuthFormHeader.tsx} (89%) delete mode 100644 components/auth/LogoutButton.tsx rename components/auth/{Social.tsx => SocialButtons.tsx} (90%) create mode 100644 components/navbar/Navbar.tsx create mode 100644 components/navbar/NavigationMenu.tsx rename components/{auth/UserButton.tsx => navbar/UserAvatarMenu.tsx} (79%) diff --git a/actions/login.ts b/actions/login.ts index 22fabd0..4b934d8 100644 --- a/actions/login.ts +++ b/actions/login.ts @@ -2,6 +2,7 @@ import { AuthError } from 'next-auth'; import * as zod from 'zod'; +import bcrypt from 'bcryptjs'; import { getVerificationTokenByEmail } from '@/data/verification-token'; import { getTwoFactorConfirmationByUserId } from '@/data/two-factor-confirmation'; @@ -28,6 +29,15 @@ export const login = async (values: zod.infer, callbackUrl?: return { error: 'Invalid credentials' }; } + // Verify password before proceeding with any other checks + const passwordsMatch = await bcrypt.compare(password, existingUser.password); + if (!passwordsMatch) { + return { error: 'Invalid credentials' }; + } + + /** Confirmation email token recently sent? + * if not, generates and send email + */ if (!existingUser.emailVerified) { const existingToken = await getVerificationTokenByEmail(email); if (existingToken) { @@ -44,7 +54,12 @@ export const login = async (values: zod.infer, callbackUrl?: return { success: 'Confirmation email sent!' }; } + /** 2FA code logic + * Currently if current token is unexpired it does not re-send a new one + * Reduce db calls and e-mail sents on this preview + */ if (existingUser.isTwoFactorEnabled && existingUser.email) { + // If user is already at the 2fa on loginForm if (code) { const twoFactorToken = await getTwoFactorTokenByEmail(existingUser.email); if (!twoFactorToken) { @@ -70,11 +85,12 @@ export const login = async (values: zod.infer, callbackUrl?: where: { id: existingConfirmation.id }, }); } - + // consumed by the signIn callback await db.twoFactorConfirmation.create({ data: { userId: existingUser.id }, }); } else { + // return { twoFactor: true }; sends the user to the 2fa on loginForm const existingTwoFactorToken = await getTwoFactorTokenByEmail(existingUser.email); if (existingTwoFactorToken) { const hasExpired = new Date(existingTwoFactorToken.expires) < new Date(); diff --git a/actions/register.ts b/actions/register.ts index fd699fe..33c9336 100644 --- a/actions/register.ts +++ b/actions/register.ts @@ -14,29 +14,26 @@ import { generateVerificationToken } from '@/lib/tokens'; export const register = async (values: zod.infer) => { const validatedFields = RegisterSchema.safeParse(values); + if (!validatedFields.success) return { error: 'Invalid fields' }; + + const { email, password, name } = validatedFields.data; + const headersList = headers(); const userIp = headersList.get('request-ip'); const hashedIp = await hashIp(userIp); - if (userIp === '127.0.0.1' || !userIp || hashedIp === 'unknown') { - return { error: 'Sorry! Something went wrong. Please try again later.' }; + /* If we can not determine the IP of the user, fails to register */ + if ((process.env.NODE_ENV === 'production' && userIp === '127.0.0.1') || !userIp || hashedIp === 'unknown') { + return { error: 'Sorry! Something went wrong. Could not identify you as user' }; } const existingAccounts = await db.user.count({ where: { ip: hashedIp }, }); - if (existingAccounts >= 2) { + if (process.env.NODE_ENV === 'production' && existingAccounts >= 2) { return { error: 'You are not allowed to register more accounts on this app preview' }; } - if (!validatedFields.success) { - return { - error: 'Invalid fields', - }; - } - - const { email, password, name } = validatedFields.data; - const existingUser = await getUserByEmail(email); if (existingUser) { return { error: 'Email already registered!' }; diff --git a/app/(protected)/_components/Navbar.tsx b/app/(protected)/_components/Navbar.tsx deleted file mode 100644 index 4b2f14e..0000000 --- a/app/(protected)/_components/Navbar.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; - -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; - -import { UserButton } from '@/components/auth/UserButton'; -import { Button } from '@/components/ui/button'; - -export function Navbar() { - const pathname = usePathname(); - return ( - - ); -} diff --git a/app/(protected)/admin/page.tsx b/app/(protected)/admin/page.tsx index 3a99f30..84b2167 100644 --- a/app/(protected)/admin/page.tsx +++ b/app/(protected)/admin/page.tsx @@ -1,7 +1,7 @@ import { UserRole } from '@prisma/client'; -import { AdminOnlyRhAndSa } from '@/app/(protected)/_components/admin-only-rh-and-sa'; -import { RoleGate } from '@/components/auth/RoleGate'; +import { AdminOnlyRhAndSa } from '@/components/admin-only-rh-and-sa'; +import { RoleGate } from '@/components/RoleGate'; import { FormSuccess } from '@/components/form-messages/FormSuccess'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; diff --git a/app/(protected)/client/client-component.tsx b/app/(protected)/client/client-component.tsx new file mode 100644 index 0000000..75c2373 --- /dev/null +++ b/app/(protected)/client/client-component.tsx @@ -0,0 +1,14 @@ +'use client'; + +import { useCurrentUser } from '@/hooks/use-current-user'; +import UserInfo from '@/components/UserInfo'; + +export default function ClientComponent() { + const userSession = useCurrentUser(); + return ( +
+ {/* This userInfo component is what we call a hybrid component, as children of a client component, is a client component */} + +
+ ); +} diff --git a/app/(protected)/client/page.tsx b/app/(protected)/client/page.tsx index 1ae2e60..da1c752 100644 --- a/app/(protected)/client/page.tsx +++ b/app/(protected)/client/page.tsx @@ -1,12 +1,11 @@ -'use client'; -import UserInfo from '@/components/user-info'; -import { useCurrentUser } from '@/hooks/use-current-user'; +import { SessionProvider } from 'next-auth/react'; + +import ClientComponent from '@/app/(protected)/client/client-component'; export default function ClientPage() { - const userSession = useCurrentUser(); return ( -
- -
+ + + ); } diff --git a/app/(protected)/layout.tsx b/app/(protected)/layout.tsx index 6820c96..e258047 100644 --- a/app/(protected)/layout.tsx +++ b/app/(protected)/layout.tsx @@ -1,4 +1,6 @@ -import { Navbar } from '@/app/(protected)/_components/Navbar'; +import { NavigationMenu } from '@/components/navbar/NavigationMenu'; +import { UserAvatarMenu } from '@/components/navbar/UserAvatarMenu'; +import { Navbar } from '@/components/navbar/Navbar'; export default function ProtectedLayout(props: { children: React.ReactNode }) { return ( @@ -6,7 +8,10 @@ export default function ProtectedLayout(props: { children: React.ReactNode }) { className='flex h-full w-full flex-col items-center justify-center gap-y-10 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-sky-400 to-blue-800' > - + + + + {props.children} ); diff --git a/app/(protected)/server/page.tsx b/app/(protected)/server/page.tsx index 5f3d2c2..7a1f397 100644 --- a/app/(protected)/server/page.tsx +++ b/app/(protected)/server/page.tsx @@ -1,4 +1,4 @@ -import UserInfo from '@/components/user-info'; +import UserInfo from '@/components/UserInfo'; import { currentSessionUser } from '@/lib/auth-utils'; export default async function ServerPage() { diff --git a/app/(protected)/settings/SettingsForm.tsx b/app/(protected)/settings/SettingsForm.tsx new file mode 100644 index 0000000..a735378 --- /dev/null +++ b/app/(protected)/settings/SettingsForm.tsx @@ -0,0 +1,189 @@ +'use client'; +import * as zod from 'zod'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { UserRole } from '@prisma/client'; +import { useSession } from 'next-auth/react'; +import { useEffect, useState, useTransition } from 'react'; + +import type { ExtendedUser } from '@/next-auth'; +import { settings } from '@/actions/settings'; +import { FormError } from '@/components/form-messages/FormError'; +import { FormSuccess } from '@/components/form-messages/FormSuccess'; +import { Button } from '@/components/ui/button'; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; +import { SettingsSchema } from '@/schemas'; + +export function SettingsForm({ user }: { user: ExtendedUser }) { + const { update } = useSession(); + + const [error, setError] = useState(); + const [success, setSuccess] = useState(); + const [isPending, startTransition] = useTransition(); + + const defaultValues = { + name: user?.name || '', + email: user?.email || '', + password: undefined, + newPassword: undefined, + role: user?.role || UserRole.USER, + isTwoFactorEnabled: user?.isTwoFactorEnabled || undefined, + }; + + const form = useForm>({ + resolver: zodResolver(SettingsSchema), + defaultValues, + }); + + useEffect(() => { + if (user) { + form.reset(defaultValues); + } + }, [user?.name, user?.email, user?.id, user?.image, user?.isOauth, user?.isTwoFactorEnabled, user?.role, form.reset]); + + const onSubmit = (values: zod.infer) => { + setError(''); + setSuccess(''); + startTransition(() => { + settings(values) + .then((data) => { + if (data.error) { + setError(data.error); + } + if (data.success) { + // updates client side session + update(); + setSuccess(data.success); + } + }) + .catch(() => setError('An error occurred!')); + }); + }; + return ( + <> +
+ +
+ {/* Name */} + ( + + Name + + + + + + )} + /> + {/* Content not shown to oauth users */} + {/* Email and Password */} + {user?.isOauth === false && ( + <> + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + ( + + New Password + + + + + + )} + /> + + )} + {/* Role */} + ( + + Role + + + + )} + /> + {/* Two Factor Authentication not shown to oauth users */} + {user?.isOauth === false && ( + ( + +
+ Two Factor Authentication + Enable Two Factor Authentication for your account +
+ + + +
+ )} + /> + )} +
+ + + + + + + ); +} diff --git a/app/(protected)/settings/page.tsx b/app/(protected)/settings/page.tsx index 552182a..17f2601 100644 --- a/app/(protected)/settings/page.tsx +++ b/app/(protected)/settings/page.tsx @@ -1,69 +1,12 @@ -'use client'; +import { SessionProvider } from 'next-auth/react'; -import { UserRole } from '@prisma/client'; -import * as zod from 'zod'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -import { useSession } from 'next-auth/react'; -import { useEffect, useState, useTransition } from 'react'; - -import { FormError } from '@/components/form-messages/FormError'; -import { FormSuccess } from '@/components/form-messages/FormSuccess'; -import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { Switch } from '@/components/ui/switch'; -import { useCurrentUser } from '@/hooks/use-current-user'; -import { SettingsSchema } from '@/schemas'; -import { settings } from '@/actions/settings'; -import { Button } from '@/components/ui/button'; +import { SettingsForm } from '@/app/(protected)/settings/SettingsForm'; +import { auth } from '@/auth'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; - -export default function SettingsPage() { - const user = useCurrentUser(); - const { update } = useSession(); - - const [error, setError] = useState(); - const [success, setSuccess] = useState(); - const [isPending, startTransition] = useTransition(); - - const defaultValues = { - name: user?.name || '', - email: user?.email || '', - password: undefined, - newPassword: undefined, - role: user?.role || UserRole.USER, - isTwoFactorEnabled: user?.isTwoFactorEnabled || undefined, - }; - - const form = useForm>({ - resolver: zodResolver(SettingsSchema), - defaultValues, - }); - - useEffect(() => { - if (user) { - form.reset(defaultValues); - } - }, [user?.name, user?.email, user?.id, user?.image, user?.isOauth, user?.isTwoFactorEnabled, user?.role, form.reset]); - const onSubmit = (values: zod.infer) => { - setError(''); - setSuccess(''); - startTransition(() => { - settings(values) - .then((data) => { - if (data.error) { - setError(data.error); - } - if (data.success) { - update(); - setSuccess(data.success); - } - }) - .catch(() => setError('An error occurred!')); - }); - }; +export default async function SettingsPage() { + const session = await auth(); + const user = session?.user; return ( @@ -72,128 +15,9 @@ export default function SettingsPage() { {user && ( - <> -
- -
- {/* Name */} - ( - - Name - - - - - - )} - /> - {/* Content not shown to oauth users */} - {/* Email and Password */} - {user?.isOauth === false && ( - <> - ( - - Email - - - - - - )} - /> - ( - - Password - - - - - - )} - /> - ( - - New Password - - - - - - )} - /> - - )} - {/* Role */} - ( - - Role - - - - )} - /> - {/* Two Factor Authentication not shown to oauth users */} - {user?.isOauth === false && ( - ( - -
- Two Factor Authentication - Enable Two Factor Authentication for your account -
- - - -
- )} - /> - )} -
- - - - - - + + + )}
diff --git a/app/api/admin/route.ts b/app/api/admin/route.ts index b495af9..c0dbf11 100644 --- a/app/api/admin/route.ts +++ b/app/api/admin/route.ts @@ -1,18 +1,17 @@ import { UserRole } from '@prisma/client'; import { NextResponse } from 'next/server'; -import { currentSessionRole } from '@/lib/auth-utils'; +import { auth } from '@/auth'; -export async function GET() { - try { - const role = await currentSessionRole(); +export const GET = auth(function GET(req) { + if (!req.auth) { + return NextResponse.json({ message: 'Not authenticated' }, { status: 401 }); + } - if (role === UserRole.ADMIN) { - return NextResponse.json({ message: 'Allowed RH call' }, { status: 200 }); - } else { - return NextResponse.json({ message: 'Forbidden RH call' }, { status: 403 }); - } - } catch (error) { - return NextResponse.json({ message: `Internal Server Error: ${error}` }, { status: 500 }); + const role = req.auth.user.role; + if (role === UserRole.ADMIN) { + return NextResponse.json({ message: 'Allowed RH call' }, { status: 200 }); + } else { + return NextResponse.json({ message: 'Forbidden RH call' }, { status: 403 }); } -} +}); diff --git a/app/layout.tsx b/app/layout.tsx index cf44b62..7397047 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,5 @@ import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; -import { SessionProvider } from 'next-auth/react'; import { Toaster } from '@/components/ui/sonner'; @@ -19,13 +18,11 @@ export default async function RootLayout({ children: React.ReactNode; }>) { return ( - - - - - {children} - - - + + + + {children} + + ); } diff --git a/app/page.tsx b/app/page.tsx index 6d86980..6965465 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,7 +2,7 @@ import { Poppins } from 'next/font/google'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; -import { LoginButton } from '@/components/auth/LoginButton'; +import { LoginButton } from '@/components/LoginButton'; const font = Poppins({ subsets: ['latin'], diff --git a/components/auth/LoginButton.tsx b/components/LoginButton.tsx similarity index 100% rename from components/auth/LoginButton.tsx rename to components/LoginButton.tsx diff --git a/components/LogoutButton.tsx b/components/LogoutButton.tsx new file mode 100644 index 0000000..676180f --- /dev/null +++ b/components/LogoutButton.tsx @@ -0,0 +1,10 @@ +'use client'; +import { signOut } from 'next-auth/react'; + +export function LogoutButton(props: { children: React.ReactNode }) { + return ( + signOut()}> + {props.children} + + ); +} diff --git a/components/auth/RoleGate.tsx b/components/RoleGate.tsx similarity index 100% rename from components/auth/RoleGate.tsx rename to components/RoleGate.tsx diff --git a/components/user-info.tsx b/components/UserInfo.tsx similarity index 100% rename from components/user-info.tsx rename to components/UserInfo.tsx diff --git a/app/(protected)/_components/admin-only-rh-and-sa.tsx b/components/admin-only-rh-and-sa.tsx similarity index 100% rename from app/(protected)/_components/admin-only-rh-and-sa.tsx rename to components/admin-only-rh-and-sa.tsx diff --git a/components/auth/Header.tsx b/components/auth/AuthFormHeader.tsx similarity index 89% rename from components/auth/Header.tsx rename to components/auth/AuthFormHeader.tsx index 1210bde..f1fae9e 100644 --- a/components/auth/Header.tsx +++ b/components/auth/AuthFormHeader.tsx @@ -8,7 +8,7 @@ interface CardHeaderProps { label: string; } -export function Header({ label }: CardHeaderProps) { +export function AuthFormHeader({ label }: CardHeaderProps) { return (

diff --git a/components/auth/CardWrapper.tsx b/components/auth/CardWrapper.tsx index 6acb93e..1de6532 100644 --- a/components/auth/CardWrapper.tsx +++ b/components/auth/CardWrapper.tsx @@ -1,8 +1,8 @@ 'use client'; import { BackButton } from '@/components/auth/BackButton'; -import { Header } from '@/components/auth/Header'; -import { Social } from '@/components/auth/Social'; +import { AuthFormHeader } from '@/components/auth/AuthFormHeader'; +import { SocialButtons } from '@/components/auth/SocialButtons'; import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'; interface CardWrapperProps { @@ -17,12 +17,12 @@ export function CardWrapper({ children, headerLabel, backButtonLabel, backButton return ( -
+ {children} {showSocial && ( - + )} diff --git a/components/auth/LogoutButton.tsx b/components/auth/LogoutButton.tsx deleted file mode 100644 index 975b5de..0000000 --- a/components/auth/LogoutButton.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { logout } from '@/actions/logout'; - -export function LogoutButton(props: { children: React.ReactNode }) { - const onClick = () => { - logout(); - }; - - return ( - - {props.children} - - ); -} diff --git a/components/auth/Social.tsx b/components/auth/SocialButtons.tsx similarity index 90% rename from components/auth/Social.tsx rename to components/auth/SocialButtons.tsx index cf6d898..ea564b3 100644 --- a/components/auth/Social.tsx +++ b/components/auth/SocialButtons.tsx @@ -8,12 +8,12 @@ import { signIn } from 'next-auth/react'; import { DEFAULT_LOGIN_REDIRECT } from '@/routes'; import { Button } from '@/components/ui/button'; -export function Social() { +export function SocialButtons() { const searchParams = useSearchParams(); const callbackUrl = searchParams.get('callbackUrl'); const onClick = (provider: 'google' | 'github') => { signIn(provider, { - callbackUrl: callbackUrl || DEFAULT_LOGIN_REDIRECT, + redirectTo: callbackUrl || DEFAULT_LOGIN_REDIRECT, }); }; return ( diff --git a/components/navbar/Navbar.tsx b/components/navbar/Navbar.tsx new file mode 100644 index 0000000..8776e06 --- /dev/null +++ b/components/navbar/Navbar.tsx @@ -0,0 +1,5 @@ +export function Navbar({ children }: { children: React.ReactNode }) { + return ( + + ); +} diff --git a/components/navbar/NavigationMenu.tsx b/components/navbar/NavigationMenu.tsx new file mode 100644 index 0000000..c1b97c1 --- /dev/null +++ b/components/navbar/NavigationMenu.tsx @@ -0,0 +1,33 @@ +'use client'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import { Button } from '@/components/ui/button'; + +export function NavigationMenu() { + const pathname = usePathname(); + return ( +
+ + + + +
+ ); +} diff --git a/components/auth/UserButton.tsx b/components/navbar/UserAvatarMenu.tsx similarity index 79% rename from components/auth/UserButton.tsx rename to components/navbar/UserAvatarMenu.tsx index 56b65a5..b5e0dad 100644 --- a/components/auth/UserButton.tsx +++ b/components/navbar/UserAvatarMenu.tsx @@ -1,10 +1,10 @@ -'use client'; +import 'server-only'; import { FaUser } from 'react-icons/fa'; import { ExitIcon } from '@radix-ui/react-icons'; -import { LogoutButton } from '@/components/auth/LogoutButton'; -import { useCurrentUser } from '@/hooks/use-current-user'; +import { currentSessionUser } from '@/lib/auth-utils'; +import { LogoutButton } from '@/components/LogoutButton'; import { DropdownMenu, DropdownMenuContent, @@ -13,8 +13,8 @@ import { } from '@/components/ui/dropdown-menu'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; -export function UserButton() { - const user = useCurrentUser(); +export async function UserAvatarMenu() { + const user = await currentSessionUser(); return ( diff --git a/hooks/use-current-role.ts b/hooks/use-current-role.ts index f7a4003..1c4dfdb 100644 --- a/hooks/use-current-role.ts +++ b/hooks/use-current-role.ts @@ -1,5 +1,9 @@ +'use client'; import { useSession } from 'next-auth/react'; +{ + /* To be used on client components */ +} export const useCurrentRole = () => { const session = useSession(); return session?.data?.user?.role; diff --git a/hooks/use-current-user.ts b/hooks/use-current-user.ts index 5658c55..ab6876a 100644 --- a/hooks/use-current-user.ts +++ b/hooks/use-current-user.ts @@ -1,5 +1,9 @@ +'use client'; import { useSession } from 'next-auth/react'; +{ + /* To be used on client components */ +} export const useCurrentUser = () => { const session = useSession(); diff --git a/lib/auth-utils.ts b/lib/auth-utils.ts index eb216ea..522432d 100644 --- a/lib/auth-utils.ts +++ b/lib/auth-utils.ts @@ -1,15 +1,20 @@ -'use server'; +import 'server-only'; import crypto from 'crypto'; +import type { UserRole } from '@prisma/client'; + +import type { ExtendedUser } from '@/next-auth'; import { auth } from '@/auth'; -export const currentSessionUser = async () => { +/** To be used in server components */ +export const currentSessionUser = async (): Promise => { const session = await auth(); return session?.user; }; -export const currentSessionRole = async () => { +/** To be used in server components */ +export const currentSessionRole = async (): Promise => { const session = await auth(); return session?.user?.role; diff --git a/middleware.ts b/middleware.ts index 2a2d2bc..b25a66c 100644 --- a/middleware.ts +++ b/middleware.ts @@ -3,14 +3,14 @@ import { NextResponse } from 'next/server'; import { Ratelimit } from '@upstash/ratelimit'; import { kv } from '@vercel/kv'; -import { apiAuthPrefix, authRoutes, DEFAULT_LOGIN_REDIRECT, publicRoutes } from '@/routes'; +import { authRoutes, DEFAULT_LOGIN_REDIRECT, publicRoutes } from '@/routes'; import authConfig from '@/auth.config'; const { auth } = NextAuth(authConfig); const ratelimit = new Ratelimit({ redis: kv, - limiter: Ratelimit.cachedFixedWindow(20, '10 s'), + limiter: Ratelimit.cachedFixedWindow(10, '10 s'), }); export default auth(async (req) => { @@ -18,23 +18,14 @@ export default auth(async (req) => { const ip = req.ip ?? '127.0.0.1'; const { success } = await ratelimit.limit(ip); if (!success) { - return new NextResponse('You are being rate limited due to too many requests. Try again later!', { status: 429 }); + return NextResponse.json('You are being rate limited due to too many requests. Try again later!', { status: 429 }); } const isLoggedIn = !!req.auth; - const isApiAuthRoute = apiAuthPrefix.includes(nextUrl.pathname); const isPublicRoute = publicRoutes.includes(nextUrl.pathname); const isAuthRoute = authRoutes.includes(nextUrl.pathname); - if (isApiAuthRoute) { - if (isAuthRoute && isLoggedIn) { - return NextResponse.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl)); - } - - return NextResponse.next(); - } - if (isAuthRoute) { if (isLoggedIn) { return NextResponse.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl)); @@ -66,5 +57,5 @@ export default auth(async (req) => { // Optionally, don't invoke Middleware on some paths export const config = { - matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)', '/api/admin'], + matcher: ['/((?!api/auth|_next/static|_next/image|favicon.ico).*)'], }; diff --git a/package-lock.json b/package-lock.json index 378d839..86808a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "react-icons": "5.3.0", "react-spinners": "0.13.8", "resend": "4.0.0", + "server-only": "^0.0.1", "sonner": "1.5.0", "tailwind-merge": "2.5.2", "tailwindcss-animate": "1.0.7", @@ -5704,6 +5705,11 @@ "node": ">=10" } }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", diff --git a/package.json b/package.json index fb1f081..7d466f1 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "react-icons": "5.3.0", "react-spinners": "0.13.8", "resend": "4.0.0", + "server-only": "0.0.1", "sonner": "1.5.0", "tailwind-merge": "2.5.2", "tailwindcss-animate": "1.0.7",