From 434536d1f265ad9e7d14bf786672b3718c6a966c Mon Sep 17 00:00:00 2001 From: Nareshkumar Rao Date: Fri, 4 Apr 2025 23:29:57 +0200 Subject: [PATCH] use authentik for auth --- package-lock.json | 149 ++++++++++++++++++++---- package.json | 3 +- src/app/api/auth/[...nextauth]/route.ts | 2 + src/app/blog/[[...tag]]/page.tsx | 4 +- src/app/blog/login/Form.tsx | 74 ------------ src/app/blog/login/action.ts | 27 ----- src/app/blog/login/page.tsx | 12 -- src/app/blog/post/[slug]/page.tsx | 4 +- src/app/blog/write/[[...slug]]/page.tsx | 8 +- src/app/blog/write/action.ts | 5 + src/auth.ts | 6 + src/components/auth.ts | 49 -------- 12 files changed, 151 insertions(+), 192 deletions(-) create mode 100644 src/app/api/auth/[...nextauth]/route.ts delete mode 100644 src/app/blog/login/Form.tsx delete mode 100644 src/app/blog/login/action.ts delete mode 100644 src/app/blog/login/page.tsx create mode 100644 src/auth.ts delete mode 100644 src/components/auth.ts diff --git a/package-lock.json b/package-lock.json index 81c8ef2..807005e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,9 @@ "argon2": "^0.41.1", "cheerio": "^1.0.0", "highlight.js": "^11.11.1", - "jose": "^6.0.10", - "lucide-react": "^0.485.0", "markdown-it": "^14.1.0", "next": "15.2.4", + "next-auth": "^5.0.0-beta.25", "nodemailer": "^6.10.0", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -38,6 +37,46 @@ "typescript": "^5" } }, + "node_modules/@auth/core": { + "version": "0.37.2", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.2.tgz", + "integrity": "sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "@types/cookie": "0.6.0", + "cookie": "0.7.1", + "jose": "^5.9.3", + "oauth4webapi": "^3.0.0", + "preact": "10.11.3", + "preact-render-to-string": "5.2.3" + }, + "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/@auth/core/node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@babel/runtime": { "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", @@ -862,6 +901,15 @@ "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/@phc/format": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", @@ -933,6 +981,12 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -2028,6 +2082,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "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", @@ -3875,15 +3938,6 @@ "node": ">= 0.4" } }, - "node_modules/jose": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.10.tgz", - "integrity": "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==", - "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", @@ -4041,15 +4095,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lucide-react": { - "version": "0.485.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.485.0.tgz", - "integrity": "sha512-NvyQJ0LKyyCxL23nPKESlr/jmz8r7fJO1bkuptSNYSy0s8VVj4ojhX0YAgmE1e0ewfxUZjIlZpvH+otfTnla8Q==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/markdown-it": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", @@ -4216,6 +4261,33 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.25", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.25.tgz", + "integrity": "sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.37.2" + }, + "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": "8.3.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", @@ -4257,6 +4329,15 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/oauth4webapi": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.4.0.tgz", + "integrity": "sha512-5lcbectYuzQHvh0Ni7Epvc13sMVq7BxWUlHEYHaNko64OA1hcats0Huq30vZjqCZULcVE/PZxAGGPansfRAWKQ==", + "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", @@ -4581,6 +4662,28 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", + "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", + "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", + "license": "MIT", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4591,6 +4694,12 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/package.json b/package.json index e0b345f..541fbae 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,9 @@ "argon2": "^0.41.1", "cheerio": "^1.0.0", "highlight.js": "^11.11.1", - "jose": "^6.0.10", - "lucide-react": "^0.485.0", "markdown-it": "^14.1.0", "next": "15.2.4", + "next-auth": "^5.0.0-beta.25", "nodemailer": "^6.10.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..7c62e2d --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,2 @@ +import { handlers } from "@/auth"; +export const { GET, POST } = handlers; diff --git a/src/app/blog/[[...tag]]/page.tsx b/src/app/blog/[[...tag]]/page.tsx index bf07807..6cee023 100644 --- a/src/app/blog/[[...tag]]/page.tsx +++ b/src/app/blog/[[...tag]]/page.tsx @@ -7,8 +7,8 @@ import React from "react"; import PostSummary from "./PostSummary"; import Pagination from "./Pagination"; import TagOverview from "./TagOverview"; -import { isLoggedIn } from "@/components/auth"; import ActionButtons from "./ActionButtons"; +import { auth } from "@/auth"; export default async function Blog({ params, @@ -31,7 +31,7 @@ export default async function Blog({ if (pageNumber > numberOfPages) { notFound(); } - const loggedIn = await isLoggedIn(); + const loggedIn = (await auth()) != null; const tags = await getTags(loggedIn); return ( diff --git a/src/app/blog/login/Form.tsx b/src/app/blog/login/Form.tsx deleted file mode 100644 index 1c94e86..0000000 --- a/src/app/blog/login/Form.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client"; - -import Form from "next/form"; -import { handleLogin } from "./action"; - -export default function FormComponent() { - return ( -
-
-
- Username: - -
-
- Password: - -
-
- -
-
-
- ); -} diff --git a/src/app/blog/login/action.ts b/src/app/blog/login/action.ts deleted file mode 100644 index a37e752..0000000 --- a/src/app/blog/login/action.ts +++ /dev/null @@ -1,27 +0,0 @@ -"use server"; - -import { PrismaClient } from "@prisma/client"; -import argon2 from "argon2"; -import { setSession } from "@/components/auth"; -import { redirect, RedirectType } from "next/navigation"; - -export async function handleLogin(data: FormData) { - const prisma = new PrismaClient(); - const username = data.get("username")?.toString(); - const password = data.get("password")?.toString(); - if (!username || !password) { - throw new Error("Missing username or password"); - } - - const user = await prisma.user.findUnique({ where: { username } }); - if (!user) { - redirect("/blog/login?error=Invalid%20credentials", RedirectType.replace); - } - - if (await argon2.verify(user.password, password)) { - setSession(); - redirect("/blog/write", RedirectType.replace); - } else { - redirect("/blog/login?error=Invalid%20credentials", RedirectType.replace); - } -} diff --git a/src/app/blog/login/page.tsx b/src/app/blog/login/page.tsx deleted file mode 100644 index c182512..0000000 --- a/src/app/blog/login/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -import { isLoggedIn } from "@/components/auth"; -import FormComponent from "./Form"; - -export default async function Login() { - if (await isLoggedIn()) { - redirect("/blog/write"); - } - return ; -} diff --git a/src/app/blog/post/[slug]/page.tsx b/src/app/blog/post/[slug]/page.tsx index 1bd185b..1e31b7d 100644 --- a/src/app/blog/post/[slug]/page.tsx +++ b/src/app/blog/post/[slug]/page.tsx @@ -1,7 +1,7 @@ import { notFound } from "next/navigation"; import { getPost } from "../../action"; import PostDisplay from "./PostDisplay"; -import { isLoggedIn } from "@/components/auth"; +import { auth } from "@/auth"; export default async function Post({ params, @@ -13,7 +13,7 @@ export default async function Post({ if (!post) { notFound(); } - const loggedIn = await isLoggedIn(); + const loggedIn = (await auth()) != null; return ( <> diff --git a/src/app/blog/write/[[...slug]]/page.tsx b/src/app/blog/write/[[...slug]]/page.tsx index 35be1b0..0065ee2 100644 --- a/src/app/blog/write/[[...slug]]/page.tsx +++ b/src/app/blog/write/[[...slug]]/page.tsx @@ -1,16 +1,16 @@ -import { notFound, redirect } from "next/navigation"; +import { notFound } from "next/navigation"; import { Post } from "../../types"; import Write from "../Write"; -import { isLoggedIn } from "@/components/auth"; import { PrismaClient } from "@prisma/client"; +import { auth, signIn } from "@/auth"; export default async function WritePage({ params, }: { params: Promise<{ slug: string[] | undefined }>; }) { - if (!(await isLoggedIn())) { - redirect("/blog/login"); + if (!(await auth())) { + signIn(); } const slug = (await params).slug?.[0]; diff --git a/src/app/blog/write/action.ts b/src/app/blog/write/action.ts index 377cc55..d3d60e0 100644 --- a/src/app/blog/write/action.ts +++ b/src/app/blog/write/action.ts @@ -5,6 +5,7 @@ import MarkdownIt from "markdown-it"; import hljs from "highlight.js"; import * as cheerio from "cheerio"; import slugify from "slugify"; +import { auth } from "@/auth"; export async function savePostServer( title: string, @@ -13,6 +14,10 @@ export async function savePostServer( is_draft: boolean, existingSlug?: string ) { + if (!(await auth())) { + throw new Error("Not authenticated"); + } + const prisma = new PrismaClient(); const md = new MarkdownIt({ diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..eddac51 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,6 @@ +import NextAuth from "next-auth"; +import Authentik from "next-auth/providers/authentik"; + +export const { handlers, signIn, signOut, auth } = NextAuth({ + providers: [Authentik], +}); diff --git a/src/components/auth.ts b/src/components/auth.ts deleted file mode 100644 index 3d110de..0000000 --- a/src/components/auth.ts +++ /dev/null @@ -1,49 +0,0 @@ -"use server"; - -import { jwtVerify, SignJWT } from "jose"; -import { cookies } from "next/headers"; -const SECRET_KEY = process.env.SESSION_SECRET; -const encodedKey = new TextEncoder().encode(SECRET_KEY); - -export type SessionPayload = { admin: true }; - -export async function encrypt(payload: SessionPayload) { - return new SignJWT(payload) - .setProtectedHeader({ alg: "HS256" }) - .setIssuedAt() - .setExpirationTime("7d") - .sign(encodedKey); -} - -export async function decrypt( - token: string | undefined = "" -): Promise { - try { - const { payload } = await jwtVerify(token, encodedKey, { - algorithms: ["HS256"], - }); - return payload as SessionPayload; - } catch { - return null; - } -} - -export async function isLoggedIn(): Promise { - const cookieStore = (await cookies()).get("session")?.value; - const session = await decrypt(cookieStore); - if (session != null && session.admin) { - return true; - } - return false; -} - -export async function setSession(): Promise { - const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); - (await cookies()).set("session", await encrypt({ admin: true }), { - httpOnly: true, - secure: true, - expires: expiresAt, - sameSite: "lax", - path: "/", - }); -}