From ddcb14e5648db51899fde4f8fc65504539fe2456 Mon Sep 17 00:00:00 2001 From: Dominik Stahl Date: Mon, 12 May 2025 21:17:46 +0200 Subject: [PATCH 1/5] refactor: move auth check into pages --- src/app/page.tsx | 10 ++++++++-- src/auth.ts | 5 ++++- src/middleware.ts | 16 +--------------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 7bcd29e..a86e576 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,9 @@ -export default function Home() { - return
; +import { auth } from '@/auth'; +import { redirect } from 'next/navigation'; + +export default async function Home() { + const session = await auth(); + + if (!session?.user) redirect('/login'); + else redirect('/home'); } diff --git a/src/auth.ts b/src/auth.ts index 50b654c..b3ed869 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -7,7 +7,10 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ ), callbacks: { authorized: async ({ auth }) => { - return !!auth; + return !!auth?.user; }, }, + pages: { + signIn: '/login', + }, }); diff --git a/src/middleware.ts b/src/middleware.ts index 175bedc..345d04d 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,18 +1,4 @@ -import { auth } from '@/auth'; - -export default auth((req) => { - if ( - !req.auth && - req.nextUrl.pathname !== '/login' && - process.env.MEETUP_SKIP_LOGIN !== 'true' - ) { - const newUrl = new URL('/login', req.nextUrl.origin); - return Response.redirect(newUrl); - } else if (req.auth != null && req.nextUrl.pathname === '/') { - const newUrl = new URL('/home', req.nextUrl.origin); - return Response.redirect(newUrl); - } -}); +export { auth as middleware } from '@/auth'; export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], From 20eb6ae04acda8cd851f6152c13fa421d092fc23 Mon Sep 17 00:00:00 2001 From: Dominik Stahl Date: Mon, 12 May 2025 21:22:56 +0200 Subject: [PATCH 2/5] refactor: dynamically generated login page --- src/app/login/page.tsx | 14 +++++---- src/auth.ts | 47 +++++++++++++++++++++++++----- src/components/labeled-input.tsx | 9 +++--- src/components/user/login-form.tsx | 22 +++++++++++++- 4 files changed, 75 insertions(+), 17 deletions(-) diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 714e996..1786e82 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,4 +1,4 @@ -import { auth } from '@/auth'; +import { auth, providerMap } from '@/auth'; import SSOLogin from '@/components/user/sso-login-button'; import LoginForm from '@/components/user/login-form'; import { redirect } from 'next/navigation'; @@ -35,11 +35,15 @@ export default async function LoginPage() { -
+ {providerMap.length > 0 &&
} - {process.env.AUTH_AUTHENTIK_ISSUER && ( - - )} + {providerMap.map((provider) => ( + + ))}
diff --git a/src/auth.ts b/src/auth.ts index b3ed869..09a5065 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,16 +1,49 @@ import NextAuth from 'next-auth'; + +import type { Provider } from 'next-auth/providers'; +import Credentials from 'next-auth/providers/credentials'; + import Authentik from 'next-auth/providers/authentik'; +const providers: Provider[] = [ + !process.env.DISABLE_PASSWORD_LOGIN && + Credentials({ + credentials: { password: { label: 'Password', type: 'password' } }, + authorize(c) { + if (c.password !== 'password') return null; + return { + id: 'test', + name: 'Test User', + email: 'test@example.com', + }; + }, + }), + process.env.AUTH_AUTHENTIK_ID && Authentik, +].filter(Boolean) as Provider[]; + +export const providerMap = providers + .map((provider) => { + if (typeof provider === 'function') { + const providerData = provider(); + return { id: providerData.id, name: providerData.name }; + } else { + return { id: provider.id, name: provider.name }; + } + }) + .filter((provider) => provider.id !== 'credentials'); + export const { handlers, signIn, signOut, auth } = NextAuth({ - providers: [process.env.AUTH_AUTHENTIK_ISSUER ? Authentik : null].filter( - (x) => x !== null, - ), - callbacks: { - authorized: async ({ auth }) => { - return !!auth?.user; - }, + providers, + session: { + strategy: 'jwt', }, pages: { signIn: '/login', + signOut: '/logout', + }, + callbacks: { + authorized({ auth }) { + return !!auth?.user; + }, }, }); diff --git a/src/components/labeled-input.tsx b/src/components/labeled-input.tsx index 7b4768a..7a416d4 100644 --- a/src/components/labeled-input.tsx +++ b/src/components/labeled-input.tsx @@ -6,23 +6,24 @@ export default function LabeledInput({ label, placeholder, value, + name, }: { type: 'text' | 'email' | 'password'; label: string; placeholder?: string; value?: string; + name?: string; }) { - const elementId = Math.random().toString(36).substring(2, 15); - return (
- +
); diff --git a/src/components/user/login-form.tsx b/src/components/user/login-form.tsx index 20438e8..2869930 100644 --- a/src/components/user/login-form.tsx +++ b/src/components/user/login-form.tsx @@ -1,18 +1,38 @@ +import { signIn } from '@/auth'; import LabeledInput from '@/components/labeled-input'; import { Button } from '@/components/ui/button'; +import { AuthError } from 'next-auth'; +import { redirect } from 'next/navigation'; + +const SIGNIN_ERROR_URL = '/error'; export default function LoginForm() { return ( -
+ { + 'use server'; + try { + await signIn('credentials', formData); + } catch (error) { + if (error instanceof AuthError) { + return redirect(`${SIGNIN_ERROR_URL}?error=${error.type}`); + } + throw error; + } + }} + > + + + + + ); +} From 4f974a0b70380a4bed8d45e455600ec08c08efb1 Mon Sep 17 00:00:00 2001 From: Dominik Stahl Date: Mon, 12 May 2025 21:27:18 +0200 Subject: [PATCH 4/5] docs: test user for development --- .env.example | 2 -- README.md | 13 +++++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index e509a47..6f53284 100644 --- a/.env.example +++ b/.env.example @@ -7,5 +7,3 @@ AUTH_AUTHENTIK_SECRET= AUTH_AUTHENTIK_ISSUER= NEXT_PUBLIC_APP_URL= - -MEETUP_SKIP_LOGIN= \ No newline at end of file diff --git a/README.md b/README.md index 8a71828..1fbb7bf 100644 --- a/README.md +++ b/README.md @@ -94,10 +94,6 @@ This project is built with a modern tech stack: # Base URL of your application NEXT_PUBLIC_APP_URL="http://localhost:3000" - - # Development: Skip login flow (set to "true" to bypass authentication) - # Ensure this is NOT set to "true" in production. - MEETUP_SKIP_LOGIN="false" ``` 4. **Apply database migrations (Prisma):** @@ -111,11 +107,20 @@ This project is built with a modern tech stack: - (Optional: If you need to generate Prisma Client without running migrations, use `npx prisma generate`) 5. **Run the development server:** + ```bash yarn dev ``` + Open [http://localhost:3000](http://localhost:3000) in your browser to see the application. + The test user for the application is: + + ```bash + email: test@example.com + password: password + ``` + **Self-Hosting with Docker (Planned):** - A Docker image and `docker-compose.yml` file will be provided in the future to allow for easy self-hosting of the MeetUP application. This setup will also include database services. Instructions will be updated here once available. From aca229b3d33c552c0a89040f82dd6ba45e9acdbc Mon Sep 17 00:00:00 2001 From: Dominik Stahl Date: Mon, 12 May 2025 21:29:05 +0200 Subject: [PATCH 5/5] refactor: removes unused sso logout button --- src/components/user/sso-logout-button.tsx | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 src/components/user/sso-logout-button.tsx diff --git a/src/components/user/sso-logout-button.tsx b/src/components/user/sso-logout-button.tsx deleted file mode 100644 index 84c4bae..0000000 --- a/src/components/user/sso-logout-button.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { signOut } from '@/auth'; -import { IconButton } from '@/components/icon-button'; -import { faDoorOpen } from '@fortawesome/free-solid-svg-icons'; - -export function Logout() { - return ( -
{ - 'use server'; - await signOut({ redirectTo: '/login' }); - }} - > - - Sign Out - -
- ); -}