Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions apps/roam/src/components/auth/Account.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<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 />
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{" "}
<Button
type="button"
onClick={() => {
setAction(AuthAction.signup);
}}
>
Sign up
</Button>
</div>

<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>
);
}
};
102 changes: 102 additions & 0 deletions apps/roam/src/components/auth/ForgotPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -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<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",
});
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&apos;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>
);
};
91 changes: 91 additions & 0 deletions apps/roam/src/components/auth/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -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<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'
} 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>
</form>
</div>
</Card>
</div>
);
};
Loading
Loading