Skip to content

Commit

Permalink
fix: fix supabase auth and SSR
Browse files Browse the repository at this point in the history
  • Loading branch information
girl-loves-coding committed Nov 7, 2024
1 parent f5e0a60 commit e00d33e
Show file tree
Hide file tree
Showing 18 changed files with 215 additions and 76 deletions.
4 changes: 2 additions & 2 deletions packages/client/app/components/auth/auth-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ export const AuthForm = ({ error }: AuthFormProps) => {
id="password"
name="password"
type="password"
placeholder="••••••••••"
placeholder="········"
required
className="h-11 text-base"
className="h-11 text-base placeholder:font-extrabold"
/>
</div>

Expand Down
4 changes: 2 additions & 2 deletions packages/client/app/components/auth/sign-up-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ export const SignUpForm = ({ error, success }: SignUpFormProps) => {
id="password"
name="password"
type="password"
placeholder="••••••••"
placeholder="········"
required
className="h-11 text-base"
className="h-11 text-base placeholder:font-extrabold"
/>
</div>

Expand Down
56 changes: 45 additions & 11 deletions packages/client/app/components/marketing/testimonial.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,52 @@
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";

export const Testimonial = () => {
const testimonials = [
"Data-River has completely transformed how my team and I handle automation. The visual editor makes complex workflows accessible, even for those with no coding experience. It's intuitive, powerful, and simply brilliant!",
"The modular design of Data-River is perfect for our growing team. We can build, share, and customize blocks, which saves us hours on repetitive tasks. Plus, our non-tech members love the easy drag-and-drop setup.",
"As an educator, Data-River is a game changer. Its intuitive, visual approach makes it easy to introduce programming concepts to students, and the platform’s flexibility allows them to explore more advanced logic when they’re ready.",
];

const [testimonial, setTestimonial] = useState(testimonials[0]);

useEffect(() => {
const randomIndex = Math.floor(Math.random() * testimonials.length);
setTestimonial(testimonials[randomIndex]);

const interval = setInterval(() => {
setTestimonial((current) => {
const currentIndex = testimonials.indexOf(current);
const nextIndex = (currentIndex + 1) % testimonials.length;
return testimonials[nextIndex];
});
}, 8000);

return () => clearInterval(interval);
}, []);

return (
<div className="hidden bg-muted/40 xl:flex xl:w-[60%] xl:flex-col xl:justify-center xl:px-24 shrink-0">
<div className="mx-auto max-w-[45%]">
<blockquote className="space-y-10">
<p className="text-3xl font-medium leading-relaxed lg:text-4xl">
"I just learned about Data River and I'm in love! It's an open
source Firebase alternative with real-time database changes and
simple UI for database interaction."
</p>
<footer className="flex items-center gap-3 text-xl">
<span className="font-semibold">Sarah Chen</span>
<span className="text-muted-foreground">• Software Engineer</span>
</footer>
</blockquote>
<AnimatePresence mode="wait">
<motion.blockquote
key={testimonial}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="space-y-10"
>
<motion.p
className="text-3xl font-medium leading-relaxed lg:text-4xl"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
>
"{testimonial}"
</motion.p>
</motion.blockquote>
</AnimatePresence>
</div>
</div>
);
Expand Down
4 changes: 3 additions & 1 deletion packages/client/app/routes/_app.profile.$userName.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useLoaderData } from "@remix-run/react";
import { type LoaderFunctionArgs, json, redirect } from "@remix-run/node";
import { getSession } from "~/utils/session.server";
import { supabase } from "~/utils/supabase.server";
import { createClient } from "~/utils/supabase.server";
import type { Database } from "~/types/supabase";
import { ProfileHeader } from "~/components/profile/profile-header";
import { AchievementsCard } from "~/components/profile/achievements-card";
Expand All @@ -18,6 +18,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
return redirect("/profile");
}

const { supabase } = await createClient(request);

const { data: profile, error } = await supabase
.from("profiles")
.select("*")
Expand Down
20 changes: 11 additions & 9 deletions packages/client/app/routes/_app.settings.profile.avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
unstable_parseMultipartFormData,
} from "@remix-run/node";
import { getSession } from "~/utils/session.server";
import { supabase } from "~/utils/supabase.server";
import { createClient } from "~/utils/supabase.server";
import { uploadHandler } from "~/utils/upload.server";
import { deleteStorageFile } from "~/utils/storage.server";

Expand All @@ -13,20 +13,20 @@ const AVATARS_BUCKET = "avatars";
async function handleAvatarUpload(
request: Request,
userId: string,
accessToken: string,
oldAvatarUrl: string | null,
) {
let newAvatarUrl: string | null = null;

try {
const formData = await unstable_parseMultipartFormData(
request,
uploadHandler(accessToken),
uploadHandler(request),
);

newAvatarUrl = formData.get("avatar") as string;

// Update profile with new avatar URL
const { supabase } = await createClient(request);
const { error: updateError } = await supabase
.from("profiles")
.update({
Expand All @@ -40,13 +40,13 @@ async function handleAvatarUpload(
}

// Delete old avatar if it exists
await deleteStorageFile(oldAvatarUrl, accessToken, AVATARS_BUCKET);
await deleteStorageFile(request, oldAvatarUrl, AVATARS_BUCKET);

return json({ success: true });
} catch (error) {
// If we failed after uploading the new avatar, clean it up
if (newAvatarUrl) {
await deleteStorageFile(newAvatarUrl, accessToken, AVATARS_BUCKET);
await deleteStorageFile(request, newAvatarUrl, AVATARS_BUCKET);
}

return json(
Expand All @@ -61,11 +61,12 @@ async function handleAvatarUpload(
}

async function handleAvatarRemoval(
request: Request,
userId: string,
oldAvatarUrl: string | null,
accessToken: string,
) {
try {
const { supabase } = await createClient(request);
const { error } = await supabase
.from("profiles")
.update({ avatar_url: null, updated_at: new Date().toISOString() })
Expand All @@ -76,7 +77,7 @@ async function handleAvatarRemoval(
}

// Delete the old avatar file
await deleteStorageFile(oldAvatarUrl, accessToken, AVATARS_BUCKET);
await deleteStorageFile(request, oldAvatarUrl, AVATARS_BUCKET);

return json({ success: true });
} catch (error) {
Expand All @@ -98,6 +99,7 @@ export async function action({ request }: ActionFunctionArgs) {
const userId = session.get("user_id") as string;

// Fetch current profile to get the old avatar URL
const { supabase } = await createClient(request);
const { data: profile, error: profileError } = await supabase
.from("profiles")
.select("avatar_url")
Expand All @@ -120,10 +122,10 @@ export async function action({ request }: ActionFunctionArgs) {
) {
const formData = await request.formData();
if (formData.get("_action") === "remove") {
return handleAvatarRemoval(userId, oldAvatarUrl, accessToken);
return handleAvatarRemoval(request, userId, oldAvatarUrl);
}
}

// Handle avatar upload
return handleAvatarUpload(request, userId, accessToken, oldAvatarUrl);
return handleAvatarUpload(request, userId, oldAvatarUrl);
}
4 changes: 3 additions & 1 deletion packages/client/app/routes/_app.settings.profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
redirect,
} from "@remix-run/node";
import { getSession } from "~/utils/session.server";
import { supabase } from "~/utils/supabase.server";
import { createClient } from "~/utils/supabase.server";
import {
Card,
CardContent,
Expand All @@ -31,6 +31,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
const session = await getSession(request);
const userId = session.get("user_id") as string;

const { supabase } = await createClient(request);
const { data: profile, error } = await supabase
.from("profiles")
.select("*")
Expand Down Expand Up @@ -76,6 +77,7 @@ export async function action({ request }: ActionFunctionArgs) {
);
}

const { supabase } = await createClient(request);
const { error } = await supabase
.from("profiles")
.update({
Expand Down
29 changes: 12 additions & 17 deletions packages/client/app/routes/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Outlet, useLoaderData } from "@remix-run/react";
import { type LoaderFunctionArgs, json, redirect } from "@remix-run/node";
import { getSession } from "~/utils/session.server";
import { supabase } from "~/utils/supabase.server";
import { createClient } from "~/utils/supabase.server";
import type { Database } from "~/types/supabase";
import { Navbar } from "~/components/layout/navbar";
import { CookieConsent } from "~/components/layout/cookie-consent";
import { isRedirectResponse } from "@remix-run/react/dist/data";

type LoaderData = {
profile: Pick<
Expand All @@ -14,27 +15,21 @@ type LoaderData = {
};

export async function loader({ request }: LoaderFunctionArgs) {
const session = await getSession(request.headers.get("Cookie"));

if (!session.has("access_token")) {
// TODO: Remove this after testing
return json<LoaderData>({
profile: {
id: "test",
display_name: "test",
avatar_url: "test",
username: "test",
},
});
// return redirect("/sign-in");
}
const { supabase } = await createClient(request);

let {
data: { user },
} = await supabase.auth.getUser();

const userId = session.get("user_id") as string;
if (!user) {
console.log("redirecting to sign-in");
return redirect("/sign-in");
}

const { data: profile, error } = await supabase
.from("profiles")
.select("*")
.eq("id", userId)
.eq("id", user.id)
.single();

if (error || !profile) {
Expand Down
15 changes: 13 additions & 2 deletions packages/client/app/routes/auth.callback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
type ActionFunctionArgs,
json,
} from "@remix-run/node";
import { supabase } from "~/utils/supabase.server";
import { createClient } from "~/utils/supabase.server";
import { getSession, commitSession } from "~/utils/session.server";

export async function loader({ request }: LoaderFunctionArgs) {
Expand All @@ -13,12 +13,21 @@ export async function loader({ request }: LoaderFunctionArgs) {
const next = url.searchParams.get("next") || "/editor";

if (code) {
const { supabase } = await createClient(request);

const { data, error } = await supabase.auth.exchangeCodeForSession(code);

if (error) {
console.error("Error exchanging code for session", error);
return redirect(`/sign-in?error=${error.message}`);
}

if (!error && data.session) {
const session = await getSession(request.headers.get("Cookie"));
session.set("access_token", data.session.access_token);
session.set("user_id", data.session.user.id);
session.set("expires_at", data.session.expires_at);
session.set("access_token", data.session.access_token);
session.set("refresh_token", data.session.refresh_token);

return redirect(next, {
headers: {
Expand All @@ -35,6 +44,8 @@ export async function action({ request }: ActionFunctionArgs) {
const { access_token } = await request.json();

if (access_token) {
const { supabase } = await createClient(request);

const {
data: { user },
error,
Expand Down
8 changes: 6 additions & 2 deletions packages/client/app/routes/auth.github.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { ActionFunctionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { supabase } from "~/utils/supabase.server";
import { createClient } from "~/utils/supabase.server";

export async function action({ request }: ActionFunctionArgs) {
const { supabase, headers } = await createClient(request);

const { data, error } = await supabase.auth.signInWithOAuth({
provider: "github",
options: {
Expand All @@ -12,5 +14,7 @@ export async function action({ request }: ActionFunctionArgs) {

if (error) throw error;

return redirect(data.url);
return redirect(data.url, {
headers,
});
}
3 changes: 2 additions & 1 deletion packages/client/app/routes/forgot-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { json, redirect } from "@remix-run/node";
import { Card, CardHeader, CardContent } from "@data-river/shared/ui";
import { AuthLayout } from "~/components/layout/auth-layout";
import { ForgotPasswordForm } from "~/components/auth/forgot-password-form";
import { supabase } from "~/utils/supabase.server";
import { createClient } from "~/utils/supabase.server";
import { getSession } from "~/utils/session.server";
import { useActionData } from "@remix-run/react";

Expand Down Expand Up @@ -38,6 +38,7 @@ export async function action({ request }: ActionFunctionArgs) {
const email = formData.get("email") as string;

try {
const { supabase } = await createClient(request);
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${new URL(request.url).origin}/reset-password`,
});
Expand Down
13 changes: 10 additions & 3 deletions packages/client/app/routes/sign-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { json, redirect } from "@remix-run/node";
import { Card, CardHeader, CardContent } from "@data-river/shared/ui";
import { AuthLayout } from "~/components/layout/auth-layout";
import { AuthForm } from "~/components/auth/auth-form";
import { supabase } from "~/utils/supabase.server";
import { createClient } from "~/utils/supabase.server";
import { getSession, commitSession } from "~/utils/session.server";
import { useActionData } from "@remix-run/react";
import { useEffect } from "react";
Expand All @@ -21,9 +21,13 @@ export const meta: MetaFunction = () => {
};

export async function loader({ request }: LoaderFunctionArgs) {
const session = await getSession(request.headers.get("Cookie"));
const { supabase } = await createClient(request, true);

if (session.has("access_token")) {
const {
data: { user },
} = await supabase.auth.getUser();

if (user) {
return redirect("/editor");
}

Expand All @@ -36,6 +40,7 @@ export async function action({ request }: ActionFunctionArgs) {
const password = formData.get("password") as string;

try {
const { supabase } = await createClient(request, true);
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
Expand All @@ -46,6 +51,8 @@ export async function action({ request }: ActionFunctionArgs) {
const session = await getSession(request.headers.get("Cookie"));
session.set("access_token", data.session.access_token);
session.set("user_id", data.user.id);
session.set("refresh_token", data.session.refresh_token);
session.set("expires_at", data.session.expires_at);

return redirect("/editor", {
headers: {
Expand Down
Loading

0 comments on commit e00d33e

Please sign in to comment.