Compare commits

...

4 Commits

Author SHA1 Message Date
juan
5d178709ef fix: update section header from 'Inicio' to 'Supabase' in README 2025-08-07 18:24:46 +02:00
juan
775133281e feat: integrate Supabase for authentication and add routing for login, signup, and logout 2025-08-07 18:23:45 +02:00
juan
d68a0113b2 add readme 2025-08-06 20:00:27 +02:00
juan
05a83e3bcb update gitignore 2025-08-06 19:48:57 +02:00
12 changed files with 473 additions and 82 deletions

12
.gitignore vendored
View File

@ -1 +1,11 @@
node_modules node_modules
.DS_Store
dist
dist-ssr
*.local
count.txt
.env
.nitro
.tanstack
.output
.vinxi

View File

@ -13,6 +13,8 @@
}, },
"dependencies": { "dependencies": {
"@heroui/react": "^2.8.2", "@heroui/react": "^2.8.2",
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.53.1",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.84.1", "@tanstack/react-query": "^5.84.1",
"@tanstack/react-query-devtools": "^5.84.1", "@tanstack/react-query-devtools": "^5.84.1",

1
readme.md Normal file
View File

@ -0,0 +1 @@
## Supabase

View File

@ -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)
})
},
},
},
)
}

View File

@ -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. // 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 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 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({ const IndexRoute = IndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AuthedPostRoute = AuthedPostRouteImport.update({
id: '/post',
path: '/post',
getParentRoute: () => AuthedRoute,
} as any)
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute
'/logout': typeof LogoutRoute
'/signup': typeof SignupRoute
'/post': typeof AuthedPostRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute
'/logout': typeof LogoutRoute
'/signup': typeof SignupRoute
'/post': typeof AuthedPostRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/_authed': typeof AuthedRouteWithChildren
'/login': typeof LoginRoute
'/logout': typeof LogoutRoute
'/signup': typeof SignupRoute
'/_authed/post': typeof AuthedPostRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' fullPaths: '/' | '/login' | '/logout' | '/signup' | '/post'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' to: '/' | '/login' | '/logout' | '/signup' | '/post'
id: '__root__' | '/' id:
| '__root__'
| '/'
| '/_authed'
| '/login'
| '/logout'
| '/signup'
| '/_authed/post'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
AuthedRoute: typeof AuthedRouteWithChildren
LoginRoute: typeof LoginRoute
LogoutRoute: typeof LogoutRoute
SignupRoute: typeof SignupRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath { 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: '/' id: '/'
path: '/' path: '/'
@ -48,11 +129,33 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport 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 = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
AuthedRoute: AuthedRouteWithChildren,
LoginRoute: LoginRoute,
LogoutRoute: LogoutRoute,
SignupRoute: SignupRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)

View File

@ -1,53 +1,74 @@
import css from "@styles/globals.css?url" import css from "@styles/globals.css?url";
import type { QueryClient } from "@tanstack/react-query" import type { QueryClient } from "@tanstack/react-query";
import { import {
createRootRouteWithContext, createRootRouteWithContext,
HeadContent, HeadContent,
Scripts Scripts,
} from "@tanstack/react-router" } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools" import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { HeroUIProvider } from "@/integrations/heroui/provider" import { HeroUIProvider } from "@/integrations/heroui/provider";
import { createServerFn } from "@tanstack/react-start";
import { getSupabaseServerClient } from "@/integrations/supabase/supabase";
interface MyRouterContext { 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<MyRouterContext>()({ export const Route = createRootRouteWithContext<MyRouterContext>()({
head: () => ({ beforeLoad: async () => {
meta: [ const user = await fetchUser();
{ return {
charSet: "utf-8" user,
}, };
{ },
name: "viewport", head: () => ({
content: "width=device-width, initial-scale=1" meta: [
}, {
{ charSet: "utf-8",
title: "TanStack Start Starter" },
} {
], name: "viewport",
links: [ content: "width=device-width, initial-scale=1",
{ },
rel: "stylesheet", {
href: css title: "TanStack Start Starter",
} },
] ],
}), links: [
{
rel: "stylesheet",
href: css,
},
],
}),
shellComponent: RootDocument shellComponent: RootDocument,
}) });
function RootDocument({ children }: { children: React.ReactNode }) { function RootDocument({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="es"> <html lang="es">
<head> <head>
<HeadContent /> <HeadContent />
</head> </head>
<body> <body>
<HeroUIProvider>{children}</HeroUIProvider> <HeroUIProvider>{children}</HeroUIProvider>
<TanStackRouterDevtools /> <TanStackRouterDevtools />
<Scripts /> <Scripts />
</body> </body>
</html> </html>
) );
} }

39
src/routes/_authed.tsx Normal file
View File

@ -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") {
<p>
Not authenticated. Please <a href="/login">login</a>.
</p>;
}
throw error;
},
});

View File

@ -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 (
<div>
Hello "/_authed/post"!{" "}
<div>
{" "}
<Button
onPress={() =>
navigate({
to: "/logout",
})
}
>
Logout
</Button>
</div>
</div>
);
}

View File

@ -1,39 +1,45 @@
import logo from "@assets/logo.svg" import logo from "@assets/logo.svg";
import { createFileRoute } from "@tanstack/react-router" import { Button } from "@heroui/react";
import { createFileRoute, getRouteApi } from "@tanstack/react-router";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
component: App component: App,
}) });
const apiRouter = getRouteApi("/");
function App() { function App() {
return ( const navigate = apiRouter.useNavigate();
<div className="text-center"> return (
<header className="min-h-screen flex flex-col items-center justify-center bg-[#282c34] text-white text-[calc(10px+2vmin)]"> <div className="text-center">
<img <header className="min-h-screen flex flex-col items-center justify-center bg-[#282c34] text-white text-[calc(10px+2vmin)]">
src={logo} <img
className="h-[40vmin] pointer-events-none animate-[spin_20s_linear_infinite]" src={logo}
alt="logo" className="h-[40vmin] pointer-events-none animate-[spin_20s_linear_infinite]"
/> alt="logo"
<p> />
Edit <code>src/routes/index.tsx</code> and save to reload. <p>
</p> Edit <code>src/routes/index.tsx</code> and save to reload.
<a </p>
className="text-[#61dafb] hover:underline" <div className="grid grid-cols-2 gap-4 mt-4">
href="https://reactjs.org" <Button
target="_blank" onPress={() =>
rel="noopener noreferrer" navigate({
> to: "/login",
Learn React })
</a> }
<a >
className="text-[#61dafb] hover:underline" Login
href="https://tanstack.com" </Button>
target="_blank" <Button
rel="noopener noreferrer" onPress={() => {
> navigate({
Learn TanStack to: "/signup",
</a> });
</header> }}
</div> >
) Signup
</Button>
</div>
</header>
</div>
);
} }

59
src/routes/login.tsx Normal file
View File

@ -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 (
<div className="flex justify-center items-center flex-col h-screen">
<p className="font-semibold mb-3">Login</p>
<Form
className="grid gap-2 max-w-sm w-full"
onSubmit={(e) => {
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 });
}}
>
<Input name="email" type="email" placeholder="Email" />
<Input name="password" type="password" placeholder="Password" />
<Button type="submit">Enviar</Button>
</Form>
</div>
);
}

24
src/routes/logout.tsx Normal file
View File

@ -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(),
});

75
src/routes/signup.tsx Normal file
View File

@ -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 (
<div className="flex justify-center items-center flex-col h-screen">
<p className="font-semibold mb-3">Signup</p>
<Form
className="grid gap-2 max-w-5xl w-full"
onSubmit={(e) => {
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,
});
}}
>
<Input name="email" type="email" placeholder="Email" />
<Input name="password" type="password" placeholder="Password" />
<Button type="submit" isLoading={signupMutation.isPending}>
Enviar
</Button>
</Form>
</div>
);
}