diff --git a/apps/roam/src/components/auth/Account.tsx b/apps/roam/src/components/auth/Account.tsx new file mode 100644 index 000000000..9aedbfe8c --- /dev/null +++ b/apps/roam/src/components/auth/Account.tsx @@ -0,0 +1,135 @@ +import React, { useState, useEffect } from "react"; +import { createClient } from "@repo/database/lib/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 ( +
+

Checking...

+
+ ); + case AuthAction.emailSent: + return ( +
+

An email was sent

+
+ ); + case AuthAction.loggedIn: + return ( +
+

Logged in!

+ +
+ +
+ ); + case AuthAction.login: + return ( +
+ +
+ Don't have an account?{" "} + +
+ +
+ + +
+ ); + 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..a7333ead7 --- /dev/null +++ b/apps/roam/src/components/auth/ForgotPasswordForm.tsx @@ -0,0 +1,102 @@ +import { cn } from "@repo/ui/lib/utils"; +import { createClient } from "@repo/database/lib/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 +
+
+
+
+
+
+ + setEmail(e.target.value)} + /> +
+ {error &&

{error}

} + +
+
+
+
+ )} +
+ ); +}; diff --git a/apps/roam/src/components/auth/LoginForm.tsx b/apps/roam/src/components/auth/LoginForm.tsx new file mode 100644 index 000000000..7241e38b1 --- /dev/null +++ b/apps/roam/src/components/auth/LoginForm.tsx @@ -0,0 +1,91 @@ +import { cn } from "@repo/ui/lib/utils"; +import { createClient } from "@repo/database/lib/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 +
+
+
+
+
+
+ + setEmail(e.target.value)} + /> +
+
+
+ +
+ setPassword(e.target.value)} + /> +
+ {error &&

{error}

} + +
+
+
+
+
+ ); +}; diff --git a/apps/roam/src/components/auth/SignUpForm.tsx b/apps/roam/src/components/auth/SignUpForm.tsx new file mode 100644 index 000000000..5e8ce13e4 --- /dev/null +++ b/apps/roam/src/components/auth/SignUpForm.tsx @@ -0,0 +1,132 @@ +import { cn } from "@repo/ui/lib/utils"; +import { createClient } from "@repo/database/lib/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 +
+
+
+
+
+
+ + setEmail(e.target.value)} + /> +
+
+
+ +
+ setPassword(e.target.value)} + /> +
+
+
+ +
+ setRepeatPassword(e.target.value)} + /> +
+ {error &&

{error}

} + +
+
+
+
+ )} +
+ ); +}; diff --git a/apps/roam/src/components/auth/UpdatePasswordForm.tsx b/apps/roam/src/components/auth/UpdatePasswordForm.tsx new file mode 100644 index 000000000..136b24a11 --- /dev/null +++ b/apps/roam/src/components/auth/UpdatePasswordForm.tsx @@ -0,0 +1,75 @@ +import { cn } from "@repo/ui/lib/utils"; +import { createClient } from "@repo/database/lib/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. +
+
+
+
+
+
+ + setPassword(e.target.value)} + /> +
+ {error &&

{error}

} + +
+
+
+
+
+ ); +}; diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index 08c31aff5..0791db0d5 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -29,6 +29,7 @@ import SuggestiveModeSettings from "./SuggestiveModeSettings"; import { getVersionWithDate } from "~/utils/getVersion"; import { LeftSidebarPersonalSections } from "./LeftSidebarPersonalSettings"; import { LeftSidebarGlobalSections } from "./LeftSidebarGlobalSettings"; +import { Account } from "~/components/auth/Account"; type SectionHeaderProps = { children: React.ReactNode; @@ -146,6 +147,12 @@ export const SettingsDialog = ({ className="overflow-y-auto" panel={} /> + } + /> } +======= + panel={ +
+ + +
+ } +>>>>>>> abef632e (Auth components for Roam. Login/Logout/etc.) />