From 775133281ebea6fb3e25e70d4abf8469ce9b7ef0 Mon Sep 17 00:00:00 2001 From: juan Date: Thu, 7 Aug 2025 18:23:45 +0200 Subject: [PATCH 1/7] 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, + }); + }} + > + + + +
+
+ ); +} From 5d178709ef83b25470fc20df3b690ca49f0d8d89 Mon Sep 17 00:00:00 2001 From: juan Date: Thu, 7 Aug 2025 18:24:46 +0200 Subject: [PATCH 2/7] fix: update section header from 'Inicio' to 'Supabase' in README --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index c3f50b6..dd165b6 100644 --- a/readme.md +++ b/readme.md @@ -1 +1 @@ -## Inicio \ No newline at end of file +## Supabase \ No newline at end of file From 83b86ab0f0d8b46d20b32554287efac9aab2a945 Mon Sep 17 00:00:00 2001 From: Jrodenas Date: Thu, 7 Aug 2025 20:12:35 +0200 Subject: [PATCH 3/7] feat: implement Sonner for toast notifications --- .vscode/settings.json | 35 +++++++ package-lock.json | 141 +++++++++++++++++++++++++- package.json | 1 + src/integrations/sonner/provider.tsx | 13 +++ src/integrations/supabase/supabase.ts | 42 ++++---- src/router.tsx | 2 +- src/routes/__root.tsx | 124 +++++++++++----------- src/routes/_authed.tsx | 66 ++++++------ src/routes/_authed/post.tsx | 44 ++++---- src/routes/index.tsx | 82 +++++++-------- src/routes/login.tsx | 108 ++++++++++---------- src/routes/logout.tsx | 36 +++---- src/routes/signup.tsx | 134 ++++++++++++------------ 13 files changed, 506 insertions(+), 322 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/integrations/sonner/provider.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..70dd163 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,35 @@ +{ + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[css]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "editor.codeActionsOnSave": { + "source.organizeImports.biome": "explicit" + } +} diff --git a/package-lock.json b/package-lock.json index b54b21e..49e70d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,8 @@ "name": "template", "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", @@ -18,6 +20,7 @@ "framer-motion": "^12.23.12", "react": "^19.1.1", "react-dom": "^19.1.1", + "sonner": "^2.0.7", "tailwindcss": "^4.1.11", "vite-tsconfig-paths": "^5.1.4" }, @@ -6020,6 +6023,114 @@ "integrity": "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==", "license": "CC0-1.0" }, + "node_modules/@supabase/auth-js": { + "version": "2.71.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.71.1.tgz", + "integrity": "sha512-mMIQHBRc+SKpZFRB2qtupuzulaUhFYupNyxqDj5Jp/LyPvcWvjaJzZzObv6URtL/O6lPxkanASnotGtNpS3H2Q==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.5.tgz", + "integrity": "sha512-v5GSqb9zbosquTo6gBwIiq7W9eQ7rE5QazsK/ezNiQXdCbY+bH8D9qEaBIkhVvX4ZRW5rP03gEfw5yw9tiq4EQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/@supabase/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@supabase/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz", + "integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.15.0.tgz", + "integrity": "sha512-SEIWApsxyoAe68WU2/5PCCuBwa11LL4Bb8K3r2FHCt3ROpaTthmDiWEhnLMGayP05N4QeYrMk0kyTZOwid/Hjw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.13", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/ssr": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.6.1.tgz", + "integrity": "sha512-QtQgEMvaDzr77Mk3vZ3jWg2/y+D8tExYF7vcJT+wQ8ysuvOeGGjYbZlvj5bHYsj/SpC0bihcisnwPrM4Gp5G4g==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.43.4" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.10.4.tgz", + "integrity": "sha512-cvL02GarJVFcNoWe36VBybQqTVRq6wQSOCvTS64C+eyuxOruFIm1utZAY0xi2qKtHJO3EjKaj8iWJKySusDmAQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.54.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.54.0.tgz", + "integrity": "sha512-DLw83YwBfAaFiL3oWV26+sHRdeCGtxmIKccjh/Pndze3BWM4fZghzYKhk3ElOQU8Bluq4AkkCJ5bM5Szl/sfRg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.71.1", + "@supabase/functions-js": "2.4.5", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.19.4", + "@supabase/realtime-js": "2.15.0", + "@supabase/storage-js": "^2.10.4" + } + }, "node_modules/@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", @@ -7009,7 +7120,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", "license": "MIT", - "optional": true, "dependencies": { "undici-types": "~7.10.0" } @@ -7020,6 +7130,12 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "license": "MIT" }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.9", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", @@ -7052,6 +7168,15 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -12739,6 +12864,16 @@ "seroval-plugins": "~1.3.0" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", @@ -13443,8 +13578,7 @@ "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/unenv": { "version": "1.10.0", @@ -14411,7 +14545,6 @@ "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 676f569..8be49e2 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "framer-motion": "^12.23.12", "react": "^19.1.1", "react-dom": "^19.1.1", + "sonner": "^2.0.7", "tailwindcss": "^4.1.11", "vite-tsconfig-paths": "^5.1.4" }, diff --git a/src/integrations/sonner/provider.tsx b/src/integrations/sonner/provider.tsx new file mode 100644 index 0000000..7453594 --- /dev/null +++ b/src/integrations/sonner/provider.tsx @@ -0,0 +1,13 @@ +import { Toaster } from "sonner" + +export const SonnerProvider = () => { + return ( + + ) +} diff --git a/src/integrations/supabase/supabase.ts b/src/integrations/supabase/supabase.ts index 39ad47a..bdc3cb5 100644 --- a/src/integrations/supabase/supabase.ts +++ b/src/integrations/supabase/supabase.ts @@ -1,24 +1,24 @@ -import { parseCookies, setCookie } from '@tanstack/react-start/server' -import { createServerClient } from '@supabase/ssr' +import { createServerClient } from "@supabase/ssr" +import { parseCookies, setCookie } from "@tanstack/react-start/server" 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) - }) - }, - }, - }, - ) + return createServerClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + { + 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/router.tsx b/src/router.tsx index 1f1e176..b2d7371 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -10,7 +10,7 @@ export const createRouter = () => { return routerWithQueryClient( createTanstackRouter({ routeTree, - context: { ...rqContext }, + context: { ...rqContext, user: null }, defaultPreload: "intent", Wrap: (props: { children: React.ReactNode }) => { return {props.children} diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 7d299d7..18e3354 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,74 +1,76 @@ -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"; -import { createServerFn } from "@tanstack/react-start"; -import { getSupabaseServerClient } from "@/integrations/supabase/supabase"; + createRootRouteWithContext, + HeadContent, + Scripts +} from "@tanstack/react-router" +import { TanStackRouterDevtools } from "@tanstack/react-router-devtools" +import { createServerFn } from "@tanstack/react-start" +import { HeroUIProvider } from "@/integrations/heroui/provider" +import { SonnerProvider } from "@/integrations/sonner/provider" +import { getSupabaseServerClient } from "@/integrations/supabase/supabase" interface MyRouterContext { - queryClient: QueryClient; - user: null; + queryClient: QueryClient + user: null } const fetchUser = createServerFn({ method: "GET" }).handler(async () => { - const supabase = getSupabaseServerClient(); - const { data, error: _error } = await supabase.auth.getUser(); + const supabase = getSupabaseServerClient() + const { data, error: _error } = await supabase.auth.getUser() - if (!data.user?.email) { - return null; - } + if (!data.user?.email) { + return null + } - return { - email: data.user.email, - }; -}); + return { + email: data.user.email + } +}) export const Route = createRootRouteWithContext()({ - 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, - }, - ], - }), + 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 index 31a35bf..3761792 100644 --- a/src/routes/_authed.tsx +++ b/src/routes/_authed.tsx @@ -1,39 +1,39 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { createServerFn } from "@tanstack/react-start"; +import { createFileRoute } from "@tanstack/react-router" +import { createServerFn } from "@tanstack/react-start" // import { Login } from "../components/Login"; -import { getSupabaseServerClient } from "@/integrations/supabase/supabase"; +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, - }; - } - }); + .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. -

; - } + 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; - }, -}); + throw error + } +}) diff --git a/src/routes/_authed/post.tsx b/src/routes/_authed/post.tsx index 4b1231c..d3925ba 100644 --- a/src/routes/_authed/post.tsx +++ b/src/routes/_authed/post.tsx @@ -1,27 +1,27 @@ -import { Button } from "@heroui/react"; -import { createFileRoute } from "@tanstack/react-router"; +import { Button } from "@heroui/react" +import { createFileRoute } from "@tanstack/react-router" export const Route = createFileRoute("/_authed/post")({ - component: RouteComponent, -}); + component: RouteComponent +}) function RouteComponent() { - const navigate = Route.useNavigate(); - return ( -
- Hello "/_authed/post"!{" "} -
- {" "} - -
-
- ); + const navigate = Route.useNavigate() + return ( +
+ Hello "/_authed/post"!{" "} +
+ {" "} + +
+
+ ) } diff --git a/src/routes/index.tsx b/src/routes/index.tsx index c58fa41..fc2ac80 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,45 +1,45 @@ -import logo from "@assets/logo.svg"; -import { Button } from "@heroui/react"; -import { createFileRoute, getRouteApi } 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, -}); -const apiRouter = getRouteApi("/"); + component: App +}) +const apiRouter = getRouteApi("/") function App() { - const navigate = apiRouter.useNavigate(); - return ( -
-
- logo -

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

-
- - -
-
-
- ); + 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 index 27acd90..aadd065 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -1,59 +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"; +import { Button, Form, Input } from "@heroui/react" +import { useMutation } from "@tanstack/react-query" +import { createFileRoute, getRouteApi } from "@tanstack/react-router" +import { useServerFn } from "@tanstack/react-start" +import { loginFn } from "./_authed" export const Route = createFileRoute("/login")({ - component: LoginComp, -}); -const apiRouter = getRouteApi("/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); + 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 }); - }} - > - - - -
-
- ); + 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 index 85d55c3..7f8de18 100644 --- a/src/routes/logout.tsx +++ b/src/routes/logout.tsx @@ -1,24 +1,24 @@ -import { getSupabaseServerClient } from "@/integrations/supabase/supabase"; -import { redirect, createFileRoute } from "@tanstack/react-router"; -import { createServerFn } from "@tanstack/react-start"; +import { createFileRoute, redirect } from "@tanstack/react-router" +import { createServerFn } from "@tanstack/react-start" +import { getSupabaseServerClient } from "@/integrations/supabase/supabase" const logoutFn = createServerFn().handler(async () => { - const supabase = getSupabaseServerClient(); - const { error } = await supabase.auth.signOut(); + const supabase = getSupabaseServerClient() + const { error } = await supabase.auth.signOut() - if (error) { - return { - error: true, - message: error.message, - }; - } + if (error) { + return { + error: true, + message: error.message + } + } - throw redirect({ - href: "/", - }); -}); + throw redirect({ + href: "/" + }) +}) export const Route = createFileRoute("/logout")({ - preload: false, - loader: () => logoutFn(), -}); + preload: false, + loader: () => logoutFn() +}) diff --git a/src/routes/signup.tsx b/src/routes/signup.tsx index 27a2442..09454f3 100644 --- a/src/routes/signup.tsx +++ b/src/routes/signup.tsx @@ -1,75 +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"; +import { Button, Form, Input } from "@heroui/react" +import { useMutation } from "@tanstack/react-query" +import { createFileRoute, redirect } from "@tanstack/react-router" +import { createServerFn, useServerFn } from "@tanstack/react-start" +import { getSupabaseServerClient } from "@/integrations/supabase/supabase" 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, - }; - } + .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 || "/", - }); - }); + throw redirect({ + href: data.redirectUrl || "/" + }) + }) export const Route = createFileRoute("/signup")({ - component: SignupComp, -}); + 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, - }); - }} - > - - - -
-
- ); + 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 + }) + }} + > + + + +
+
+ ) } From 51c7b9f86d869d9b7393c7dbe8b10f9e6be0c576 Mon Sep 17 00:00:00 2001 From: juan Date: Sun, 10 Aug 2025 16:37:53 +0200 Subject: [PATCH 4/7] feat: refactor authentication routes and add toast notifications for login/signup/logout actions --- readme.md | 3 +- src/lib/mutations/mutationLogin.tsx | 61 ++++++++++++++++++++++++++++ src/lib/mutations/mutationSignup.tsx | 58 ++++++++++++++++++++++++++ src/routes/_authed.tsx | 19 --------- src/routes/login.tsx | 36 ++-------------- src/routes/logout.tsx | 2 + src/routes/signup.tsx | 46 ++------------------- 7 files changed, 129 insertions(+), 96 deletions(-) create mode 100644 src/lib/mutations/mutationLogin.tsx create mode 100644 src/lib/mutations/mutationSignup.tsx diff --git a/readme.md b/readme.md index dd165b6..178c352 100644 --- a/readme.md +++ b/readme.md @@ -1 +1,2 @@ -## Supabase \ No newline at end of file +## Supabase +Usuarios de prueba demo12 - juan.penalver@outlook.com \ No newline at end of file diff --git a/src/lib/mutations/mutationLogin.tsx b/src/lib/mutations/mutationLogin.tsx new file mode 100644 index 0000000..587ded3 --- /dev/null +++ b/src/lib/mutations/mutationLogin.tsx @@ -0,0 +1,61 @@ +import { useMutation } from "@tanstack/react-query" +import { getRouteApi } from "@tanstack/react-router" +import { createServerFn, useServerFn } from "@tanstack/react-start" +import { toast } from "sonner" +import { getSupabaseServerClient } from "@/integrations/supabase/supabase" + +const apiRouter = getRouteApi("/login") + +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 mutationLogin = () => { + const navigate = apiRouter.useNavigate() + const loginFunction = useServerFn(loginFn) + const mutation = useMutation({ + mutationKey: ["login"], + mutationFn: async (data: { email: string; password: string }) => { + toast.loading("Logging in...", { id: "login" }) + return loginFunction({ + data: { + email: data.email, + password: data.password + } + }) + }, + onSuccess: (data, ctx) => { + console.log("Login successful", data) + console.log("ctx", ctx) + + if (data?.error) { + toast.error(data.message) + return + } + toast.success("Login successful! Redirecting to posts...", { + id: "login" + }) + navigate({ + to: "/post" + }) + }, + onError: (error) => { + toast.error("Login failed. Please try again.", { id: "login" }) + console.log("No se ha podido procesar el login", error) + } + }) + + return mutation +} diff --git a/src/lib/mutations/mutationSignup.tsx b/src/lib/mutations/mutationSignup.tsx new file mode 100644 index 0000000..57b5aa4 --- /dev/null +++ b/src/lib/mutations/mutationSignup.tsx @@ -0,0 +1,58 @@ +import { useMutation } from "@tanstack/react-query" +import { getRouteApi, redirect } from "@tanstack/react-router" +import { createServerFn, useServerFn } from "@tanstack/react-start" +import { toast } from "sonner" +import { getSupabaseServerClient } from "@/integrations/supabase/supabase" + +const apiRouter = getRouteApi("/signup") +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 mutationSignup = () => { + const navigate = apiRouter.useNavigate() + const signup = useServerFn(signupFn) + const mutation = useMutation({ + mutationKey: ["signup"], + mutationFn: async (data: { + email: string + password: string + redirectUrl?: string + }) => { + toast.loading("Signing up...", { id: "signup" }) + return signup({ + data: { + email: data.email, + password: data.password, + redirectUrl: data.redirectUrl + } + }) + }, + onSuccess: () => { + toast.success("Signup successful! Redirecting to login...", { id: "signup" }) + navigate({ + to: "/login" + }) + } + }) + + return mutation +} diff --git a/src/routes/_authed.tsx b/src/routes/_authed.tsx index 3761792..052bb7e 100644 --- a/src/routes/_authed.tsx +++ b/src/routes/_authed.tsx @@ -1,24 +1,5 @@ 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 }) => { diff --git a/src/routes/login.tsx b/src/routes/login.tsx index aadd065..01ce9e9 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -1,42 +1,12 @@ import { Button, Form, Input } from "@heroui/react" -import { useMutation } from "@tanstack/react-query" -import { createFileRoute, getRouteApi } from "@tanstack/react-router" -import { useServerFn } from "@tanstack/react-start" -import { loginFn } from "./_authed" +import { createFileRoute } from "@tanstack/react-router" +import { mutationLogin } from "@/lib/mutations/mutationLogin" 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) - } - }) + const loginMutation = mutationLogin(); return (

Login

diff --git a/src/routes/logout.tsx b/src/routes/logout.tsx index 7f8de18..b5bf84e 100644 --- a/src/routes/logout.tsx +++ b/src/routes/logout.tsx @@ -1,5 +1,6 @@ import { createFileRoute, redirect } from "@tanstack/react-router" import { createServerFn } from "@tanstack/react-start" +import { toast } from "sonner" import { getSupabaseServerClient } from "@/integrations/supabase/supabase" const logoutFn = createServerFn().handler(async () => { @@ -7,6 +8,7 @@ const logoutFn = createServerFn().handler(async () => { const { error } = await supabase.auth.signOut() if (error) { + toast.error("Logout failed. Please try again.") return { error: true, message: error.message diff --git a/src/routes/signup.tsx b/src/routes/signup.tsx index 09454f3..5c36830 100644 --- a/src/routes/signup.tsx +++ b/src/routes/signup.tsx @@ -1,53 +1,13 @@ import { Button, Form, Input } from "@heroui/react" -import { useMutation } from "@tanstack/react-query" -import { createFileRoute, redirect } from "@tanstack/react-router" -import { createServerFn, useServerFn } from "@tanstack/react-start" -import { getSupabaseServerClient } from "@/integrations/supabase/supabase" - -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 || "/" - }) - }) +import { createFileRoute } from "@tanstack/react-router" +import { mutationSignup } from "@/lib/mutations/mutationSignup" 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 - } - }) - } - }) + const signupMutation = mutationSignup() return (

Signup

From e45772e2a975546fefe2aaca3b54731da56e5ece Mon Sep 17 00:00:00 2001 From: juan Date: Sun, 10 Aug 2025 18:30:07 +0200 Subject: [PATCH 5/7] feat: add validation schemas for login and signup, integrate validation in respective components --- package-lock.json | 54 +++++++++++++++++++++++++--- package.json | 3 +- src/lib/hooks/useValidation.tsx | 37 +++++++++++++++++++ src/lib/mutations/mutationLogin.tsx | 10 +++--- src/lib/mutations/mutationSignup.tsx | 9 ++--- src/lib/schemas/login.ts | 6 ++++ src/lib/schemas/signup.ts | 7 ++++ src/routes/login.tsx | 26 +++++++++++--- src/routes/signup.tsx | 25 +++++++++---- 9 files changed, 150 insertions(+), 27 deletions(-) create mode 100644 src/lib/hooks/useValidation.tsx create mode 100644 src/lib/schemas/login.ts create mode 100644 src/lib/schemas/signup.ts diff --git a/package-lock.json b/package-lock.json index 49e70d7..f39ae46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,8 @@ "react-dom": "^19.1.1", "sonner": "^2.0.7", "tailwindcss": "^4.1.11", - "vite-tsconfig-paths": "^5.1.4" + "vite-tsconfig-paths": "^5.1.4", + "zod": "^4.0.17" }, "devDependencies": { "@biomejs/biome": "2.1.3", @@ -3663,6 +3664,15 @@ "node": ">=10" } }, + "node_modules/@netlify/zip-it-and-ship-it/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6632,6 +6642,15 @@ "vite": ">=6.0.0" } }, + "node_modules/@tanstack/react-start-plugin/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@tanstack/react-start-server": { "version": "1.130.12", "resolved": "https://registry.npmjs.org/@tanstack/react-start-server/-/react-start-server-1.130.12.tgz", @@ -6767,6 +6786,15 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/router-generator/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@tanstack/router-plugin": { "version": "1.130.15", "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.130.15.tgz", @@ -6820,6 +6848,15 @@ } } }, + "node_modules/@tanstack/router-plugin/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@tanstack/router-utils": { "version": "1.130.12", "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.130.12.tgz", @@ -6938,6 +6975,15 @@ "node": ">=6.9.0" } }, + "node_modules/@tanstack/start-plugin-core/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@tanstack/start-server-core": { "version": "1.130.12", "resolved": "https://registry.npmjs.org/@tanstack/start-server-core/-/start-server-core-1.130.12.tgz", @@ -14740,9 +14786,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.17.tgz", + "integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 8be49e2..1de3c7e 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "react-dom": "^19.1.1", "sonner": "^2.0.7", "tailwindcss": "^4.1.11", - "vite-tsconfig-paths": "^5.1.4" + "vite-tsconfig-paths": "^5.1.4", + "zod": "^4.0.17" }, "devDependencies": { "@biomejs/biome": "2.1.3", diff --git a/src/lib/hooks/useValidation.tsx b/src/lib/hooks/useValidation.tsx new file mode 100644 index 0000000..b89253c --- /dev/null +++ b/src/lib/hooks/useValidation.tsx @@ -0,0 +1,37 @@ +import { useState } from "react" +import type { z } from "zod" + +type FormDataValidation = Record + +export const useValidation = ({ + defaultSchema +}: { + defaultSchema?: z.ZodSchema +}) => { + const [errors, setErrors] = useState() + + const validate = ({ + formData, + schema + }: { + formData: FormDataValidation + schema?: z.ZodSchema + }) => { + const result = + schema?.safeParse(formData) ?? defaultSchema?.safeParse(formData) + + if (!result) { + throw new Error("No schema provided") + } + + if (!result.success) { + setErrors(result.error.flatten().fieldErrors as T) + return false + } + + setErrors(undefined) + return true + } + + return { errors, validate } +} diff --git a/src/lib/mutations/mutationLogin.tsx b/src/lib/mutations/mutationLogin.tsx index 587ded3..fe9fc1b 100644 --- a/src/lib/mutations/mutationLogin.tsx +++ b/src/lib/mutations/mutationLogin.tsx @@ -3,11 +3,12 @@ import { getRouteApi } from "@tanstack/react-router" import { createServerFn, useServerFn } from "@tanstack/react-start" import { toast } from "sonner" import { getSupabaseServerClient } from "@/integrations/supabase/supabase" +import { loginSchema } from "../schemas/login" const apiRouter = getRouteApi("/login") export const loginFn = createServerFn({ method: "POST" }) - .validator((d: { email: string; password: string }) => d) + .validator(loginSchema) .handler(async ({ data }) => { const supabase = getSupabaseServerClient() const response = await supabase.auth.signInWithPassword({ @@ -36,12 +37,9 @@ export const mutationLogin = () => { } }) }, - onSuccess: (data, ctx) => { - console.log("Login successful", data) - console.log("ctx", ctx) - + onSuccess: (data) => { if (data?.error) { - toast.error(data.message) + toast.error(data.message, { id: "login" }) return } toast.success("Login successful! Redirecting to posts...", { diff --git a/src/lib/mutations/mutationSignup.tsx b/src/lib/mutations/mutationSignup.tsx index 57b5aa4..a4f8596 100644 --- a/src/lib/mutations/mutationSignup.tsx +++ b/src/lib/mutations/mutationSignup.tsx @@ -3,12 +3,11 @@ import { getRouteApi, redirect } from "@tanstack/react-router" import { createServerFn, useServerFn } from "@tanstack/react-start" import { toast } from "sonner" import { getSupabaseServerClient } from "@/integrations/supabase/supabase" +import { signupSchema } from "../schemas/signup" const apiRouter = getRouteApi("/signup") export const signupFn = createServerFn({ method: "POST" }) - .validator( - (d: { email: string; password: string; redirectUrl?: string }) => d - ) + .validator(signupSchema) .handler(async ({ data }) => { const supabase = getSupabaseServerClient() const { error } = await supabase.auth.signUp({ @@ -47,7 +46,9 @@ export const mutationSignup = () => { }) }, onSuccess: () => { - toast.success("Signup successful! Redirecting to login...", { id: "signup" }) + toast.success("Signup successful! Redirecting to login...", { + id: "signup" + }) navigate({ to: "/login" }) diff --git a/src/lib/schemas/login.ts b/src/lib/schemas/login.ts new file mode 100644 index 0000000..e9ba067 --- /dev/null +++ b/src/lib/schemas/login.ts @@ -0,0 +1,6 @@ +import z from "zod" + +export const loginSchema = z.object({ + email: z.email("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters long") +}) diff --git a/src/lib/schemas/signup.ts b/src/lib/schemas/signup.ts new file mode 100644 index 0000000..93b081b --- /dev/null +++ b/src/lib/schemas/signup.ts @@ -0,0 +1,7 @@ +import z from "zod" + +export const signupSchema = z.object({ + email: z.email("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters long"), + redirectUrl: z.string().optional() +}) diff --git a/src/routes/login.tsx b/src/routes/login.tsx index 01ce9e9..33aa73a 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -1,28 +1,44 @@ import { Button, Form, Input } from "@heroui/react" import { createFileRoute } from "@tanstack/react-router" +import { useValidation } from "@/lib/hooks/useValidation" import { mutationLogin } from "@/lib/mutations/mutationLogin" +import { loginSchema } from "@/lib/schemas/login" export const Route = createFileRoute("/login")({ component: LoginComp }) + + + function LoginComp() { - const loginMutation = mutationLogin(); + const loginMutation = mutationLogin() + const { errors, validate } = useValidation({ + defaultSchema: loginSchema + }) 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 }) + if ( + validate({ + formData: { email, password } + }) + ) + loginMutation.mutate({ email, password }) }} > - - - + + +
) diff --git a/src/routes/signup.tsx b/src/routes/signup.tsx index 5c36830..7fa08b9 100644 --- a/src/routes/signup.tsx +++ b/src/routes/signup.tsx @@ -1,6 +1,8 @@ import { Button, Form, Input } from "@heroui/react" import { createFileRoute } from "@tanstack/react-router" +import { useValidation } from "@/lib/hooks/useValidation" import { mutationSignup } from "@/lib/mutations/mutationSignup" +import { signupSchema } from "@/lib/schemas/signup" export const Route = createFileRoute("/signup")({ component: SignupComp @@ -8,24 +10,33 @@ export const Route = createFileRoute("/signup")({ function SignupComp() { const signupMutation = mutationSignup() + const { errors, validate } = useValidation({ + defaultSchema: signupSchema + }) 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 - }) + if ( + validate({ + formData: { email, password } + }) + ) + signupMutation.mutate({ + email, + password + }) }} > - - + + From a2ae7d5b5a7df82884948c2c66c7fee99b615fe7 Mon Sep 17 00:00:00 2001 From: Jrodenas Date: Sun, 10 Aug 2025 20:16:27 +0200 Subject: [PATCH 6/7] feat: refactor authentication flow, implement user hooks, and add validation schemas for login/signup --- biome.json | 5 +- src/integrations/heroui/provider.tsx | 2 +- src/lib/db/user.ts | 84 ++++++++++++++++++++++++++++ src/lib/hooks/useValidation.tsx | 3 +- src/lib/hooks/user/useLogin.tsx | 54 ++++++++++++++++++ src/lib/hooks/user/useSignup.tsx | 44 +++++++++++++++ src/lib/mutations/mutationLogin.tsx | 59 ------------------- src/lib/mutations/mutationSignup.tsx | 59 ------------------- src/lib/schemas/login.ts | 6 -- src/lib/schemas/signup.ts | 7 --- src/lib/validation/user.ts | 12 ++++ src/routes/__root.tsx | 17 +----- src/routes/_authed.tsx | 9 +-- src/routes/index.tsx | 6 +- src/routes/login.tsx | 39 ++++++------- src/routes/logout.tsx | 25 +-------- src/routes/signup.tsx | 40 ++++++------- 17 files changed, 247 insertions(+), 224 deletions(-) create mode 100644 src/lib/db/user.ts create mode 100644 src/lib/hooks/user/useLogin.tsx create mode 100644 src/lib/hooks/user/useSignup.tsx delete mode 100644 src/lib/mutations/mutationLogin.tsx delete mode 100644 src/lib/mutations/mutationSignup.tsx delete mode 100644 src/lib/schemas/login.ts delete mode 100644 src/lib/schemas/signup.ts create mode 100644 src/lib/validation/user.ts diff --git a/biome.json b/biome.json index 44cdcad..826fa01 100644 --- a/biome.json +++ b/biome.json @@ -6,13 +6,14 @@ "useIgnoreFile": false }, "files": { - "ignoreUnknown": false, + "ignoreUnknown": true, "includes": [ "**/src/**/*", "**/.vscode/**/*", "**/index.html", "**/vite.config.js", - "!**/src/routeTree.gen.ts" + "!**/src/routeTree.gen.ts", + "!**/node_modules/**/*" ] }, "formatter": { diff --git a/src/integrations/heroui/provider.tsx b/src/integrations/heroui/provider.tsx index 3af714c..46c3600 100644 --- a/src/integrations/heroui/provider.tsx +++ b/src/integrations/heroui/provider.tsx @@ -1,5 +1,5 @@ import { HeroUIProvider as HeroProvider } from "@heroui/react" export const HeroUIProvider = ({ children }: { children: React.ReactNode }) => { - return {children} + return {children} } diff --git a/src/lib/db/user.ts b/src/lib/db/user.ts new file mode 100644 index 0000000..be5fb95 --- /dev/null +++ b/src/lib/db/user.ts @@ -0,0 +1,84 @@ +import { redirect } from "@tanstack/react-router" +import { createServerFn } from "@tanstack/react-start" +import { getSupabaseServerClient } from "@/integrations/supabase/supabase" +import { loginFormSchema, signupFormSchema } from "../validation/user" + +export const getUser = createServerFn().handler(async () => { + const supabase = getSupabaseServerClient() + const { data, error } = await supabase.auth.getUser() + if (error || !data.user) { + return { + error: true, + message: error?.message ?? "Unknown error" + } + } + + return { + user: { + id: data.user.id, + email: data.user.email, + name: data.user.user_metadata.name || "" + }, + error: false + } +}) + +export const loginUser = createServerFn({ + method: "POST" +}) + .validator(loginFormSchema) + .handler(async ({ data }) => { + const supabase = getSupabaseServerClient() + const login = await supabase.auth.signInWithPassword({ + email: data.email, + password: data.password + }) + if (login.error) { + return { + error: true, + message: login.error.message + } + } + + return { + error: false + } + }) + +export const logoutUser = createServerFn().handler(async () => { + const supabase = getSupabaseServerClient() + + const { error } = await supabase.auth.signOut() + if (error) { + return { + error: true, + message: error.message + } + } + + throw redirect({ + to: "/", + viewTransition: true, + replace: true + }) +}) + +export const signupUser = createServerFn({ method: "POST" }) + .validator(signupFormSchema) + .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 || "/" + }) + }) diff --git a/src/lib/hooks/useValidation.tsx b/src/lib/hooks/useValidation.tsx index b89253c..f21010d 100644 --- a/src/lib/hooks/useValidation.tsx +++ b/src/lib/hooks/useValidation.tsx @@ -15,7 +15,7 @@ export const useValidation = ({ schema }: { formData: FormDataValidation - schema?: z.ZodSchema + schema?: z.ZodType }) => { const result = schema?.safeParse(formData) ?? defaultSchema?.safeParse(formData) @@ -25,6 +25,7 @@ export const useValidation = ({ } if (!result.success) { + //FIXME: Flatten errors, new in zod v4 setErrors(result.error.flatten().fieldErrors as T) return false } diff --git a/src/lib/hooks/user/useLogin.tsx b/src/lib/hooks/user/useLogin.tsx new file mode 100644 index 0000000..d2d6412 --- /dev/null +++ b/src/lib/hooks/user/useLogin.tsx @@ -0,0 +1,54 @@ +import { useMutation } from "@tanstack/react-query" +import { useNavigate } from "@tanstack/react-router" +import { toast } from "sonner" +import type z from "zod" +import { loginUser } from "@/lib/db/user" +import { loginFormSchema } from "@/lib/validation/user" +import { useValidation } from "../useValidation" + +type TLoginForm = z.infer + +export const useLogin = () => { + const navigate = useNavigate() + const { errors, validate } = useValidation({ + defaultSchema: loginFormSchema + }) + const loginMutation = useMutation({ + mutationKey: ["login"], + mutationFn: async (data: TLoginForm) => + loginUser({ + data + }), + onMutate: () => { + toast.loading("Logging in...", { id: "login" }) + }, + onSuccess: () => { + toast.success("Login successful! Redirecting to posts..", { id: "login" }) + navigate({ + to: "/post" + }) + }, + onError: () => { + toast.error("Failed to log in.", { id: "login" }) + } + }) + + const validateLogin = (formData: TLoginForm) => { + const isValid = validate({ + formData + }) + + if (!isValid) { + toast.error("Error en el formulario.") + return false + } + + loginMutation.mutate(formData) + } + + return { + login: validateLogin, + isPending: loginMutation.isPending, + errors: errors + } +} diff --git a/src/lib/hooks/user/useSignup.tsx b/src/lib/hooks/user/useSignup.tsx new file mode 100644 index 0000000..9b58ae4 --- /dev/null +++ b/src/lib/hooks/user/useSignup.tsx @@ -0,0 +1,44 @@ +import { useMutation } from "@tanstack/react-query" +import { useNavigate } from "@tanstack/react-router" +import { toast } from "sonner" +import type z from "zod" +import { signupUser } from "@/lib/db/user" +import { signupFormSchema } from "@/lib/validation/user" +import { useValidation } from "../useValidation" + +type TSignupForm = z.infer + +export const useSignup = () => { + const navigate = useNavigate() + const { validate, errors } = useValidation({ + defaultSchema: signupFormSchema + }) + const signup = useMutation({ + mutationKey: ["signup"], + mutationFn: async (data: TSignupForm) => signupUser({ data }), + onSuccess: () => { + toast.success("Signup successful! Redirecting to login...", { + id: "signup" + }) + navigate({ + to: "/login" + }) + } + }) + + const validateSignup = (formData: TSignupForm) => { + const isValid = validate({ formData }) + if (!isValid) { + toast.error("Signup failed. Please check your input.", { + id: "signup" + }) + } + signup.mutate(formData) + } + + return { + signup: validateSignup, + errors, + isPending: signup.isPending + } +} diff --git a/src/lib/mutations/mutationLogin.tsx b/src/lib/mutations/mutationLogin.tsx deleted file mode 100644 index fe9fc1b..0000000 --- a/src/lib/mutations/mutationLogin.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useMutation } from "@tanstack/react-query" -import { getRouteApi } from "@tanstack/react-router" -import { createServerFn, useServerFn } from "@tanstack/react-start" -import { toast } from "sonner" -import { getSupabaseServerClient } from "@/integrations/supabase/supabase" -import { loginSchema } from "../schemas/login" - -const apiRouter = getRouteApi("/login") - -export const loginFn = createServerFn({ method: "POST" }) - .validator(loginSchema) - .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 mutationLogin = () => { - const navigate = apiRouter.useNavigate() - const loginFunction = useServerFn(loginFn) - const mutation = useMutation({ - mutationKey: ["login"], - mutationFn: async (data: { email: string; password: string }) => { - toast.loading("Logging in...", { id: "login" }) - return loginFunction({ - data: { - email: data.email, - password: data.password - } - }) - }, - onSuccess: (data) => { - if (data?.error) { - toast.error(data.message, { id: "login" }) - return - } - toast.success("Login successful! Redirecting to posts...", { - id: "login" - }) - navigate({ - to: "/post" - }) - }, - onError: (error) => { - toast.error("Login failed. Please try again.", { id: "login" }) - console.log("No se ha podido procesar el login", error) - } - }) - - return mutation -} diff --git a/src/lib/mutations/mutationSignup.tsx b/src/lib/mutations/mutationSignup.tsx deleted file mode 100644 index a4f8596..0000000 --- a/src/lib/mutations/mutationSignup.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useMutation } from "@tanstack/react-query" -import { getRouteApi, redirect } from "@tanstack/react-router" -import { createServerFn, useServerFn } from "@tanstack/react-start" -import { toast } from "sonner" -import { getSupabaseServerClient } from "@/integrations/supabase/supabase" -import { signupSchema } from "../schemas/signup" - -const apiRouter = getRouteApi("/signup") -export const signupFn = createServerFn({ method: "POST" }) - .validator(signupSchema) - .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 mutationSignup = () => { - const navigate = apiRouter.useNavigate() - const signup = useServerFn(signupFn) - const mutation = useMutation({ - mutationKey: ["signup"], - mutationFn: async (data: { - email: string - password: string - redirectUrl?: string - }) => { - toast.loading("Signing up...", { id: "signup" }) - return signup({ - data: { - email: data.email, - password: data.password, - redirectUrl: data.redirectUrl - } - }) - }, - onSuccess: () => { - toast.success("Signup successful! Redirecting to login...", { - id: "signup" - }) - navigate({ - to: "/login" - }) - } - }) - - return mutation -} diff --git a/src/lib/schemas/login.ts b/src/lib/schemas/login.ts deleted file mode 100644 index e9ba067..0000000 --- a/src/lib/schemas/login.ts +++ /dev/null @@ -1,6 +0,0 @@ -import z from "zod" - -export const loginSchema = z.object({ - email: z.email("Invalid email address"), - password: z.string().min(6, "Password must be at least 6 characters long") -}) diff --git a/src/lib/schemas/signup.ts b/src/lib/schemas/signup.ts deleted file mode 100644 index 93b081b..0000000 --- a/src/lib/schemas/signup.ts +++ /dev/null @@ -1,7 +0,0 @@ -import z from "zod" - -export const signupSchema = z.object({ - email: z.email("Invalid email address"), - password: z.string().min(6, "Password must be at least 6 characters long"), - redirectUrl: z.string().optional() -}) diff --git a/src/lib/validation/user.ts b/src/lib/validation/user.ts new file mode 100644 index 0000000..a9c8259 --- /dev/null +++ b/src/lib/validation/user.ts @@ -0,0 +1,12 @@ +import z from "zod" + +export const loginFormSchema = z.object({ + email: z.string("Invalid email address"), + password: z.string().min(1, "Password must be at least 1 character long") +}) + +export const signupFormSchema = z.object({ + email: z.string("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters long"), + redirectUrl: z.string().optional() +}) diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 18e3354..e424124 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -6,31 +6,18 @@ import { Scripts } from "@tanstack/react-router" import { TanStackRouterDevtools } from "@tanstack/react-router-devtools" -import { createServerFn } from "@tanstack/react-start" import { HeroUIProvider } from "@/integrations/heroui/provider" import { SonnerProvider } from "@/integrations/sonner/provider" -import { getSupabaseServerClient } from "@/integrations/supabase/supabase" +import { getUser } from "@/lib/db/user" interface MyRouterContext { 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()({ beforeLoad: async () => { - const user = await fetchUser() + const user = await getUser() return { user } diff --git a/src/routes/_authed.tsx b/src/routes/_authed.tsx index 052bb7e..4580cfd 100644 --- a/src/routes/_authed.tsx +++ b/src/routes/_authed.tsx @@ -1,6 +1,5 @@ import { createFileRoute } from "@tanstack/react-router" - export const Route = createFileRoute("/_authed")({ beforeLoad: ({ context }) => { console.log("contextw", context) @@ -10,9 +9,11 @@ export const Route = createFileRoute("/_authed")({ }, errorComponent: ({ error }) => { if (error.message === "Not authenticated") { - ;

- Not authenticated. Please login. -

+ return ( +

+ Not authenticated. Please login. +

+ ) } throw error diff --git a/src/routes/index.tsx b/src/routes/index.tsx index fc2ac80..94c5e07 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,13 +1,13 @@ import logo from "@assets/logo.svg" import { Button } from "@heroui/react" -import { createFileRoute, getRouteApi } from "@tanstack/react-router" +import { createFileRoute } from "@tanstack/react-router" export const Route = createFileRoute("/")({ component: App }) -const apiRouter = getRouteApi("/") + function App() { - const navigate = apiRouter.useNavigate() + const navigate = Route.useNavigate() return (
diff --git a/src/routes/login.tsx b/src/routes/login.tsx index 33aa73a..a506496 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -1,42 +1,37 @@ import { Button, Form, Input } from "@heroui/react" import { createFileRoute } from "@tanstack/react-router" -import { useValidation } from "@/lib/hooks/useValidation" -import { mutationLogin } from "@/lib/mutations/mutationLogin" -import { loginSchema } from "@/lib/schemas/login" +import type { FormEvent } from "react" +import { useLogin } from "@/lib/hooks/user/useLogin" export const Route = createFileRoute("/login")({ component: LoginComp }) - - function LoginComp() { - const loginMutation = mutationLogin() - const { errors, validate } = useValidation({ - defaultSchema: loginSchema - }) + const { errors, isPending, login } = useLogin() + + const handleSubmit = (e: FormEvent) => { + e.preventDefault() + + const formData = new FormData(e.currentTarget) + + login({ + email: formData.get("email") as string, + password: formData.get("password") as string + }) + } + return (

Login

{ - e.preventDefault() - const formData = new FormData(e.currentTarget) - const email = formData.get("email") as string - const password = formData.get("password") as string - if ( - validate({ - formData: { email, password } - }) - ) - loginMutation.mutate({ email, password }) - }} + onSubmit={handleSubmit} > - diff --git a/src/routes/logout.tsx b/src/routes/logout.tsx index b5bf84e..add9371 100644 --- a/src/routes/logout.tsx +++ b/src/routes/logout.tsx @@ -1,26 +1,7 @@ -import { createFileRoute, redirect } from "@tanstack/react-router" -import { createServerFn } from "@tanstack/react-start" -import { toast } from "sonner" -import { getSupabaseServerClient } from "@/integrations/supabase/supabase" - -const logoutFn = createServerFn().handler(async () => { - const supabase = getSupabaseServerClient() - const { error } = await supabase.auth.signOut() - - if (error) { - toast.error("Logout failed. Please try again.") - return { - error: true, - message: error.message - } - } - - throw redirect({ - href: "/" - }) -}) +import { createFileRoute } from "@tanstack/react-router" +import { logoutUser } from "@/lib/db/user" export const Route = createFileRoute("/logout")({ preload: false, - loader: () => logoutFn() + loader: () => logoutUser() }) diff --git a/src/routes/signup.tsx b/src/routes/signup.tsx index 7fa08b9..c9c4223 100644 --- a/src/routes/signup.tsx +++ b/src/routes/signup.tsx @@ -1,43 +1,37 @@ import { Button, Form, Input } from "@heroui/react" import { createFileRoute } from "@tanstack/react-router" -import { useValidation } from "@/lib/hooks/useValidation" -import { mutationSignup } from "@/lib/mutations/mutationSignup" -import { signupSchema } from "@/lib/schemas/signup" +import type { FormEvent } from "react" +import { useSignup } from "@/lib/hooks/user/useSignup" export const Route = createFileRoute("/signup")({ component: SignupComp }) function SignupComp() { - const signupMutation = mutationSignup() - const { errors, validate } = useValidation({ - defaultSchema: signupSchema - }) + const { signup, errors, isPending } = useSignup() + + const handleSubmit = (e: FormEvent) => { + e.preventDefault() + + const formData = new FormData(e.currentTarget) + + signup({ + email: formData.get("email") as string, + password: formData.get("password") as string + }) + } + return (

Signup

{ - e.preventDefault() - const formData = new FormData(e.currentTarget) - const email = formData.get("email") as string - const password = formData.get("password") as string - if ( - validate({ - formData: { email, password } - }) - ) - signupMutation.mutate({ - email, - password - }) - }} + onSubmit={handleSubmit} > -
From d4d384ba2bc0eb1900c61a0cc25057e5698d8753 Mon Sep 17 00:00:00 2001 From: juan Date: Mon, 11 Aug 2025 14:06:13 +0200 Subject: [PATCH 7/7] fix: update validation logic in useValidation hook and improve error handling in useLogin hook --- src/lib/hooks/useValidation.tsx | 5 +++-- src/lib/hooks/user/useLogin.tsx | 15 ++++++++++----- src/lib/validation/user.ts | 4 ++-- src/routes/__root.tsx | 2 +- src/routes/_authed.tsx | 7 +++---- src/routes/login.tsx | 1 - 6 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/lib/hooks/useValidation.tsx b/src/lib/hooks/useValidation.tsx index f21010d..108f996 100644 --- a/src/lib/hooks/useValidation.tsx +++ b/src/lib/hooks/useValidation.tsx @@ -1,5 +1,5 @@ import { useState } from "react" -import type { z } from "zod" +import { z } from "zod" type FormDataValidation = Record @@ -26,7 +26,8 @@ export const useValidation = ({ if (!result.success) { //FIXME: Flatten errors, new in zod v4 - setErrors(result.error.flatten().fieldErrors as T) + // setErrors(result.error.flatten().fieldErrors as T) + setErrors(z.flattenError(result.error).fieldErrors as T) return false } diff --git a/src/lib/hooks/user/useLogin.tsx b/src/lib/hooks/user/useLogin.tsx index d2d6412..fe6e47d 100644 --- a/src/lib/hooks/user/useLogin.tsx +++ b/src/lib/hooks/user/useLogin.tsx @@ -15,10 +15,15 @@ export const useLogin = () => { }) const loginMutation = useMutation({ mutationKey: ["login"], - mutationFn: async (data: TLoginForm) => - loginUser({ + mutationFn: async (data: TLoginForm) => { + const response = await loginUser({ data - }), + }) + + if (response.error) { + throw new Error(response.message) + } + }, onMutate: () => { toast.loading("Logging in...", { id: "login" }) }, @@ -28,8 +33,8 @@ export const useLogin = () => { to: "/post" }) }, - onError: () => { - toast.error("Failed to log in.", { id: "login" }) + onError: (error) => { + toast.error(error.message, { id: "login" }) } }) diff --git a/src/lib/validation/user.ts b/src/lib/validation/user.ts index a9c8259..bf73223 100644 --- a/src/lib/validation/user.ts +++ b/src/lib/validation/user.ts @@ -1,12 +1,12 @@ import z from "zod" export const loginFormSchema = z.object({ - email: z.string("Invalid email address"), + email: z.email("Invalid email address"), password: z.string().min(1, "Password must be at least 1 character long") }) export const signupFormSchema = z.object({ - email: z.string("Invalid email address"), + email: z.email("Invalid email address"), password: z.string().min(6, "Password must be at least 6 characters long"), redirectUrl: z.string().optional() }) diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index e424124..900fe74 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -19,7 +19,7 @@ export const Route = createRootRouteWithContext()({ beforeLoad: async () => { const user = await getUser() return { - user + ...user } }, head: () => ({ diff --git a/src/routes/_authed.tsx b/src/routes/_authed.tsx index 4580cfd..ec1f096 100644 --- a/src/routes/_authed.tsx +++ b/src/routes/_authed.tsx @@ -1,10 +1,10 @@ -import { createFileRoute } from "@tanstack/react-router" +import { createFileRoute, redirect } from "@tanstack/react-router" export const Route = createFileRoute("/_authed")({ beforeLoad: ({ context }) => { - console.log("contextw", context) - if (!context?.user) { + if (context.error) { throw new Error("Not authenticated") + // TODO: Redirect to login page } }, errorComponent: ({ error }) => { @@ -15,7 +15,6 @@ export const Route = createFileRoute("/_authed")({

) } - throw error } }) diff --git a/src/routes/login.tsx b/src/routes/login.tsx index a506496..e24c272 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -12,7 +12,6 @@ function LoginComp() { const handleSubmit = (e: FormEvent) => { e.preventDefault() - const formData = new FormData(e.currentTarget) login({