diff --git a/internal/server/auth.go b/internal/server/auth.go index 1e6ca52..c47e8ae 100644 --- a/internal/server/auth.go +++ b/internal/server/auth.go @@ -77,7 +77,7 @@ func (h *Handler) RegisterEndpoint(w http.ResponseWriter, r *http.Request) { type loginRequest struct { Email string `json:"email" validate:"required,email,max=254"` - Password string `json:"password" validate:"min=8"` + Password string `json:"password" validate:"required"` } var dummyHash, _ = hashPassword("dummy-password-for-timing") diff --git a/web/src/main.tsx b/web/src/main.tsx index 59224e6..229be48 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,11 +1,28 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { + MutationCache, + QueryCache, + QueryClient, + QueryClientProvider, +} from "@tanstack/react-query"; import { RouterProvider, createRouter } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen"; +import { meQuery } from "@/api/auth/me"; import "./index.css"; -const queryClient = new QueryClient(); +function handleUnauthorized(error: unknown) { + if ((error as { status?: number } | null)?.status !== 401) return; + if (!queryClient.getQueryData(meQuery.queryKey)) return; + + queryClient.setQueryData(meQuery.queryKey, null); + router.invalidate(); +} + +const queryClient = new QueryClient({ + queryCache: new QueryCache({ onError: handleUnauthorized }), + mutationCache: new MutationCache({ onError: handleUnauthorized }), +}); const router = createRouter({ routeTree, context: { queryClient } }); declare module "@tanstack/react-router" { diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 8db2531..23f6be5 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -10,10 +10,11 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as IndexRouteImport } from './routes/index' +import { Route as GuestRouteImport } from './routes/_guest' import { Route as ClientRouteRouteImport } from './routes/client/route' -import { Route as authLoginRouteImport } from './routes/(auth)/login' import { Route as authLogoutRouteImport } from './routes/(auth)/logout' -import { Route as authRegisterRouteImport } from './routes/(auth)/register' +import { Route as GuestLoginRouteImport } from './routes/_guest/login' +import { Route as GuestRegisterRouteImport } from './routes/_guest/register' import { Route as ClientIndexRouteImport } from './routes/client/index' const IndexRoute = IndexRouteImport.update({ @@ -21,25 +22,29 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) +const GuestRoute = GuestRouteImport.update({ + id: '/_guest', + getParentRoute: () => rootRouteImport, +} as any) const ClientRouteRoute = ClientRouteRouteImport.update({ id: '/client', path: '/client', getParentRoute: () => rootRouteImport, } as any) -const authLoginRoute = authLoginRouteImport.update({ - id: '/(auth)/login', - path: '/login', - getParentRoute: () => rootRouteImport, -} as any) const authLogoutRoute = authLogoutRouteImport.update({ id: '/(auth)/logout', path: '/logout', getParentRoute: () => rootRouteImport, } as any) -const authRegisterRoute = authRegisterRouteImport.update({ - id: '/(auth)/register', +const GuestLoginRoute = GuestLoginRouteImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => GuestRoute, +} as any) +const GuestRegisterRoute = GuestRegisterRouteImport.update({ + id: '/register', path: '/register', - getParentRoute: () => rootRouteImport, + getParentRoute: () => GuestRoute, } as any) const ClientIndexRoute = ClientIndexRouteImport.update({ id: '/', @@ -50,48 +55,49 @@ const ClientIndexRoute = ClientIndexRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/client': typeof ClientRouteRouteWithChildren - '/login': typeof authLoginRoute '/logout': typeof authLogoutRoute - '/register': typeof authRegisterRoute + '/login': typeof GuestLoginRoute + '/register': typeof GuestRegisterRoute '/client/': typeof ClientIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute - '/login': typeof authLoginRoute '/logout': typeof authLogoutRoute - '/register': typeof authRegisterRoute + '/login': typeof GuestLoginRoute + '/register': typeof GuestRegisterRoute '/client': typeof ClientIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/client': typeof ClientRouteRouteWithChildren - '/(auth)/login': typeof authLoginRoute + '/_guest': typeof GuestRouteWithChildren '/(auth)/logout': typeof authLogoutRoute - '/(auth)/register': typeof authRegisterRoute + '/_guest/login': typeof GuestLoginRoute + '/_guest/register': typeof GuestRegisterRoute '/client/': typeof ClientIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/client' | '/login' | '/logout' | '/register' | '/client/' + fullPaths: '/' | '/client' | '/logout' | '/login' | '/register' | '/client/' fileRoutesByTo: FileRoutesByTo - to: '/' | '/login' | '/logout' | '/register' | '/client' + to: '/' | '/logout' | '/login' | '/register' | '/client' id: | '__root__' | '/' | '/client' - | '/(auth)/login' + | '/_guest' | '/(auth)/logout' - | '/(auth)/register' + | '/_guest/login' + | '/_guest/register' | '/client/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute ClientRouteRoute: typeof ClientRouteRouteWithChildren - authLoginRoute: typeof authLoginRoute + GuestRoute: typeof GuestRouteWithChildren authLogoutRoute: typeof authLogoutRoute - authRegisterRoute: typeof authRegisterRoute } declare module '@tanstack/react-router' { @@ -103,6 +109,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/_guest': { + id: '/_guest' + path: '' + fullPath: '/' + preLoaderRoute: typeof GuestRouteImport + parentRoute: typeof rootRouteImport + } '/client': { id: '/client' path: '/client' @@ -110,13 +123,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ClientRouteRouteImport parentRoute: typeof rootRouteImport } - '/(auth)/login': { - id: '/(auth)/login' - path: '/login' - fullPath: '/login' - preLoaderRoute: typeof authLoginRouteImport - parentRoute: typeof rootRouteImport - } '/(auth)/logout': { id: '/(auth)/logout' path: '/logout' @@ -124,12 +130,19 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof authLogoutRouteImport parentRoute: typeof rootRouteImport } - '/(auth)/register': { - id: '/(auth)/register' + '/_guest/login': { + id: '/_guest/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof GuestLoginRouteImport + parentRoute: typeof GuestRoute + } + '/_guest/register': { + id: '/_guest/register' path: '/register' fullPath: '/register' - preLoaderRoute: typeof authRegisterRouteImport - parentRoute: typeof rootRouteImport + preLoaderRoute: typeof GuestRegisterRouteImport + parentRoute: typeof GuestRoute } '/client/': { id: '/client/' @@ -153,12 +166,23 @@ const ClientRouteRouteWithChildren = ClientRouteRoute._addFileChildren( ClientRouteRouteChildren, ) +interface GuestRouteChildren { + GuestLoginRoute: typeof GuestLoginRoute + GuestRegisterRoute: typeof GuestRegisterRoute +} + +const GuestRouteChildren: GuestRouteChildren = { + GuestLoginRoute: GuestLoginRoute, + GuestRegisterRoute: GuestRegisterRoute, +} + +const GuestRouteWithChildren = GuestRoute._addFileChildren(GuestRouteChildren) + const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, ClientRouteRoute: ClientRouteRouteWithChildren, - authLoginRoute: authLoginRoute, + GuestRoute: GuestRouteWithChildren, authLogoutRoute: authLogoutRoute, - authRegisterRoute: authRegisterRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/web/src/routes/_guest.tsx b/web/src/routes/_guest.tsx new file mode 100644 index 0000000..d6ebe43 --- /dev/null +++ b/web/src/routes/_guest.tsx @@ -0,0 +1,13 @@ +import { Outlet, createFileRoute, redirect } from "@tanstack/react-router"; + +import { meQuery } from "@/api/auth/me"; + +export const Route = createFileRoute("/_guest")({ + beforeLoad: async ({ context }) => { + const user = await context.queryClient.query(meQuery); + if (user) { + throw redirect({ to: "/client" }); + } + }, + component: () => , +}); diff --git a/web/src/routes/(auth)/login.tsx b/web/src/routes/_guest/login.tsx similarity index 94% rename from web/src/routes/(auth)/login.tsx rename to web/src/routes/_guest/login.tsx index 7a68504..bb24cbc 100644 --- a/web/src/routes/(auth)/login.tsx +++ b/web/src/routes/_guest/login.tsx @@ -11,7 +11,7 @@ import { AuthLayout } from "@/components/auth/auth-layout"; import { StatefulButton, type ButtonState } from "@/components/motion/button/stateful"; import { Input } from "@/components/motion/input"; -export const Route = createFileRoute("/(auth)/login")({ +export const Route = createFileRoute("/_guest/login")({ validateSearch: (search: Record): { redirect?: string } => ({ redirect: typeof search.redirect === "string" ? search.redirect : undefined, }), @@ -21,7 +21,7 @@ export const Route = createFileRoute("/(auth)/login")({ const loginSchema = z .object({ email: z.email("Enter a valid email").max(254), - password: z.string().min(8, "Password must be at least 8 characters"), + password: z.string().min(1, "Password is required"), }); function Login() { @@ -58,7 +58,7 @@ function Login() { const { fieldErrors: serverFieldErrors, formError } = parseServerError( login.error, { - 409: { email: "An account with this email already exists" }, + 401: "Invalid email or password", }, ); diff --git a/web/src/routes/(auth)/register.tsx b/web/src/routes/_guest/register.tsx similarity index 99% rename from web/src/routes/(auth)/register.tsx rename to web/src/routes/_guest/register.tsx index 6ce60a3..af9821d 100644 --- a/web/src/routes/(auth)/register.tsx +++ b/web/src/routes/_guest/register.tsx @@ -14,7 +14,7 @@ import { import { Checkbox } from "@/components/motion/checkbox"; import { Input } from "@/components/motion/input"; -export const Route = createFileRoute("/(auth)/register")({ +export const Route = createFileRoute("/_guest/register")({ component: Register, });