-
Notifications
You must be signed in to change notification settings - Fork 4
ENG-373 draft: Add routes and components for auth #238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div> | ||
| <p>Checking...</p> | ||
| </div> | ||
| ); | ||
| case AuthAction.emailSent: | ||
| return ( | ||
| <div> | ||
| <p>An email was sent</p> | ||
| </div> | ||
| ); | ||
| case AuthAction.loggedIn: | ||
| return ( | ||
| <div> | ||
| <p>Logged in!</p> | ||
| <Button | ||
| type="button" | ||
| onClick={() => { | ||
| supabase.auth.signOut().then(() => { | ||
| setAction(AuthAction.login); | ||
| }); | ||
| }} | ||
| > | ||
| Log out | ||
| </Button> | ||
| <br /> | ||
| <Button | ||
| type="button" | ||
| onClick={() => { | ||
| setAction(AuthAction.updatePassword); | ||
| }} | ||
| > | ||
| Reset password | ||
| </Button> | ||
| </div> | ||
| ); | ||
| case AuthAction.login: | ||
| return ( | ||
| <div> | ||
| <LoginForm /> | ||
| <Button | ||
| type="button" | ||
| onClick={() => { | ||
| setAction(AuthAction.signup); | ||
| }} | ||
| > | ||
| Sign up | ||
| </Button> | ||
| <br /> | ||
| <Label htmlFor="login_to_forgot">Forgot your password?</Label> | ||
| <Button | ||
| id="login_to_forgot" | ||
| type="button" | ||
| onClick={() => { | ||
| setAction(AuthAction.forgotPassword); | ||
| }} | ||
| > | ||
| Reset password | ||
| </Button> | ||
| </div> | ||
| ); | ||
| case AuthAction.signup: | ||
| return ( | ||
| <div> | ||
| <SignUpForm /> | ||
| <Button | ||
| type="button" | ||
| onClick={() => { | ||
| setAction(AuthAction.login); | ||
| }} | ||
| > | ||
| login | ||
| </Button> | ||
| </div> | ||
| ); | ||
| case AuthAction.forgotPassword: | ||
| return ( | ||
| <div> | ||
| <ForgotPasswordForm /> | ||
| </div> | ||
| ); | ||
| case AuthAction.updatePassword: | ||
| return ( | ||
| <div> | ||
| <UpdatePasswordForm /> | ||
| </div> | ||
| ); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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<string | null>(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", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace hard-coded localhost URL with environment variable. The hard-coded localhost URL will not work in production or other environments and should be configurable. - redirectTo: "http://localhost:3000/update-password",
+ redirectTo: `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/update-password`,📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| }); | ||||||
| if (error) throw error; | ||||||
| setSuccess(true); | ||||||
| } catch (error: unknown) { | ||||||
| setError(error instanceof Error ? error.message : "An error occurred"); | ||||||
| } finally { | ||||||
| setIsLoading(false); | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| return ( | ||||||
| <div className={cn("flex flex-col gap-6", className)} {...props}> | ||||||
| {success ? ( | ||||||
| <Card> | ||||||
| <div className={cn("flex flex-col space-y-1.5 p-6", className)}> | ||||||
| <div | ||||||
| className={cn( | ||||||
| "text-2xl font-semibold leading-none tracking-tight", | ||||||
| className, | ||||||
| )} | ||||||
| > | ||||||
| Check Your Email | ||||||
| </div> | ||||||
| <div className={cn("text-muted-foreground text-sm", className)}> | ||||||
| Password reset instructions sent | ||||||
| </div> | ||||||
| </div> | ||||||
| <div className={cn("p-6 pt-0", className)}> | ||||||
| <p className="text-muted-foreground text-sm"> | ||||||
| If you registered using your email and password, you will receive | ||||||
| a password reset email. | ||||||
| </p> | ||||||
| </div> | ||||||
| </Card> | ||||||
| ) : ( | ||||||
| <Card> | ||||||
| <div className={cn("flex flex-col space-y-1.5 p-6", className)}> | ||||||
| <div | ||||||
| className={cn( | ||||||
| "text-2xl font-semibold leading-none tracking-tight", | ||||||
| className, | ||||||
| )} | ||||||
| > | ||||||
| Reset Your Password | ||||||
| </div> | ||||||
| <div className={cn("text-muted-foreground text-sm", className)}> | ||||||
| Type in your email and we'll send you a link to reset your | ||||||
| password | ||||||
| </div> | ||||||
| </div> | ||||||
| <div className={cn("p-6 pt-0", className)}> | ||||||
| <form onSubmit={handleForgotPassword}> | ||||||
| <div className="flex flex-col gap-6"> | ||||||
| <div className="grid gap-2"> | ||||||
| <Label htmlFor="email">Email</Label> | ||||||
| <InputGroup | ||||||
| id="email" | ||||||
| type="email" | ||||||
| placeholder="m@example.com" | ||||||
| required | ||||||
| value={email} | ||||||
| onChange={(e) => setEmail(e.target.value)} | ||||||
| /> | ||||||
| </div> | ||||||
| {error && <p className="text-sm text-red-500">{error}</p>} | ||||||
| <Button type="submit" className="w-full" disabled={isLoading}> | ||||||
| {isLoading ? "Sending..." : "Send reset email"} | ||||||
| </Button> | ||||||
| </div> | ||||||
| </form> | ||||||
| </div> | ||||||
| </Card> | ||||||
| )} | ||||||
| </div> | ||||||
| ); | ||||||
| }; | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<string | null>(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' | ||||||||||||||
|
Comment on lines
+29
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Implement proper navigation after successful login. The commented code suggests navigation should happen after login, but it's currently not implemented. This leaves users without clear feedback about successful authentication. Consider implementing proper navigation. Since this is a roam app, you might want to: 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'
+ // Navigate to authenticated state or refresh the component
+ window.location.reload(); // or implement proper state management📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| } catch (error: unknown) { | ||||||||||||||
| setError(error instanceof Error ? error.message : "An error occurred"); | ||||||||||||||
| } finally { | ||||||||||||||
| setIsLoading(false); | ||||||||||||||
| } | ||||||||||||||
| }; | ||||||||||||||
|
|
||||||||||||||
| return ( | ||||||||||||||
| <div className={cn("flex flex-col gap-6", className)} {...props}> | ||||||||||||||
| <Card> | ||||||||||||||
| <div className={cn("flex flex-col space-y-1.5 p-6", className)}> | ||||||||||||||
| <div | ||||||||||||||
| className={cn( | ||||||||||||||
| "text-2xl font-semibold leading-none tracking-tight", | ||||||||||||||
| className, | ||||||||||||||
| )} | ||||||||||||||
| > | ||||||||||||||
| Login | ||||||||||||||
| </div> | ||||||||||||||
| <div className={cn("text-muted-foreground text-sm", className)}> | ||||||||||||||
| Enter your email below to login to your account | ||||||||||||||
| </div> | ||||||||||||||
| </div> | ||||||||||||||
| <div className={cn("p-6 pt-0", className)}> | ||||||||||||||
| <form onSubmit={handleLogin}> | ||||||||||||||
| <div className="flex flex-col gap-6"> | ||||||||||||||
| <div className="grid gap-2"> | ||||||||||||||
| <Label htmlFor="email">Email</Label> | ||||||||||||||
| <InputGroup | ||||||||||||||
| id="email" | ||||||||||||||
| type="email" | ||||||||||||||
| placeholder="m@example.com" | ||||||||||||||
| required | ||||||||||||||
| value={email} | ||||||||||||||
| onChange={(e) => setEmail(e.target.value)} | ||||||||||||||
| /> | ||||||||||||||
| </div> | ||||||||||||||
| <div className="grid gap-2"> | ||||||||||||||
| <div className="flex items-center"> | ||||||||||||||
| <Label htmlFor="password">Password</Label> | ||||||||||||||
| </div> | ||||||||||||||
| <InputGroup | ||||||||||||||
| id="password" | ||||||||||||||
| type="password" | ||||||||||||||
| required | ||||||||||||||
| value={password} | ||||||||||||||
| onChange={(e) => setPassword(e.target.value)} | ||||||||||||||
| /> | ||||||||||||||
| </div> | ||||||||||||||
| {error && <p className="text-sm text-red-500">{error}</p>} | ||||||||||||||
| <Button type="submit" className="w-full" disabled={isLoading}> | ||||||||||||||
| {isLoading ? "Logging in..." : "Login"} | ||||||||||||||
| </Button> | ||||||||||||||
| </div> | ||||||||||||||
| <div className="mt-4 text-center text-sm"> | ||||||||||||||
| Don't have an account?{" "} | ||||||||||||||
| <a href="/sign-up" className="underline underline-offset-4"> | ||||||||||||||
| Sign up | ||||||||||||||
| </a> | ||||||||||||||
| </div> | ||||||||||||||
| </form> | ||||||||||||||
| </div> | ||||||||||||||
| </Card> | ||||||||||||||
| </div> | ||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Optimize Supabase client creation and fix useEffect dependencies.
The Supabase client is being recreated on every render. Consider using
useMemoto memoize it. Also,supabaseshould be included in the useEffect dependency array.🤖 Prompt for AI Agents