Skip to content

Commit af66513

Browse files
committed
ENG-373 draft: Add routes and components for auth
In ui, roam and website
1 parent 2a1b0ad commit af66513

34 files changed

+4436
-268
lines changed

apps/roam/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"@octokit/core": "^6.1.3",
3333
"@repo/types": "*",
3434
"@supabase/auth-js": "^2.71.1",
35-
"@supabase/supabase-js": "^2.52.0",
35+
"@supabase/auth-ui-react": "0.4.7",
36+
"@supabase/supabase-js": "^2.52.1",
3637
"@tldraw/tldraw": "^2.0.0-alpha.12",
3738
"@vercel/blob": "^0.27.0",
3839
"contrast-color": "^1.0.1",
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import React, { useState, useEffect } from "react";
2+
import { createClient } from "@/lib/supabase/client";
3+
import { Button, Label } from "@blueprintjs/core";
4+
import { SignUpForm } from "./SignUpForm";
5+
import { LoginForm } from "./LoginForm";
6+
import { ForgotPasswordForm } from "./ForgotPasswordForm";
7+
import { UpdatePasswordForm } from "./UpdatePasswordForm";
8+
9+
// based on https://supabase.com/ui/docs/react/password-based-auth
10+
11+
enum AuthAction {
12+
waiting,
13+
loggedIn,
14+
login,
15+
signup,
16+
forgotPassword,
17+
updatePassword,
18+
emailSent,
19+
}
20+
21+
export const Account = () => {
22+
const [action, setAction] = useState(AuthAction.waiting);
23+
24+
const supabase = createClient();
25+
useEffect(() => {
26+
supabase.auth.getUser().then(({ data: { user } }) => {
27+
if (user) setAction(AuthAction.loggedIn);
28+
else setAction(AuthAction.login);
29+
});
30+
31+
const {
32+
data: { subscription },
33+
} = supabase.auth.onAuthStateChange((_event, session) => {
34+
if (session) setAction(AuthAction.loggedIn);
35+
else setAction(AuthAction.login);
36+
});
37+
38+
return () => subscription.unsubscribe();
39+
}, []);
40+
41+
switch (action) {
42+
case AuthAction.waiting:
43+
return (
44+
<div>
45+
<p>Checking...</p>
46+
</div>
47+
);
48+
case AuthAction.emailSent:
49+
return (
50+
<div>
51+
<p>An email was sent</p>
52+
</div>
53+
);
54+
case AuthAction.loggedIn:
55+
return (
56+
<div>
57+
<p>Logged in!</p>
58+
<Button
59+
type="button"
60+
onClick={() => {
61+
supabase.auth.signOut().then(() => {
62+
setAction(AuthAction.login);
63+
});
64+
}}
65+
>
66+
Log out
67+
</Button>
68+
<br />
69+
<Button
70+
type="button"
71+
onClick={() => {
72+
setAction(AuthAction.updatePassword);
73+
}}
74+
>
75+
Reset password
76+
</Button>
77+
</div>
78+
);
79+
case AuthAction.login:
80+
return (
81+
<div>
82+
<LoginForm />
83+
<Button
84+
type="button"
85+
onClick={() => {
86+
setAction(AuthAction.signup);
87+
}}
88+
>
89+
Sign up
90+
</Button>
91+
<br />
92+
<Label htmlFor="login_to_forgot">Forgot your password?</Label>
93+
<Button
94+
id="login_to_forgot"
95+
type="button"
96+
onClick={() => {
97+
setAction(AuthAction.forgotPassword);
98+
}}
99+
>
100+
Reset password
101+
</Button>
102+
</div>
103+
);
104+
case AuthAction.signup:
105+
return (
106+
<div>
107+
<SignUpForm />
108+
<Button
109+
type="button"
110+
onClick={() => {
111+
setAction(AuthAction.login);
112+
}}
113+
>
114+
login
115+
</Button>
116+
</div>
117+
);
118+
case AuthAction.forgotPassword:
119+
return (
120+
<div>
121+
<ForgotPasswordForm />
122+
</div>
123+
);
124+
case AuthAction.updatePassword:
125+
return (
126+
<div>
127+
<UpdatePasswordForm />
128+
</div>
129+
);
130+
}
131+
};
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { cn } from "@/lib/utils";
2+
import { createClient } from "@/lib/supabase/client";
3+
import { Button, Card, InputGroup, Label } from "@blueprintjs/core";
4+
import React, { useState } from "react";
5+
6+
// based on https://supabase.com/ui/docs/react/password-based-auth
7+
8+
export const ForgotPasswordForm = ({
9+
className,
10+
...props
11+
}: React.ComponentPropsWithoutRef<"div">) => {
12+
const [email, setEmail] = useState("");
13+
const [error, setError] = useState<string | null>(null);
14+
const [success, setSuccess] = useState(false);
15+
const [isLoading, setIsLoading] = useState(false);
16+
17+
const handleForgotPassword = async (e: React.FormEvent) => {
18+
const supabase = createClient();
19+
e.preventDefault();
20+
setIsLoading(true);
21+
setError(null);
22+
23+
try {
24+
// 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
25+
const { error } = await supabase.auth.resetPasswordForEmail(email, {
26+
redirectTo: "http://localhost:3000/update-password",
27+
});
28+
if (error) throw error;
29+
setSuccess(true);
30+
} catch (error: unknown) {
31+
setError(error instanceof Error ? error.message : "An error occurred");
32+
} finally {
33+
setIsLoading(false);
34+
}
35+
};
36+
37+
return (
38+
<div className={cn("flex flex-col gap-6", className)} {...props}>
39+
{success ? (
40+
<Card>
41+
<div className={cn("flex flex-col space-y-1.5 p-6", className)}>
42+
<div
43+
className={cn(
44+
"text-2xl font-semibold leading-none tracking-tight",
45+
className,
46+
)}
47+
>
48+
Check Your Email
49+
</div>
50+
<div className={cn("text-muted-foreground text-sm", className)}>
51+
Password reset instructions sent
52+
</div>
53+
</div>
54+
<div className={cn("p-6 pt-0", className)}>
55+
<p className="text-muted-foreground text-sm">
56+
If you registered using your email and password, you will receive
57+
a password reset email.
58+
</p>
59+
</div>
60+
</Card>
61+
) : (
62+
<Card>
63+
<div className={cn("flex flex-col space-y-1.5 p-6", className)}>
64+
<div
65+
className={cn(
66+
"text-2xl font-semibold leading-none tracking-tight",
67+
className,
68+
)}
69+
>
70+
Reset Your Password
71+
</div>
72+
<div className={cn("text-muted-foreground text-sm", className)}>
73+
Type in your email and we&apos;ll send you a link to reset your
74+
password
75+
</div>
76+
</div>
77+
<div className={cn("p-6 pt-0", className)}>
78+
<form onSubmit={handleForgotPassword}>
79+
<div className="flex flex-col gap-6">
80+
<div className="grid gap-2">
81+
<Label htmlFor="email">Email</Label>
82+
<InputGroup
83+
id="email"
84+
type="email"
85+
placeholder="m@example.com"
86+
required
87+
value={email}
88+
onChange={(e) => setEmail(e.target.value)}
89+
/>
90+
</div>
91+
{error && <p className="text-sm text-red-500">{error}</p>}
92+
<Button type="submit" className="w-full" disabled={isLoading}>
93+
{isLoading ? "Sending..." : "Send reset email"}
94+
</Button>
95+
</div>
96+
</form>
97+
</div>
98+
</Card>
99+
)}
100+
</div>
101+
);
102+
};
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { cn } from "@/lib/utils";
2+
import { createClient } from "@/lib/supabase/client";
3+
import { Button, Card, InputGroup, Label } from "@blueprintjs/core";
4+
import React, { useState } from "react";
5+
6+
// based on https://supabase.com/ui/docs/react/password-based-auth
7+
8+
export const LoginForm = ({
9+
className,
10+
...props
11+
}: React.ComponentPropsWithoutRef<"div">) => {
12+
const [email, setEmail] = useState("");
13+
const [password, setPassword] = useState("");
14+
const [error, setError] = useState<string | null>(null);
15+
const [isLoading, setIsLoading] = useState(false);
16+
const supabase = createClient();
17+
18+
const handleLogin = async (e: React.FormEvent) => {
19+
e.preventDefault();
20+
setIsLoading(true);
21+
setError(null);
22+
23+
try {
24+
const { error } = await supabase.auth.signInWithPassword({
25+
email,
26+
password,
27+
});
28+
if (error) throw error;
29+
// Original: Update this route to redirect to an authenticated route. The user already has an active session.
30+
// TODO: Replacement action
31+
// location.href = '/protected'
32+
} catch (error: unknown) {
33+
setError(error instanceof Error ? error.message : "An error occurred");
34+
} finally {
35+
setIsLoading(false);
36+
}
37+
};
38+
39+
return (
40+
<div className={cn("flex flex-col gap-6", className)} {...props}>
41+
<Card>
42+
<div className={cn("flex flex-col space-y-1.5 p-6", className)}>
43+
<div
44+
className={cn(
45+
"text-2xl font-semibold leading-none tracking-tight",
46+
className,
47+
)}
48+
>
49+
Login
50+
</div>
51+
<div className={cn("text-muted-foreground text-sm", className)}>
52+
Enter your email below to login to your account
53+
</div>
54+
</div>
55+
<div className={cn("p-6 pt-0", className)}>
56+
<form onSubmit={handleLogin}>
57+
<div className="flex flex-col gap-6">
58+
<div className="grid gap-2">
59+
<Label htmlFor="email">Email</Label>
60+
<InputGroup
61+
id="email"
62+
type="email"
63+
placeholder="m@example.com"
64+
required
65+
value={email}
66+
onChange={(e) => setEmail(e.target.value)}
67+
/>
68+
</div>
69+
<div className="grid gap-2">
70+
<div className="flex items-center">
71+
<Label htmlFor="password">Password</Label>
72+
</div>
73+
<InputGroup
74+
id="password"
75+
type="password"
76+
required
77+
value={password}
78+
onChange={(e) => setPassword(e.target.value)}
79+
/>
80+
</div>
81+
{error && <p className="text-sm text-red-500">{error}</p>}
82+
<Button type="submit" className="w-full" disabled={isLoading}>
83+
{isLoading ? "Logging in..." : "Login"}
84+
</Button>
85+
</div>
86+
<div className="mt-4 text-center text-sm">
87+
Don&apos;t have an account?{" "}
88+
<a href="/sign-up" className="underline underline-offset-4">
89+
Sign up
90+
</a>
91+
</div>
92+
</form>
93+
</div>
94+
</Card>
95+
</div>
96+
);
97+
}

0 commit comments

Comments
 (0)