mirror of
https://github.com/DerTyp7/f1r3wave-website.git
synced 2025-10-29 04:52:08 +01:00
Removed next-auth and refactored authentication
This commit is contained in:
143
package-lock.json
generated
143
package-lock.json
generated
@@ -13,8 +13,8 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^7.0.1",
|
"@fortawesome/free-solid-svg-icons": "^7.0.1",
|
||||||
"@fortawesome/react-fontawesome": "^3.0.2",
|
"@fortawesome/react-fontawesome": "^3.0.2",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
|
"iron-session": "^8.0.4",
|
||||||
"next": "^15.5.4",
|
"next": "^15.5.4",
|
||||||
"next-auth": "^5.0.0-beta.29",
|
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-google-recaptcha": "^3.1.0",
|
"react-google-recaptcha": "^3.1.0",
|
||||||
@@ -40,35 +40,6 @@
|
|||||||
"typescript": "^5"
|
"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": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
|
||||||
@@ -1043,15 +1014,6 @@
|
|||||||
"node": ">=12.4.0"
|
"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": {
|
"node_modules/@parcel/watcher": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||||
@@ -2560,6 +2522,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -3963,6 +3934,30 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -4413,15 +4408,6 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"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": {
|
"node_modules/node-addon-api": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||||
@@ -4840,15 +4799,6 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"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": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -5166,25 +5116,6 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"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": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@@ -6673,6 +6604,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^7.0.1",
|
"@fortawesome/free-solid-svg-icons": "^7.0.1",
|
||||||
"@fortawesome/react-fontawesome": "^3.0.2",
|
"@fortawesome/react-fontawesome": "^3.0.2",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
|
"iron-session": "^8.0.4",
|
||||||
"next": "^15.5.4",
|
"next": "^15.5.4",
|
||||||
"next-auth": "^5.0.0-beta.29",
|
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-google-recaptcha": "^3.1.0",
|
"react-google-recaptcha": "^3.1.0",
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import ImageManager from '@/components/ImageManager';
|
import ImageManager from '@/components/ImageManager';
|
||||||
import ImageUpload from '@/components/ImageUpload';
|
import ImageUpload from '@/components/ImageUpload';
|
||||||
import { ImagesResponse, TagsResponse } from '@/interfaces/api';
|
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 styles from '@/styles/AdminPage.module.scss';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
export default async function AdminPage() {
|
export default async function AdminPage() {
|
||||||
const { isAuthenticated } = await getAuthStatus();
|
const session = await getSession();
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!session.isAuthenticated) {
|
||||||
redirect('/login');
|
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 { deleteImageById } from '@/lib/data';
|
||||||
|
import { getSession } from '@/lib/session';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) {
|
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 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { imagesDir } from '@/const/api';
|
import { imagesDir } from '@/const/api';
|
||||||
import { ImagesResponse } from '@/interfaces/api';
|
import { ImagesResponse } from '@/interfaces/api';
|
||||||
import { ImageMeta } from '@/interfaces/image';
|
import { ImageMeta } from '@/interfaces/image';
|
||||||
import { isAuthenticated } from '@/lib/auth-utils';
|
|
||||||
import { addImage, getImageData, stringToTags } from '@/lib/data';
|
import { addImage, getImageData, stringToTags } from '@/lib/data';
|
||||||
|
import { getSession } from '@/lib/session';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import path from 'path';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
@@ -52,8 +53,9 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
console.log('upload file', request);
|
const session = await getSession();
|
||||||
if (!(await isAuthenticated(request))) {
|
|
||||||
|
if (!session.isAuthenticated) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
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.mkdir(imagesDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(imagesDir, uuidFilename), buffer);
|
||||||
|
|
||||||
const imageInfo = await sharp(buffer).metadata();
|
const imageInfo = await sharp(buffer).metadata();
|
||||||
|
|
||||||
const newImage: ImageMeta = {
|
const newImage: ImageMeta = {
|
||||||
@@ -95,8 +99,9 @@ export async function POST(request: NextRequest) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await addImage(newImage);
|
await addImage(newImage);
|
||||||
return NextResponse.json({ message: 'File deleted successfully' }, { status: 201 });
|
return NextResponse.json({ message: 'File uploaded successfully' }, { status: 201 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(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 { stringToTags, updateTagsOfImageId } from '@/lib/data';
|
||||||
|
import { getSession } from '@/lib/session';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
export async function PUT(request: NextRequest, context: { params: Promise<{ imageId: string }> }) {
|
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 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import LoginForm from '@/components/LoginForm';
|
import LoginForm from '@/components/LoginForm';
|
||||||
import { getAuthStatus } from '@/lib/auth-utils';
|
import { getSession } from '@/lib/session';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
export default async function LoginPage() {
|
export default async function LoginPage() {
|
||||||
const { isAuthenticated } = await getAuthStatus();
|
const session = await getSession();
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (session.isAuthenticated) {
|
||||||
redirect('/admin');
|
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';
|
'use client';
|
||||||
|
|
||||||
import logo from '@/assets/logo_text.png';
|
import logo from '@/assets/logo_text.png';
|
||||||
import { performLogout } from '@/lib/actions';
|
import { deleteSessionCookie } from '@/lib/actions';
|
||||||
import { getAuthStatus } from '@/lib/auth-utils';
|
|
||||||
import styles from '@/styles/Header.module.scss';
|
import styles from '@/styles/Header.module.scss';
|
||||||
import { faInstagram } from '@fortawesome/free-brands-svg-icons';
|
import { faInstagram } from '@fortawesome/free-brands-svg-icons';
|
||||||
import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
|
import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
const [isAuth, setIsAuth] = useState<boolean>(false);
|
const [isAuth, setIsAuth] = useState<boolean>(false);
|
||||||
const isActive = (path: string) => pathname.split('/')[1] === path.split('/')[1];
|
const isActive = (path: string) => pathname.split('/')[1] === path.split('/')[1];
|
||||||
|
|
||||||
|
const performLogout = async (): Promise<void> => {
|
||||||
|
await fetch('/api/auth/logout', { method: 'POST' });
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const auth = async () => {
|
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);
|
setIsAuth(isAuthenticated);
|
||||||
};
|
};
|
||||||
auth();
|
auth();
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await performLogout();
|
||||||
|
setIsAuth(false);
|
||||||
|
router.push('/');
|
||||||
|
router.refresh();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={`${styles.header}`}>
|
<header className={`${styles.header}`}>
|
||||||
<Link href="/" className={styles.logo}>
|
<Link href="/" className={styles.logo}>
|
||||||
@@ -41,7 +58,7 @@ export default function Header() {
|
|||||||
{isAuth ? (
|
{isAuth ? (
|
||||||
<p
|
<p
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
performLogout();
|
handleLogout();
|
||||||
}}
|
}}
|
||||||
className={`${styles.navLink}`}>
|
className={`${styles.navLink}`}>
|
||||||
Logout
|
Logout
|
||||||
|
|||||||
@@ -2,21 +2,45 @@
|
|||||||
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import InputField from '@/components/InputField';
|
import InputField from '@/components/InputField';
|
||||||
import { authenticate } from '@/lib/actions';
|
|
||||||
import styles from '@/styles/LoginForm.module.scss';
|
import styles from '@/styles/LoginForm.module.scss';
|
||||||
import { useActionState } from 'react';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
export default function LoginForm() {
|
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 (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<form action={dispatch} className={styles.form}>
|
<form onSubmit={handleSubmit} className={styles.form}>
|
||||||
<span className={styles.errorMessage}>{errorMessage}</span>
|
<span className={styles.errorMessage}>{errorMessage}</span>
|
||||||
<InputField
|
<InputField
|
||||||
label="Token"
|
label="Token"
|
||||||
name="token"
|
name="token"
|
||||||
type="password"
|
type="password"
|
||||||
|
onValueChange={(v) => {
|
||||||
|
setPassword(v.currentTarget.value);
|
||||||
|
}}
|
||||||
required={true}
|
required={true}
|
||||||
placeholder="Enter your token"
|
placeholder="Enter your token"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
|
|||||||
@@ -1,35 +1,8 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { signIn, signOut } from '@/auth';
|
import { cookies } from 'next/headers';
|
||||||
import { AuthError } from 'next-auth';
|
|
||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
export async function authenticate(_prevState: string | undefined, formData: FormData): Promise<string> {
|
export async function deleteSessionCookie() {
|
||||||
try {
|
const cookieStore = await cookies();
|
||||||
const result = await signIn('credentials', {
|
cookieStore.delete('session');
|
||||||
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('/');
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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