diff --git a/apps/web/src/app/(auth)/layout.tsx b/apps/web/src/app/(auth)/layout.tsx
index 819fc61..8ff3499 100644
--- a/apps/web/src/app/(auth)/layout.tsx
+++ b/apps/web/src/app/(auth)/layout.tsx
@@ -1,11 +1,14 @@
import type { ReactNode } from 'react';
+import { GuestGuard } from '@/components/guest-guard';
export default function AuthLayout({ children }: { children: ReactNode }): React.ReactElement {
return (
-
-
+
);
}
diff --git a/apps/web/src/app/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx
index 40e86e8..b01d04f 100644
--- a/apps/web/src/app/(auth)/login/page.tsx
+++ b/apps/web/src/app/(auth)/login/page.tsx
@@ -1,14 +1,50 @@
'use client';
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
import Link from 'next/link';
+import { signIn } from '@/lib/auth-client';
export default function LoginPage(): React.ReactElement {
+ const router = useRouter();
+ const [error, setError] = useState
(null);
+ const [loading, setLoading] = useState(false);
+
+ async function handleSubmit(e: React.FormEvent): Promise {
+ e.preventDefault();
+ setError(null);
+ setLoading(true);
+
+ const form = new FormData(e.currentTarget);
+ const email = form.get('email') as string;
+ const password = form.get('password') as string;
+
+ const result = await signIn.email({ email, password });
+
+ if (result.error) {
+ setError(result.error.message ?? 'Sign in failed');
+ setLoading(false);
+ return;
+ }
+
+ router.push('/chat');
+ }
+
return (
Sign in
Sign in to your Mosaic account
-
diff --git a/apps/web/src/app/(auth)/register/page.tsx b/apps/web/src/app/(auth)/register/page.tsx
index 12d30bd..a17228c 100644
--- a/apps/web/src/app/(auth)/register/page.tsx
+++ b/apps/web/src/app/(auth)/register/page.tsx
@@ -1,14 +1,51 @@
'use client';
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
import Link from 'next/link';
+import { signUp } from '@/lib/auth-client';
export default function RegisterPage(): React.ReactElement {
+ const router = useRouter();
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(false);
+
+ async function handleSubmit(e: React.FormEvent): Promise {
+ e.preventDefault();
+ setError(null);
+ setLoading(true);
+
+ const form = new FormData(e.currentTarget);
+ const name = form.get('name') as string;
+ const email = form.get('email') as string;
+ const password = form.get('password') as string;
+
+ const result = await signUp.email({ name, email, password });
+
+ if (result.error) {
+ setError(result.error.message ?? 'Registration failed');
+ setLoading(false);
+ return;
+ }
+
+ router.push('/chat');
+ }
+
return (
Create account
Get started with Mosaic
-
@@ -49,16 +88,18 @@ export default function RegisterPage(): React.ReactElement {
type="password"
autoComplete="new-password"
required
- className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
+ disabled={loading}
+ className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50"
placeholder="••••••••"
/>
diff --git a/apps/web/src/app/(dashboard)/layout.tsx b/apps/web/src/app/(dashboard)/layout.tsx
index 7e70dc1..9edd362 100644
--- a/apps/web/src/app/(dashboard)/layout.tsx
+++ b/apps/web/src/app/(dashboard)/layout.tsx
@@ -1,6 +1,11 @@
import type { ReactNode } from 'react';
import { AppShell } from '@/components/layout/app-shell';
+import { AuthGuard } from '@/components/auth-guard';
export default function DashboardLayout({ children }: { children: ReactNode }): React.ReactElement {
- return {children};
+ return (
+
+ {children}
+
+ );
}
diff --git a/apps/web/src/components/auth-guard.tsx b/apps/web/src/components/auth-guard.tsx
new file mode 100644
index 0000000..3de8aef
--- /dev/null
+++ b/apps/web/src/components/auth-guard.tsx
@@ -0,0 +1,34 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+import { useEffect } from 'react';
+import { useSession } from '@/lib/auth-client';
+
+interface AuthGuardProps {
+ children: React.ReactNode;
+}
+
+export function AuthGuard({ children }: AuthGuardProps): React.ReactElement | null {
+ const { data: session, isPending } = useSession();
+ const router = useRouter();
+
+ useEffect(() => {
+ if (!isPending && !session) {
+ router.replace('/login');
+ }
+ }, [isPending, session, router]);
+
+ if (isPending) {
+ return (
+
+ );
+ }
+
+ if (!session) {
+ return null;
+ }
+
+ return <>{children}>;
+}
diff --git a/apps/web/src/components/guest-guard.tsx b/apps/web/src/components/guest-guard.tsx
new file mode 100644
index 0000000..d71e210
--- /dev/null
+++ b/apps/web/src/components/guest-guard.tsx
@@ -0,0 +1,35 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+import { useEffect } from 'react';
+import { useSession } from '@/lib/auth-client';
+
+interface GuestGuardProps {
+ children: React.ReactNode;
+}
+
+/** Redirects authenticated users away from auth pages. */
+export function GuestGuard({ children }: GuestGuardProps): React.ReactElement | null {
+ const { data: session, isPending } = useSession();
+ const router = useRouter();
+
+ useEffect(() => {
+ if (!isPending && session) {
+ router.replace('/chat');
+ }
+ }, [isPending, session, router]);
+
+ if (isPending) {
+ return (
+
+ );
+ }
+
+ if (session) {
+ return null;
+ }
+
+ return <>{children}>;
+}
diff --git a/apps/web/src/components/layout/topbar.tsx b/apps/web/src/components/layout/topbar.tsx
index 2d4bb7d..49e3538 100644
--- a/apps/web/src/components/layout/topbar.tsx
+++ b/apps/web/src/components/layout/topbar.tsx
@@ -1,9 +1,16 @@
'use client';
+import { useRouter } from 'next/navigation';
import { useSession, signOut } from '@/lib/auth-client';
export function Topbar(): React.ReactElement {
const { data: session } = useSession();
+ const router = useRouter();
+
+ async function handleSignOut(): Promise {
+ await signOut();
+ router.replace('/login');
+ }
return (
@@ -17,7 +24,7 @@ export function Topbar(): React.ReactElement {