Removed next-auth and refactored authentication

This commit is contained in:
2025-10-09 14:04:34 +02:00
parent a12d607565
commit 67e986ab07
18 changed files with 174 additions and 247 deletions

View File

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

View 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 });
}
}

View 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 });
}

View 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 });
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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$).*)'],
};