From 775133281ebea6fb3e25e70d4abf8469ce9b7ef0 Mon Sep 17 00:00:00 2001 From: juan Date: Thu, 7 Aug 2025 18:23:45 +0200 Subject: [PATCH] feat: integrate Supabase for authentication and add routing for login, signup, and logout --- package.json | 2 + src/integrations/supabase/supabase.ts | 24 ++++++ src/routeTree.gen.ts | 109 +++++++++++++++++++++++++- src/routes/__root.tsx | 107 +++++++++++++++---------- src/routes/_authed.tsx | 39 +++++++++ src/routes/_authed/post.tsx | 27 +++++++ src/routes/index.tsx | 76 +++++++++--------- src/routes/login.tsx | 59 ++++++++++++++ src/routes/logout.tsx | 24 ++++++ src/routes/signup.tsx | 75 ++++++++++++++++++ 10 files changed, 461 insertions(+), 81 deletions(-) create mode 100644 src/integrations/supabase/supabase.ts create mode 100644 src/routes/_authed.tsx create mode 100644 src/routes/_authed/post.tsx create mode 100644 src/routes/login.tsx create mode 100644 src/routes/logout.tsx create mode 100644 src/routes/signup.tsx diff --git a/package.json b/package.json index 39bc49b..676f569 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ }, "dependencies": { "@heroui/react": "^2.8.2", + "@supabase/ssr": "^0.6.1", + "@supabase/supabase-js": "^2.53.1", "@tailwindcss/vite": "^4.1.11", "@tanstack/react-query": "^5.84.1", "@tanstack/react-query-devtools": "^5.84.1", diff --git a/src/integrations/supabase/supabase.ts b/src/integrations/supabase/supabase.ts new file mode 100644 index 0000000..39ad47a --- /dev/null +++ b/src/integrations/supabase/supabase.ts @@ -0,0 +1,24 @@ +import { parseCookies, setCookie } from '@tanstack/react-start/server' +import { createServerClient } from '@supabase/ssr' + +export function getSupabaseServerClient() { + return createServerClient( + process.env.SUPABASE_URL!, + process.env.SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return Object.entries(parseCookies()).map(([name, value]) => ({ + name, + value, + })) + }, + setAll(cookies) { + cookies.forEach((cookie) => { + setCookie(cookie.name, cookie.value) + }) + }, + }, + }, + ) +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index d204c26..6f037e6 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -9,38 +9,119 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as SignupRouteImport } from './routes/signup' +import { Route as LogoutRouteImport } from './routes/logout' +import { Route as LoginRouteImport } from './routes/login' +import { Route as AuthedRouteImport } from './routes/_authed' import { Route as IndexRouteImport } from './routes/index' +import { Route as AuthedPostRouteImport } from './routes/_authed/post' +const SignupRoute = SignupRouteImport.update({ + id: '/signup', + path: '/signup', + getParentRoute: () => rootRouteImport, +} as any) +const LogoutRoute = LogoutRouteImport.update({ + id: '/logout', + path: '/logout', + getParentRoute: () => rootRouteImport, +} as any) +const LoginRoute = LoginRouteImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => rootRouteImport, +} as any) +const AuthedRoute = AuthedRouteImport.update({ + id: '/_authed', + getParentRoute: () => rootRouteImport, +} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', getParentRoute: () => rootRouteImport, } as any) +const AuthedPostRoute = AuthedPostRouteImport.update({ + id: '/post', + path: '/post', + getParentRoute: () => AuthedRoute, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/login': typeof LoginRoute + '/logout': typeof LogoutRoute + '/signup': typeof SignupRoute + '/post': typeof AuthedPostRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/login': typeof LoginRoute + '/logout': typeof LogoutRoute + '/signup': typeof SignupRoute + '/post': typeof AuthedPostRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/_authed': typeof AuthedRouteWithChildren + '/login': typeof LoginRoute + '/logout': typeof LogoutRoute + '/signup': typeof SignupRoute + '/_authed/post': typeof AuthedPostRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' + fullPaths: '/' | '/login' | '/logout' | '/signup' | '/post' fileRoutesByTo: FileRoutesByTo - to: '/' - id: '__root__' | '/' + to: '/' | '/login' | '/logout' | '/signup' | '/post' + id: + | '__root__' + | '/' + | '/_authed' + | '/login' + | '/logout' + | '/signup' + | '/_authed/post' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + AuthedRoute: typeof AuthedRouteWithChildren + LoginRoute: typeof LoginRoute + LogoutRoute: typeof LogoutRoute + SignupRoute: typeof SignupRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/signup': { + id: '/signup' + path: '/signup' + fullPath: '/signup' + preLoaderRoute: typeof SignupRouteImport + parentRoute: typeof rootRouteImport + } + '/logout': { + id: '/logout' + path: '/logout' + fullPath: '/logout' + preLoaderRoute: typeof LogoutRouteImport + parentRoute: typeof rootRouteImport + } + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginRouteImport + parentRoute: typeof rootRouteImport + } + '/_authed': { + id: '/_authed' + path: '' + fullPath: '' + preLoaderRoute: typeof AuthedRouteImport + parentRoute: typeof rootRouteImport + } '/': { id: '/' path: '/' @@ -48,11 +129,33 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/_authed/post': { + id: '/_authed/post' + path: '/post' + fullPath: '/post' + preLoaderRoute: typeof AuthedPostRouteImport + parentRoute: typeof AuthedRoute + } } } +interface AuthedRouteChildren { + AuthedPostRoute: typeof AuthedPostRoute +} + +const AuthedRouteChildren: AuthedRouteChildren = { + AuthedPostRoute: AuthedPostRoute, +} + +const AuthedRouteWithChildren = + AuthedRoute._addFileChildren(AuthedRouteChildren) + const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + AuthedRoute: AuthedRouteWithChildren, + LoginRoute: LoginRoute, + LogoutRoute: LogoutRoute, + SignupRoute: SignupRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 24fd9da..7d299d7 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,53 +1,74 @@ -import css from "@styles/globals.css?url" -import type { QueryClient } from "@tanstack/react-query" +import css from "@styles/globals.css?url"; +import type { QueryClient } from "@tanstack/react-query"; import { - createRootRouteWithContext, - HeadContent, - Scripts -} from "@tanstack/react-router" -import { TanStackRouterDevtools } from "@tanstack/react-router-devtools" -import { HeroUIProvider } from "@/integrations/heroui/provider" + createRootRouteWithContext, + HeadContent, + Scripts, +} from "@tanstack/react-router"; +import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; +import { HeroUIProvider } from "@/integrations/heroui/provider"; +import { createServerFn } from "@tanstack/react-start"; +import { getSupabaseServerClient } from "@/integrations/supabase/supabase"; interface MyRouterContext { - queryClient: QueryClient + queryClient: QueryClient; + user: null; } +const fetchUser = createServerFn({ method: "GET" }).handler(async () => { + const supabase = getSupabaseServerClient(); + const { data, error: _error } = await supabase.auth.getUser(); + + if (!data.user?.email) { + return null; + } + + return { + email: data.user.email, + }; +}); export const Route = createRootRouteWithContext()({ - head: () => ({ - meta: [ - { - charSet: "utf-8" - }, - { - name: "viewport", - content: "width=device-width, initial-scale=1" - }, - { - title: "TanStack Start Starter" - } - ], - links: [ - { - rel: "stylesheet", - href: css - } - ] - }), + beforeLoad: async () => { + const user = await fetchUser(); + return { + user, + }; + }, + head: () => ({ + meta: [ + { + charSet: "utf-8", + }, + { + name: "viewport", + content: "width=device-width, initial-scale=1", + }, + { + title: "TanStack Start Starter", + }, + ], + links: [ + { + rel: "stylesheet", + href: css, + }, + ], + }), - shellComponent: RootDocument -}) + shellComponent: RootDocument, +}); function RootDocument({ children }: { children: React.ReactNode }) { - return ( - - - - - - {children} - - - - - ) + return ( + + + + + + {children} + + + + + ); } diff --git a/src/routes/_authed.tsx b/src/routes/_authed.tsx new file mode 100644 index 0000000..31a35bf --- /dev/null +++ b/src/routes/_authed.tsx @@ -0,0 +1,39 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { createServerFn } from "@tanstack/react-start"; +// import { Login } from "../components/Login"; +import { getSupabaseServerClient } from "@/integrations/supabase/supabase"; + +export const loginFn = createServerFn({ method: "POST" }) + .validator((d: { email: string; password: string }) => d) + .handler(async ({ data }) => { + const supabase = getSupabaseServerClient(); + const response = await supabase.auth.signInWithPassword({ + email: data.email, + password: data.password, + }); + console.log(response); + if (response.error) { + return { + error: true, + message: response.error.message, + }; + } + }); + +export const Route = createFileRoute("/_authed")({ + beforeLoad: ({ context }) => { + console.log("contextw", context); + if (!context?.user) { + throw new Error("Not authenticated"); + } + }, + errorComponent: ({ error }) => { + if (error.message === "Not authenticated") { +

+ Not authenticated. Please login. +

; + } + + throw error; + }, +}); diff --git a/src/routes/_authed/post.tsx b/src/routes/_authed/post.tsx new file mode 100644 index 0000000..4b1231c --- /dev/null +++ b/src/routes/_authed/post.tsx @@ -0,0 +1,27 @@ +import { Button } from "@heroui/react"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_authed/post")({ + component: RouteComponent, +}); + +function RouteComponent() { + const navigate = Route.useNavigate(); + return ( +
+ Hello "/_authed/post"!{" "} +
+ {" "} + +
+
+ ); +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index f262a1f..c58fa41 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,39 +1,45 @@ -import logo from "@assets/logo.svg" -import { createFileRoute } from "@tanstack/react-router" +import logo from "@assets/logo.svg"; +import { Button } from "@heroui/react"; +import { createFileRoute, getRouteApi } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ - component: App -}) - + component: App, +}); +const apiRouter = getRouteApi("/"); function App() { - return ( -
-
- logo -

- Edit src/routes/index.tsx and save to reload. -

- - Learn React - - - Learn TanStack - -
-
- ) + const navigate = apiRouter.useNavigate(); + return ( +
+
+ logo +

+ Edit src/routes/index.tsx and save to reload. +

+
+ + +
+
+
+ ); } diff --git a/src/routes/login.tsx b/src/routes/login.tsx new file mode 100644 index 0000000..27acd90 --- /dev/null +++ b/src/routes/login.tsx @@ -0,0 +1,59 @@ +import { useMutation } from "@tanstack/react-query"; +import { createFileRoute, getRouteApi } from "@tanstack/react-router"; +import { useServerFn } from "@tanstack/react-start"; +import { loginFn } from "./_authed"; +import { Button, Form, Input } from "@heroui/react"; + +export const Route = createFileRoute("/login")({ + component: LoginComp, +}); +const apiRouter = getRouteApi("/login"); +function LoginComp() { + const navigate = apiRouter.useNavigate(); + const loginFunction = useServerFn(loginFn); + const loginMutation = useMutation({ + mutationKey: ["login"], + mutationFn: async (data: { email: string; password: string }) => { + return loginFunction({ + data: { + email: data.email, + password: data.password, + }, + }); + }, + onSuccess: (data, ctx) => { + console.log("Login successful", data); + console.log("ctx", ctx); + + if (data?.error) { + alert(data.message); + return; + } + navigate({ + to: "/post", + }); + }, + onError: (error) => { + console.log("No se ha podido procesar el login", error); + }, + }); + return ( +
+

Login

+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const email = formData.get("email") as string; + const password = formData.get("password") as string; + loginMutation.mutate({ email, password }); + }} + > + + + +
+
+ ); +} diff --git a/src/routes/logout.tsx b/src/routes/logout.tsx new file mode 100644 index 0000000..85d55c3 --- /dev/null +++ b/src/routes/logout.tsx @@ -0,0 +1,24 @@ +import { getSupabaseServerClient } from "@/integrations/supabase/supabase"; +import { redirect, createFileRoute } from "@tanstack/react-router"; +import { createServerFn } from "@tanstack/react-start"; + +const logoutFn = createServerFn().handler(async () => { + const supabase = getSupabaseServerClient(); + const { error } = await supabase.auth.signOut(); + + if (error) { + return { + error: true, + message: error.message, + }; + } + + throw redirect({ + href: "/", + }); +}); + +export const Route = createFileRoute("/logout")({ + preload: false, + loader: () => logoutFn(), +}); diff --git a/src/routes/signup.tsx b/src/routes/signup.tsx new file mode 100644 index 0000000..27a2442 --- /dev/null +++ b/src/routes/signup.tsx @@ -0,0 +1,75 @@ +import { redirect, createFileRoute } from "@tanstack/react-router"; +import { createServerFn, useServerFn } from "@tanstack/react-start"; +import { getSupabaseServerClient } from "@/integrations/supabase/supabase"; +import { Button, Form, Input } from "@heroui/react"; +import { useMutation } from "@tanstack/react-query"; + +export const signupFn = createServerFn({ method: "POST" }) + .validator( + (d: { email: string; password: string; redirectUrl?: string }) => d + ) + .handler(async ({ data }) => { + const supabase = getSupabaseServerClient(); + const { error } = await supabase.auth.signUp({ + email: data.email, + password: data.password, + }); + if (error) { + return { + error: true, + message: error.message, + }; + } + + throw redirect({ + href: data.redirectUrl || "/", + }); + }); + +export const Route = createFileRoute("/signup")({ + component: SignupComp, +}); + +function SignupComp() { + const signup = useServerFn(signupFn); + const signupMutation = useMutation({ + mutationKey: ["signup"], + mutationFn: async (data: { + email: string; + password: string; + redirectUrl?: string; + }) => { + return signup({ + data: { + email: data.email, + password: data.password, + redirectUrl: data.redirectUrl, + }, + }); + }, + }); + return ( +
+

Signup

+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const email = formData.get("email") as string; + const password = formData.get("password") as string; + signupMutation.mutate({ + email, + password, + }); + }} + > + + + +
+
+ ); +}