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

143
package-lock.json generated
View File

@@ -13,8 +13,8 @@
"@fortawesome/free-solid-svg-icons": "^7.0.1",
"@fortawesome/react-fontawesome": "^3.0.2",
"bcryptjs": "^3.0.2",
"iron-session": "^8.0.4",
"next": "^15.5.4",
"next-auth": "^5.0.0-beta.29",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-google-recaptcha": "^3.1.0",
@@ -40,35 +40,6 @@
"typescript": "^5"
}
},
"node_modules/@auth/core": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.40.0.tgz",
"integrity": "sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==",
"license": "ISC",
"dependencies": {
"@panva/hkdf": "^1.2.1",
"jose": "^6.0.6",
"oauth4webapi": "^3.3.0",
"preact": "10.24.3",
"preact-render-to-string": "6.5.11"
},
"peerDependencies": {
"@simplewebauthn/browser": "^9.0.1",
"@simplewebauthn/server": "^9.0.2",
"nodemailer": "^6.8.0"
},
"peerDependenciesMeta": {
"@simplewebauthn/browser": {
"optional": true
},
"@simplewebauthn/server": {
"optional": true
},
"nodemailer": {
"optional": true
}
}
},
"node_modules/@emnapi/core": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
@@ -1043,15 +1014,6 @@
"node": ">=12.4.0"
}
},
"node_modules/@panva/hkdf": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/@parcel/watcher": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
@@ -2560,6 +2522,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3963,6 +3934,30 @@
"node": ">= 0.4"
}
},
"node_modules/iron-session": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/iron-session/-/iron-session-8.0.4.tgz",
"integrity": "sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA==",
"funding": [
"https://github.com/sponsors/vvo",
"https://github.com/sponsors/brc-dd"
],
"license": "MIT",
"dependencies": {
"cookie": "^0.7.2",
"iron-webcrypto": "^1.2.1",
"uncrypto": "^0.1.3"
}
},
"node_modules/iron-webcrypto": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
"integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/brc-dd"
}
},
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -4413,15 +4408,6 @@
"node": ">= 0.4"
}
},
"node_modules/jose": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.0.tgz",
"integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4806,33 +4792,6 @@
}
}
},
"node_modules/next-auth": {
"version": "5.0.0-beta.29",
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.29.tgz",
"integrity": "sha512-Ukpnuk3NMc/LiOl32njZPySk7pABEzbjhMUFd5/n10I0ZNC7NCuVv8IY2JgbDek2t/PUOifQEoUiOOTLy4os5A==",
"license": "ISC",
"dependencies": {
"@auth/core": "0.40.0"
},
"peerDependencies": {
"@simplewebauthn/browser": "^9.0.1",
"@simplewebauthn/server": "^9.0.2",
"next": "^14.0.0-0 || ^15.0.0-0",
"nodemailer": "^6.6.5",
"react": "^18.2.0 || ^19.0.0-0"
},
"peerDependenciesMeta": {
"@simplewebauthn/browser": {
"optional": true
},
"@simplewebauthn/server": {
"optional": true
},
"nodemailer": {
"optional": true
}
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
@@ -4840,15 +4799,6 @@
"license": "MIT",
"optional": true
},
"node_modules/oauth4webapi": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.1.tgz",
"integrity": "sha512-olkZDELNycOWQf9LrsELFq8n05LwJgV8UkrS0cburk6FOwf8GvLam+YB+Uj5Qvryee+vwWOfQVeI5Vm0MVg7SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -5166,25 +5116,6 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/preact": {
"version": "10.24.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/preact-render-to-string": {
"version": "6.5.11",
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz",
"integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==",
"license": "MIT",
"peerDependencies": {
"preact": ">=10"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -6673,6 +6604,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/uncrypto": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",

View File

@@ -14,8 +14,8 @@
"@fortawesome/free-solid-svg-icons": "^7.0.1",
"@fortawesome/react-fontawesome": "^3.0.2",
"bcryptjs": "^3.0.2",
"iron-session": "^8.0.4",
"next": "^15.5.4",
"next-auth": "^5.0.0-beta.29",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-google-recaptcha": "^3.1.0",

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