diff --git a/apps/web/.env.example b/apps/web/.env.example index b321f09a..f27da946 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -7,6 +7,7 @@ NODE_ENV=development # Public NEXT_PUBLIC_SITE_URL=http://localhost:3000 NEXT_PUBLIC_MARBLE_API_URL=https://api.marblecms.com +NEXT_PUBLIC_AUTH_API_BASE_URL=http://localhost:3001/api # Server DATABASE_URL="postgresql://opencut:opencut@localhost:5432/opencut" @@ -25,4 +26,4 @@ R2_ACCESS_KEY_ID=your_access_key_here R2_SECRET_ACCESS_KEY=your_secret_key_here R2_BUCKET_NAME=opencut-transcription # whatever you named your r2 bucket -MODAL_TRANSCRIPTION_URL=your_modal_url_here \ No newline at end of file +MODAL_TRANSCRIPTION_URL=your_modal_url_here diff --git a/apps/web/src/app/auth/login/page.tsx b/apps/web/src/app/auth/login/page.tsx new file mode 100644 index 00000000..5bd1e186 --- /dev/null +++ b/apps/web/src/app/auth/login/page.tsx @@ -0,0 +1,98 @@ +"use client"; + +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useAuthStore } from "@/stores/auth-store"; + +export default function LoginPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const nextPath = searchParams.get("next") || "/projects"; + + const login = useAuthStore((state) => state.login); + const clearError = useAuthStore((state) => state.clearError); + const error = useAuthStore((state) => state.error); + const status = useAuthStore((state) => state.status); + const isInitialized = useAuthStore((state) => state.isInitialized); + + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + + useEffect(() => { + if (status === "authenticated") { + router.replace(nextPath); + } + }, [nextPath, router, status]); + + const isSubmitting = status === "loading"; + + return ( +
+ + + Sign in + + + {error ? ( +
+ {error} +
+ ) : null} +
{ + event.preventDefault(); + clearError(); + try { + await login({ username, password }); + } catch { + // Error state is already set in the auth store. + } + }} + > +
+ + setUsername(event.target.value)} + placeholder="john" + required + /> +
+
+ + setPassword(event.target.value)} + placeholder="********" + required + /> +
+ +
+
+ No account yet?{" "} + + Create one + +
+ {!isInitialized ? ( +
Loading session...
+ ) : null} +
+
+
+ ); +} diff --git a/apps/web/src/app/auth/register/page.tsx b/apps/web/src/app/auth/register/page.tsx new file mode 100644 index 00000000..f39704ae --- /dev/null +++ b/apps/web/src/app/auth/register/page.tsx @@ -0,0 +1,131 @@ +"use client"; + +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useAuthStore } from "@/stores/auth-store"; + +export default function RegisterPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const nextPath = searchParams.get("next") || "/projects"; + + const register = useAuthStore((state) => state.register); + const clearError = useAuthStore((state) => state.clearError); + const error = useAuthStore((state) => state.error); + const status = useAuthStore((state) => state.status); + + const [firstname, setFirstname] = useState(""); + const [lastname, setLastname] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [organisationName, setOrganisationName] = useState(""); + + useEffect(() => { + if (status === "authenticated") { + router.replace(nextPath); + } + }, [nextPath, router, status]); + + const isSubmitting = status === "loading"; + + return ( +
+ + + Create account + + + {error ? ( +
+ {error} +
+ ) : null} +
{ + event.preventDefault(); + clearError(); + try { + await register({ + email, + firstname, + lastname, + password, + organisationName: organisationName || undefined, + }); + } catch { + // Error state is already set in the auth store. + } + }} + > +
+
+ + setFirstname(event.target.value)} + required + /> +
+
+ + setLastname(event.target.value)} + required + /> +
+
+
+ + setEmail(event.target.value)} + required + /> +
+
+ + setPassword(event.target.value)} + required + minLength={8} + /> +
+
+ + setOrganisationName(event.target.value)} + /> +
+ +
+
+ Already have an account?{" "} + + Sign in + +
+
+
+
+ ); +} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 2b3d0c30..e036236d 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -7,6 +7,8 @@ import { baseMetaData } from "./metadata"; import { BotIdClient } from "botid/client"; import { webEnv } from "@opencut/env/web"; import { Inter } from "next/font/google"; +import { AuthProvider } from "@/components/providers/auth-provider"; +import { AuthGuard } from "@/components/auth/auth-guard"; const siteFont = Inter({ subsets: ["latin"] }); @@ -35,22 +37,24 @@ export default function RootLayout({ defaultTheme="system" disableTransitionOnChange={true} > - - -