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/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 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/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 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; + } + }} + >