diff --git a/apps/roam/src/components/auth/Account.tsx b/apps/roam/src/components/auth/Account.tsx
new file mode 100644
index 000000000..54261c3c5
--- /dev/null
+++ b/apps/roam/src/components/auth/Account.tsx
@@ -0,0 +1,131 @@
+import React, { useState, useEffect } from "react";
+import { createClient } from "@/lib/supabase/client";
+import { Button, Label } from "@blueprintjs/core";
+import { SignUpForm } from "./SignUpForm";
+import { LoginForm } from "./LoginForm";
+import { ForgotPasswordForm } from "./ForgotPasswordForm";
+import { UpdatePasswordForm } from "./UpdatePasswordForm";
+
+// based on https://supabase.com/ui/docs/react/password-based-auth
+
+enum AuthAction {
+ waiting,
+ loggedIn,
+ login,
+ signup,
+ forgotPassword,
+ updatePassword,
+ emailSent,
+}
+
+export const Account = () => {
+ const [action, setAction] = useState(AuthAction.waiting);
+
+ const supabase = createClient();
+ useEffect(() => {
+ supabase.auth.getUser().then(({ data: { user } }) => {
+ if (user) setAction(AuthAction.loggedIn);
+ else setAction(AuthAction.login);
+ });
+
+ const {
+ data: { subscription },
+ } = supabase.auth.onAuthStateChange((_event, session) => {
+ if (session) setAction(AuthAction.loggedIn);
+ else setAction(AuthAction.login);
+ });
+
+ return () => subscription.unsubscribe();
+ }, []);
+
+ switch (action) {
+ case AuthAction.waiting:
+ return (
+
+ );
+ case AuthAction.emailSent:
+ return (
+
+ );
+ case AuthAction.loggedIn:
+ return (
+
+
Logged in!
+
+
+
+
+ );
+ case AuthAction.login:
+ return (
+
+
+
+
+
+
+
+ );
+ case AuthAction.signup:
+ return (
+
+
+
+
+ );
+ case AuthAction.forgotPassword:
+ return (
+
+
+
+ );
+ case AuthAction.updatePassword:
+ return (
+
+
+
+ );
+ }
+};
diff --git a/apps/roam/src/components/auth/ForgotPasswordForm.tsx b/apps/roam/src/components/auth/ForgotPasswordForm.tsx
new file mode 100644
index 000000000..7c0cd5cd6
--- /dev/null
+++ b/apps/roam/src/components/auth/ForgotPasswordForm.tsx
@@ -0,0 +1,102 @@
+import { cn } from "@/lib/utils";
+import { createClient } from "@/lib/supabase/client";
+import { Button, Card, InputGroup, Label } from "@blueprintjs/core";
+import React, { useState } from "react";
+
+// based on https://supabase.com/ui/docs/react/password-based-auth
+
+export const ForgotPasswordForm = ({
+ className,
+ ...props
+}: React.ComponentPropsWithoutRef<"div">) => {
+ const [email, setEmail] = useState("");
+ const [error, setError] = useState(null);
+ const [success, setSuccess] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleForgotPassword = async (e: React.FormEvent) => {
+ const supabase = createClient();
+ e.preventDefault();
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ // The url which will be included in the email. This URL needs to be configured in your redirect URLs in the Supabase dashboard at https://supabase.com/dashboard/project/_/auth/url-configuration
+ const { error } = await supabase.auth.resetPasswordForEmail(email, {
+ redirectTo: "http://localhost:3000/update-password",
+ });
+ if (error) throw error;
+ setSuccess(true);
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : "An error occurred");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ {success ? (
+
+
+
+ Check Your Email
+
+
+ Password reset instructions sent
+
+
+
+
+ If you registered using your email and password, you will receive
+ a password reset email.
+
+
+
+ ) : (
+
+
+
+ Reset Your Password
+
+
+ Type in your email and we'll send you a link to reset your
+ password
+
+
+
+
+ )}
+
+ );
+};
diff --git a/apps/roam/src/components/auth/LoginForm.tsx b/apps/roam/src/components/auth/LoginForm.tsx
new file mode 100644
index 000000000..979a992a4
--- /dev/null
+++ b/apps/roam/src/components/auth/LoginForm.tsx
@@ -0,0 +1,97 @@
+import { cn } from "@/lib/utils";
+import { createClient } from "@/lib/supabase/client";
+import { Button, Card, InputGroup, Label } from "@blueprintjs/core";
+import React, { useState } from "react";
+
+// based on https://supabase.com/ui/docs/react/password-based-auth
+
+export const LoginForm = ({
+ className,
+ ...props
+}: React.ComponentPropsWithoutRef<"div">) => {
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [error, setError] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const supabase = createClient();
+
+ const handleLogin = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const { error } = await supabase.auth.signInWithPassword({
+ email,
+ password,
+ });
+ if (error) throw error;
+ // Original: Update this route to redirect to an authenticated route. The user already has an active session.
+ // TODO: Replacement action
+ // location.href = '/protected'
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : "An error occurred");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+ Login
+
+
+ Enter your email below to login to your account
+
+
+
+
+
+ );
+}
diff --git a/apps/roam/src/components/auth/SignUpForm.tsx b/apps/roam/src/components/auth/SignUpForm.tsx
new file mode 100644
index 000000000..b1206fafa
--- /dev/null
+++ b/apps/roam/src/components/auth/SignUpForm.tsx
@@ -0,0 +1,132 @@
+import { cn } from "@/lib/utils";
+import { createClient } from "@/lib/supabase/client";
+import { Button, Card, InputGroup, Label } from "@blueprintjs/core";
+import React, { useState } from "react";
+
+// based on https://supabase.com/ui/docs/react/password-based-auth
+
+export const SignUpForm = ({
+ className,
+ ...props
+}: React.ComponentPropsWithoutRef<"div">) => {
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [repeatPassword, setRepeatPassword] = useState("");
+ const [error, setError] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [success, setSuccess] = useState(false);
+
+ const handleSignUp = async (e: React.FormEvent) => {
+ const supabase = createClient();
+ e.preventDefault();
+ setError(null);
+
+ if (password !== repeatPassword) {
+ setError("Passwords do not match");
+ return;
+ }
+ setIsLoading(true);
+
+ try {
+ const { error } = await supabase.auth.signUp({
+ email,
+ password,
+ });
+ if (error) throw error;
+ setSuccess(true);
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : "An error occurred");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ {success ? (
+
+
+
+ Thank you for signing up!
+
+
+ Check your email to confirm
+
+
+
+
+ You've successfully signed up. Please check your email to confirm
+ your account before signing in.
+
+
+
+ ) : (
+
+
+
+ Sign up
+
+
+ Create a new account
+
+
+
+
+ )}
+
+ );
+};
diff --git a/apps/roam/src/components/auth/UpdatePasswordForm.tsx b/apps/roam/src/components/auth/UpdatePasswordForm.tsx
new file mode 100644
index 000000000..31f933b0a
--- /dev/null
+++ b/apps/roam/src/components/auth/UpdatePasswordForm.tsx
@@ -0,0 +1,75 @@
+import { cn } from "@/lib/utils";
+import { createClient } from "@/lib/supabase/client";
+import { Button, Card, InputGroup, Label } from "@blueprintjs/core";
+import React, { useState } from "react";
+
+// based on https://supabase.com/ui/docs/react/password-based-auth
+
+export const UpdatePasswordForm = ({
+ className,
+ ...props
+}: React.ComponentPropsWithoutRef<"div">) => {
+ const [password, setPassword] = useState("");
+ const [error, setError] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleForgotPassword = async (e: React.FormEvent) => {
+ const supabase = createClient();
+ e.preventDefault();
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const { error } = await supabase.auth.updateUser({ password });
+ if (error) throw error;
+ // Original: Update this route to redirect to an authenticated route. The user already has an active session.
+ // TODO: Replacement action
+ // location.href = "/protected";
+ } catch (error: unknown) {
+ setError(error instanceof Error ? error.message : "An error occurred");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+ Reset Your Password
+
+
+ Please enter your new password below.
+
+
+
+
+
+ );
+};
diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx
index 6046fa158..723a08136 100644
--- a/apps/roam/src/components/settings/Settings.tsx
+++ b/apps/roam/src/components/settings/Settings.tsx
@@ -25,6 +25,7 @@ import sendErrorEmail from "~/utils/sendErrorEmail";
import HomePersonalSettings from "./HomePersonalSettings";
import refreshConfigTree from "~/utils/refreshConfigTree";
import { FeedbackWidget } from "~/components/BirdEatsBugs";
+import { Account } from "~/components/auth/Account";
type SectionHeaderProps = {
children: React.ReactNode;
@@ -139,6 +140,12 @@ export const SettingsDialog = ({
className="overflow-y-auto"
panel={}
/>
+ }
+ />