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. diff --git a/package.json b/package.json index 8286dc4..2cdd489 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,8 @@ "@eslint/eslintrc": "3.3.1", "@tailwindcss/postcss": "4.1.6", "@types/node": "22.15.17", - "@types/react": "19.1.3", - "@types/react-dom": "19.1.4", + "@types/react": "19.1.4", + "@types/react-dom": "19.1.5", "cypress": "14.3.3", "dotenv-cli": "^8.0.0", "eslint": "9.26.0", diff --git a/src/app/home/page.tsx b/src/app/home/page.tsx index c61abdd..4e6773b 100644 --- a/src/app/home/page.tsx +++ b/src/app/home/page.tsx @@ -1,4 +1,3 @@ -import { Logout } from '@/components/user/sso-logout-button'; import { RedirectButton } from '@/components/user/redirect-button'; import { ThemePicker } from '@/components/user/theme-picker'; @@ -8,7 +7,7 @@ export default function Home() {
{}

Home

- +
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 6b74310..34d1b07 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'; @@ -37,15 +37,16 @@ export default async function LoginPage() { -
+ {providerMap.length > 0 &&
} - {process.env.AUTH_AUTHENTIK_ISSUER && ( + {providerMap.map((provider) => ( - )} + ))}
diff --git a/src/app/logout/page.tsx b/src/app/logout/page.tsx new file mode 100644 index 0000000..15f29aa --- /dev/null +++ b/src/app/logout/page.tsx @@ -0,0 +1,40 @@ +import { signOut } from '@/auth'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; + +export default function SignOutPage() { + return ( +
+
{ + 'use server'; + await signOut({ redirectTo: '/login' }); + }} + > + + + Logout + + Are you sure you want to log out? + + + + + + +
+
+ ); +} 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..09a5065 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,13 +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, - ), + providers, + session: { + strategy: 'jwt', + }, + pages: { + signIn: '/login', + signOut: '/logout', + }, callbacks: { - authorized: async ({ auth }) => { - return !!auth; + authorized({ auth }) { + return !!auth?.user; }, }, }); diff --git a/src/components/labeled-input.tsx b/src/components/labeled-input.tsx index 1f715a1..840d599 100644 --- a/src/components/labeled-input.tsx +++ b/src/components/labeled-input.tsx @@ -17,7 +17,7 @@ export default function LabeledInput({ return (
- + +
{ + 'use server'; + try { + await signIn('credentials', formData); + } catch (error) { + if (error instanceof AuthError) { + return redirect(`${SIGNIN_ERROR_URL}?error=${error.type}`); + } + throw error; + } + }} + >