mirror of
				https://github.com/DerTyp7/f1r3wave-website.git
				synced 2025-10-30 21:47:09 +01:00 
			
		
		
		
	Removed next-auth and refactored authentication
This commit is contained in:
		| @@ -1,14 +1,14 @@ | ||||
| import ImageManager from '@/components/ImageManager'; | ||||
| import ImageUpload from '@/components/ImageUpload'; | ||||
| import { ImagesResponse, TagsResponse } from '@/interfaces/api'; | ||||
| import { getAuthStatus } from '@/lib/auth-utils'; | ||||
| import { getSession } from '@/lib/session'; | ||||
| import styles from '@/styles/AdminPage.module.scss'; | ||||
| import { redirect } from 'next/navigation'; | ||||
|  | ||||
| export default async function AdminPage() { | ||||
|   const { isAuthenticated } = await getAuthStatus(); | ||||
|   const session = await getSession(); | ||||
|  | ||||
|   if (!isAuthenticated) { | ||||
|   if (!session.isAuthenticated) { | ||||
|     redirect('/login'); | ||||
|   } | ||||
|  | ||||
|   | ||||
							
								
								
									
										15
									
								
								src/app/api/auth/login/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/app/api/auth/login/route.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import { getSession } from '@/lib/session'; | ||||
| import { NextResponse } from 'next/server'; | ||||
|  | ||||
| export async function POST(request: Request) { | ||||
|   const session = await getSession(); | ||||
|   const { password } = await request.json(); | ||||
|  | ||||
|   if (password === process.env.ADMIN_TOKEN) { | ||||
|     session.isAuthenticated = true; | ||||
|     await session.save(); | ||||
|     return NextResponse.json({ success: true }); | ||||
|   } else { | ||||
|     return NextResponse.json({ error: 'Invalid password' }, { status: 401 }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										10
									
								
								src/app/api/auth/logout/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/app/api/auth/logout/route.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import { getSession } from '@/lib/session'; | ||||
| import { NextResponse } from 'next/server'; | ||||
|  | ||||
| export async function POST() { | ||||
|   const session = await getSession(); | ||||
|   session.isAuthenticated = false; | ||||
|   await session.save(); | ||||
|  | ||||
|   return NextResponse.json({ success: true }); | ||||
| } | ||||
							
								
								
									
										7
									
								
								src/app/api/auth/status/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/app/api/auth/status/route.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| import { getSession } from '@/lib/session'; | ||||
| import { NextResponse } from 'next/server'; | ||||
|  | ||||
| export async function GET() { | ||||
|   const session = await getSession(); | ||||
|   return NextResponse.json({ isAuthenticated: session.isAuthenticated }); | ||||
| } | ||||
| @@ -1,9 +1,11 @@ | ||||
| import { isAuthenticated } from '@/lib/auth-utils'; | ||||
| import { deleteImageById } from '@/lib/data'; | ||||
| import { getSession } from '@/lib/session'; | ||||
| import { NextRequest, NextResponse } from 'next/server'; | ||||
|  | ||||
| export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) { | ||||
|   if (!(await isAuthenticated(request))) { | ||||
|   const session = await getSession(); | ||||
|  | ||||
|   if (!session.isAuthenticated) { | ||||
|     return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| import { imagesDir } from '@/const/api'; | ||||
| import { ImagesResponse } from '@/interfaces/api'; | ||||
| import { ImageMeta } from '@/interfaces/image'; | ||||
| import { isAuthenticated } from '@/lib/auth-utils'; | ||||
| import { addImage, getImageData, stringToTags } from '@/lib/data'; | ||||
| import { getSession } from '@/lib/session'; | ||||
| import { promises as fs } from 'fs'; | ||||
| import { NextRequest, NextResponse } from 'next/server'; | ||||
| import path from 'path'; | ||||
| import sharp from 'sharp'; | ||||
| import { v4 as uuidv4 } from 'uuid'; | ||||
|  | ||||
| @@ -52,8 +53,9 @@ export async function GET(request: NextRequest) { | ||||
| } | ||||
|  | ||||
| export async function POST(request: NextRequest) { | ||||
|   console.log('upload file', request); | ||||
|   if (!(await isAuthenticated(request))) { | ||||
|   const session = await getSession(); | ||||
|  | ||||
|   if (!session.isAuthenticated) { | ||||
|     return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); | ||||
|   } | ||||
|  | ||||
| @@ -83,6 +85,8 @@ export async function POST(request: NextRequest) { | ||||
|       await fs.mkdir(imagesDir, { recursive: true }); | ||||
|     } | ||||
|  | ||||
|     await fs.writeFile(path.join(imagesDir, uuidFilename), buffer); | ||||
|  | ||||
|     const imageInfo = await sharp(buffer).metadata(); | ||||
|  | ||||
|     const newImage: ImageMeta = { | ||||
| @@ -95,8 +99,9 @@ export async function POST(request: NextRequest) { | ||||
|     }; | ||||
|  | ||||
|     await addImage(newImage); | ||||
|     return NextResponse.json({ message: 'File deleted successfully' }, { status: 201 }); | ||||
|     return NextResponse.json({ message: 'File uploaded successfully' }, { status: 201 }); | ||||
|   } catch (error) { | ||||
|     console.error(error); | ||||
|     return NextResponse.json({ error: error }, { status: 500 }); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| import { isAuthenticated } from '@/lib/auth-utils'; | ||||
| import { stringToTags, updateTagsOfImageId } from '@/lib/data'; | ||||
| import { getSession } from '@/lib/session'; | ||||
| import { NextRequest, NextResponse } from 'next/server'; | ||||
|  | ||||
| export async function PUT(request: NextRequest, context: { params: Promise<{ imageId: string }> }) { | ||||
|   if (!(await isAuthenticated(request))) { | ||||
|   const session = await getSession(); | ||||
|  | ||||
|   if (!session.isAuthenticated) { | ||||
|     return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import LoginForm from '@/components/LoginForm'; | ||||
| import { getAuthStatus } from '@/lib/auth-utils'; | ||||
| import { getSession } from '@/lib/session'; | ||||
| import { redirect } from 'next/navigation'; | ||||
|  | ||||
| export default async function LoginPage() { | ||||
|   const { isAuthenticated } = await getAuthStatus(); | ||||
|   const session = await getSession(); | ||||
|  | ||||
|   if (isAuthenticated) { | ||||
|   if (session.isAuthenticated) { | ||||
|     redirect('/admin'); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,21 +0,0 @@ | ||||
| import type { NextAuthConfig } from 'next-auth'; | ||||
|  | ||||
| export const authConfig = { | ||||
|   pages: { | ||||
|     signIn: '/login', | ||||
|   }, | ||||
|   callbacks: { | ||||
|     authorized({ auth, request: { nextUrl } }) { | ||||
|       const isLoggedIn = !!auth?.user; | ||||
|  | ||||
|       const isOnAdminRoute = nextUrl.pathname.startsWith('/admin'); | ||||
|  | ||||
|       if (isOnAdminRoute) { | ||||
|         if (isLoggedIn) return true; | ||||
|         return false; | ||||
|       } | ||||
|       return true; | ||||
|     }, | ||||
|   }, | ||||
|   providers: [], | ||||
| } satisfies NextAuthConfig; | ||||
							
								
								
									
										30
									
								
								src/auth.ts
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								src/auth.ts
									
									
									
									
									
								
							| @@ -1,30 +0,0 @@ | ||||
| import { validateAdminToken } from '@/lib/auth-utils'; | ||||
| import NextAuth from 'next-auth'; | ||||
| import Credentials from 'next-auth/providers/credentials'; | ||||
| import { z } from 'zod'; | ||||
| import { authConfig } from './auth.config'; | ||||
|  | ||||
| export const { auth, signIn, signOut } = NextAuth({ | ||||
|   ...authConfig, | ||||
|   providers: [ | ||||
|     Credentials({ | ||||
|       async authorize(credentials) { | ||||
|         const parsedCredentials = z.object({ token: z.string().min(1) }).safeParse(credentials); | ||||
|         if (parsedCredentials.success) { | ||||
|           const { token } = parsedCredentials.data; | ||||
|           const isValidToken = await validateAdminToken(token); | ||||
|  | ||||
|           if (isValidToken) { | ||||
|             return { | ||||
|               id: 'admin-id', | ||||
|               name: 'Administrator', | ||||
|               role: 'admin', | ||||
|             }; | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|       }, | ||||
|     }), | ||||
|   ], | ||||
| }); | ||||
| @@ -1,30 +1,47 @@ | ||||
| 'use client'; | ||||
|  | ||||
| import logo from '@/assets/logo_text.png'; | ||||
| import { performLogout } from '@/lib/actions'; | ||||
| import { getAuthStatus } from '@/lib/auth-utils'; | ||||
| import { deleteSessionCookie } from '@/lib/actions'; | ||||
| import styles from '@/styles/Header.module.scss'; | ||||
| import { faInstagram } from '@fortawesome/free-brands-svg-icons'; | ||||
| import { faEnvelope } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; | ||||
| import Image from 'next/image'; | ||||
| import Link from 'next/link'; | ||||
| import { usePathname } from 'next/navigation'; | ||||
| import { usePathname, useRouter } from 'next/navigation'; | ||||
| import { useEffect, useState } from 'react'; | ||||
|  | ||||
| export default function Header() { | ||||
|   const pathname = usePathname(); | ||||
|   const router = useRouter(); | ||||
|   const [isAuth, setIsAuth] = useState<boolean>(false); | ||||
|   const isActive = (path: string) => pathname.split('/')[1] === path.split('/')[1]; | ||||
|  | ||||
|   const performLogout = async (): Promise<void> => { | ||||
|     await fetch('/api/auth/logout', { method: 'POST' }); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const auth = async () => { | ||||
|       const { isAuthenticated } = await getAuthStatus(); | ||||
|       const result = await fetch('/api/auth/status', { method: 'GET' }); | ||||
|       const isAuthenticated = (await result.json()).isAuthenticated; | ||||
|  | ||||
|       if (!isAuthenticated) { | ||||
|         await deleteSessionCookie(); | ||||
|       } | ||||
|  | ||||
|       setIsAuth(isAuthenticated); | ||||
|     }; | ||||
|     auth(); | ||||
|   }, [pathname]); | ||||
|  | ||||
|   const handleLogout = async () => { | ||||
|     await performLogout(); | ||||
|     setIsAuth(false); | ||||
|     router.push('/'); | ||||
|     router.refresh(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <header className={`${styles.header}`}> | ||||
|       <Link href="/" className={styles.logo}> | ||||
| @@ -41,7 +58,7 @@ export default function Header() { | ||||
|         {isAuth ? ( | ||||
|           <p | ||||
|             onClick={async () => { | ||||
|               performLogout(); | ||||
|               handleLogout(); | ||||
|             }} | ||||
|             className={`${styles.navLink}`}> | ||||
|             Logout | ||||
|   | ||||
| @@ -2,21 +2,45 @@ | ||||
|  | ||||
| import Button from '@/components/Button'; | ||||
| import InputField from '@/components/InputField'; | ||||
| import { authenticate } from '@/lib/actions'; | ||||
| import styles from '@/styles/LoginForm.module.scss'; | ||||
| import { useActionState } from 'react'; | ||||
| import { useRouter } from 'next/navigation'; | ||||
| import { useState } from 'react'; | ||||
|  | ||||
| export default function LoginForm() { | ||||
|   const [errorMessage, dispatch, isPending] = useActionState(authenticate, undefined); | ||||
|   const [isPending, setIsPending] = useState<boolean>(false); | ||||
|   const [password, setPassword] = useState<string>(''); | ||||
|   const [errorMessage, setErrorMessage] = useState<string>(''); | ||||
|   const router = useRouter(); | ||||
|  | ||||
|   const handleSubmit = async (e: React.FormEvent) => { | ||||
|     e.preventDefault(); | ||||
|     setIsPending(true); | ||||
|     const response = await fetch('/api/auth/login', { | ||||
|       method: 'POST', | ||||
|       headers: { 'Content-Type': 'application/json' }, | ||||
|       body: JSON.stringify({ password }), | ||||
|     }); | ||||
|  | ||||
|     if (response.ok) { | ||||
|       router.push('/admin'); | ||||
|       router.refresh(); | ||||
|     } else { | ||||
|       setErrorMessage((await response.json()).error ?? ''); | ||||
|       setIsPending(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <div className={styles.container}> | ||||
|       <form action={dispatch} className={styles.form}> | ||||
|       <form onSubmit={handleSubmit} className={styles.form}> | ||||
|         <span className={styles.errorMessage}>{errorMessage}</span> | ||||
|         <InputField | ||||
|           label="Token" | ||||
|           name="token" | ||||
|           type="password" | ||||
|           onValueChange={(v) => { | ||||
|             setPassword(v.currentTarget.value); | ||||
|           }} | ||||
|           required={true} | ||||
|           placeholder="Enter your token" | ||||
|           disabled={isPending} | ||||
|   | ||||
| @@ -1,35 +1,8 @@ | ||||
| 'use server'; | ||||
|  | ||||
| import { signIn, signOut } from '@/auth'; | ||||
| import { AuthError } from 'next-auth'; | ||||
| import { redirect } from 'next/navigation'; | ||||
| import { cookies } from 'next/headers'; | ||||
|  | ||||
| export async function authenticate(_prevState: string | undefined, formData: FormData): Promise<string> { | ||||
|   try { | ||||
|     const result = await signIn('credentials', { | ||||
|       redirect: false, | ||||
|       token: formData.get('token'), | ||||
|     }); | ||||
|  | ||||
|     if (result?.error) { | ||||
|       return 'Invalid token'; | ||||
|     } | ||||
|  | ||||
|     redirect('/admin'); | ||||
|   } catch (error) { | ||||
|     if (error instanceof AuthError) { | ||||
|       switch (error.type) { | ||||
|         case 'CredentialsSignin': | ||||
|           return 'Invalid token'; | ||||
|         default: | ||||
|           return 'Something went wrong'; | ||||
|       } | ||||
|     } | ||||
|     throw error; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export async function performLogout() { | ||||
|   await signOut({ redirectTo: '/', redirect: true }); | ||||
|   redirect('/'); | ||||
| export async function deleteSessionCookie() { | ||||
|   const cookieStore = await cookies(); | ||||
|   cookieStore.delete('session'); | ||||
| } | ||||
|   | ||||
| @@ -1,30 +0,0 @@ | ||||
| 'use server'; | ||||
|  | ||||
| import { getToken } from 'next-auth/jwt'; | ||||
| import { NextRequest } from 'next/server'; | ||||
|  | ||||
| export async function validateAdminToken(token: string): Promise<boolean> { | ||||
|   return token === process.env.ADMIN_TOKEN; | ||||
| } | ||||
|  | ||||
| export async function getAuthStatus() { | ||||
|   const { auth } = await import('@/auth'); | ||||
|   const session = await auth(); | ||||
|  | ||||
|   return { | ||||
|     isAuthenticated: !!session?.user, | ||||
|     user: session?.user, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export async function isAuthenticated(request: NextRequest): Promise<boolean> { | ||||
|   console.log('isAuthenticated'); | ||||
|   const token = await getToken({ req: request, secret: process.env.AUTH_SECRET }); | ||||
|   console.log('token', token); | ||||
|   if (!token || (token.exp && Date.now() / 1000 > token.exp)) { | ||||
|     console.log('false'); | ||||
|     return false; | ||||
|   } | ||||
|   console.log('true'); | ||||
|   return true; | ||||
| } | ||||
							
								
								
									
										24
									
								
								src/lib/session.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/lib/session.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| import { getIronSession, SessionOptions } from 'iron-session'; | ||||
| import { cookies } from 'next/headers'; | ||||
|  | ||||
| export interface SessionData { | ||||
|   isAuthenticated: boolean; | ||||
| } | ||||
|  | ||||
| export const sessionOptions: SessionOptions = { | ||||
|   password: process.env.AUTH_SECRET!, | ||||
|   cookieName: 'session', | ||||
|   cookieOptions: { | ||||
|     httpOnly: true, | ||||
|     secure: process.env.NODE_ENV === 'production', | ||||
|     maxAge: 60 * 60 * 24 * 7, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export async function getSession() { | ||||
|   const session = await getIronSession<SessionData>(await cookies(), sessionOptions); | ||||
|   if (!session.isAuthenticated) { | ||||
|     session.isAuthenticated = false; | ||||
|   } | ||||
|   return session; | ||||
| } | ||||
| @@ -1,8 +0,0 @@ | ||||
| import NextAuth from 'next-auth'; | ||||
| import { authConfig } from './auth.config'; | ||||
|  | ||||
| export default NextAuth(authConfig).auth; | ||||
|  | ||||
| export const config = { | ||||
|   matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user