From 1d1ece2b3a596e2c57e62425952efbed22fab211 Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Mon, 22 Jul 2024 11:24:18 +0200 Subject: [PATCH 01/16] feat: add user settings functionality --- .../app/api/mutations/useChangePassword.ts | 37 +++++++++ .../web/app/api/mutations/useUpdateUser.ts | 38 +++++++++ .../modules/Dashboard/Dashboard.layout.tsx | 5 +- .../Dashboard/Settings/Settings.layout.tsx | 34 ++++++++ .../Dashboard/Settings/Settings.page.tsx | 11 +++ .../Settings/forms/ChangePasswordForm.tsx | 77 +++++++++++++++++++ .../Dashboard/Settings/forms/UserForm.tsx | 64 +++++++++++++++ .../common_nestjs_remix/apps/web/routes.ts | 9 +++ 8 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 examples/common_nestjs_remix/apps/web/app/api/mutations/useChangePassword.ts create mode 100644 examples/common_nestjs_remix/apps/web/app/api/mutations/useUpdateUser.ts create mode 100644 examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/Settings.layout.tsx create mode 100644 examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/Settings.page.tsx create mode 100644 examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/forms/ChangePasswordForm.tsx create mode 100644 examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/forms/UserForm.tsx diff --git a/examples/common_nestjs_remix/apps/web/app/api/mutations/useChangePassword.ts b/examples/common_nestjs_remix/apps/web/app/api/mutations/useChangePassword.ts new file mode 100644 index 0000000..260800b --- /dev/null +++ b/examples/common_nestjs_remix/apps/web/app/api/mutations/useChangePassword.ts @@ -0,0 +1,37 @@ +import { useMutation } from "@tanstack/react-query"; +import { AxiosError } from "axios"; +import { toast } from "sonner"; +import { useAuthStore } from "~/modules/Auth/authStore"; +import { ApiClient } from "../api-client"; +import { ChangePasswordBody } from "../generated-api"; + +type ChangePasswordOptions = { + data: ChangePasswordBody; +}; + +export function useChangePassword() { + const { currentUser } = useAuthStore(); + if (!currentUser) { + throw new Error("User is not logged in"); + } + + return useMutation({ + mutationFn: async (options: ChangePasswordOptions) => { + const response = await ApiClient.users.usersControllerChangePassword( + currentUser.id, + options.data + ); + + return response.data; + }, + onSuccess: () => { + toast.success("Password updated successfully"); + }, + onError: (error) => { + if (error instanceof AxiosError) { + return toast.error(error.response?.data.message); + } + toast.error(error.message); + }, + }); +} diff --git a/examples/common_nestjs_remix/apps/web/app/api/mutations/useUpdateUser.ts b/examples/common_nestjs_remix/apps/web/app/api/mutations/useUpdateUser.ts new file mode 100644 index 0000000..745c1fc --- /dev/null +++ b/examples/common_nestjs_remix/apps/web/app/api/mutations/useUpdateUser.ts @@ -0,0 +1,38 @@ +import { useMutation } from "@tanstack/react-query"; +import { AxiosError } from "axios"; +import { toast } from "sonner"; +import { useAuthStore } from "~/modules/Auth/authStore"; +import { ApiClient } from "../api-client"; +import { UpdateUserBody } from "../generated-api"; + +type UpdateUserOptions = { + data: UpdateUserBody; +}; + +export function useUpdateUser() { + const { setCurrentUser, currentUser } = useAuthStore(); + if (!currentUser) { + throw new Error("User is not logged in"); + } + + return useMutation({ + mutationFn: async (options: UpdateUserOptions) => { + const response = await ApiClient.users.usersControllerUpdateUser( + currentUser.id, + options.data + ); + + return response.data; + }, + onSuccess: (data) => { + setCurrentUser(data.data); + toast.success("User updated successfully"); + }, + onError: (error) => { + if (error instanceof AxiosError) { + return toast.error(error.response?.data.message); + } + toast.error(error.message); + }, + }); +} diff --git a/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Dashboard.layout.tsx b/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Dashboard.layout.tsx index ab198c5..928d860 100644 --- a/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Dashboard.layout.tsx +++ b/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Dashboard.layout.tsx @@ -43,7 +43,10 @@ export default function DashboardLayout() { My Account - Settings + Dashboard + + + Settings Support diff --git a/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/Settings.layout.tsx b/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/Settings.layout.tsx new file mode 100644 index 0000000..0858f64 --- /dev/null +++ b/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/Settings.layout.tsx @@ -0,0 +1,34 @@ +import { Link, Outlet, useLocation } from "@remix-run/react"; +import { replace } from "lodash-es"; +import { cn } from "~/lib/utils"; + +export default function SettingsLayout() { + const { hash } = useLocation(); + const currentTab = replace(hash, "#", "") || "user-info"; + + return ( +
+ + +
+ ); +} diff --git a/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/Settings.page.tsx b/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/Settings.page.tsx new file mode 100644 index 0000000..791fb6f --- /dev/null +++ b/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/Settings.page.tsx @@ -0,0 +1,11 @@ +import ChangePasswordForm from "./forms/ChangePasswordForm"; +import UserForm from "./forms/UserForm"; + +export default function SettingsPage() { + return ( +
+ + +
+ ); +} diff --git a/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/forms/ChangePasswordForm.tsx b/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/forms/ChangePasswordForm.tsx new file mode 100644 index 0000000..a57c50f --- /dev/null +++ b/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/forms/ChangePasswordForm.tsx @@ -0,0 +1,77 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { ChangePasswordBody } from "~/api/generated-api"; +import { Button } from "~/components/ui/button"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, +} from "~/components/ui/card"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { useChangePassword } from "~/api/mutations/useChangePassword"; +import { cn } from "~/lib/utils"; + +const passwordSchema = z.object({ + oldPassword: z.string().min(8), + newPassword: z.string().min(8), +}); + +export default function ChangePasswordForm() { + const { mutate: changePassword } = useChangePassword(); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ resolver: zodResolver(passwordSchema) }); + + const onSubmit = (data: ChangePasswordBody) => { + changePassword({ data }); + }; + + return ( + +
+ + Change password + Update your password here. + + + + + {errors.oldPassword && ( +

+ {errors.oldPassword.message} +

+ )} + + + {errors.newPassword && ( +

+ {errors.newPassword.message} +

+ )} +
+ + + +
+
+ ); +} diff --git a/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/forms/UserForm.tsx b/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/forms/UserForm.tsx new file mode 100644 index 0000000..c230977 --- /dev/null +++ b/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/forms/UserForm.tsx @@ -0,0 +1,64 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { UpdateUserBody } from "~/api/generated-api"; +import { useUpdateUser } from "~/api/mutations/useUpdateUser"; +import { Button } from "~/components/ui/button"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, +} from "~/components/ui/card"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { cn } from "~/lib/utils"; +import { useAuthStore } from "~/modules/Auth/authStore"; + +const updateUserSchema = z.object({ + email: z.string().email(), +}); + +export default function UserForm() { + const { mutate: updateUser } = useUpdateUser(); + const currentUser = useAuthStore.getState().currentUser; + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ resolver: zodResolver(updateUserSchema) }); + + const onSubmit = (data: UpdateUserBody) => { + updateUser({ data }); + }; + + return ( + +
+ + Change user information + Update your user information here. + + + + + {errors.email && ( +

{errors.email.message}

+ )} +
+ + + +
+
+ ); +} diff --git a/examples/common_nestjs_remix/apps/web/routes.ts b/examples/common_nestjs_remix/apps/web/routes.ts index 1fa2b2c..6137f0f 100644 --- a/examples/common_nestjs_remix/apps/web/routes.ts +++ b/examples/common_nestjs_remix/apps/web/routes.ts @@ -21,6 +21,15 @@ export const routes: ( route("", "modules/Dashboard/Dashboard.page.tsx", { index: true, }); + route( + "settings", + "modules/Dashboard/Settings/Settings.layout.tsx", + () => { + route("", "modules/Dashboard/Settings/Settings.page.tsx", { + index: true, + }); + } + ); }); route("auth", "modules/Auth/Auth.layout.tsx", () => { route("login", "modules/Auth/Login.page.tsx"); From ec4c044e5b5add4b3c3b42d3d4d9df107fee2852 Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Mon, 22 Jul 2024 11:24:19 +0200 Subject: [PATCH 02/16] fix: update user state on successful login --- .../apps/web/app/api/mutations/useLoginUser.ts | 7 +++++-- .../apps/web/app/modules/Auth/authStore.ts | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts b/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts index b29f8af..eab6ace 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts @@ -10,14 +10,17 @@ type LoginUserOptions = { }; export function useLoginUser() { - const { setLoggedIn } = useAuthStore(); + const { setLoggedIn, setCurrentUser } = useAuthStore(); return useMutation({ mutationFn: async (options: LoginUserOptions) => { const response = await ApiClient.auth.authControllerLogin(options.data); + if (!response) throw new Error("Invalid username or password"); + return response.data; }, - onSuccess: () => { + onSuccess: (data) => { + setCurrentUser(data.data); setLoggedIn(true); }, onError: (error) => { diff --git a/examples/common_nestjs_remix/apps/web/app/modules/Auth/authStore.ts b/examples/common_nestjs_remix/apps/web/app/modules/Auth/authStore.ts index daab72e..aeb39e1 100644 --- a/examples/common_nestjs_remix/apps/web/app/modules/Auth/authStore.ts +++ b/examples/common_nestjs_remix/apps/web/app/modules/Auth/authStore.ts @@ -1,9 +1,12 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; +import { UpdateUserResponse } from "~/api/generated-api"; type AuthStore = { isLoggedIn: boolean; setLoggedIn: (value: boolean) => void; + currentUser: UpdateUserResponse["data"] | null; + setCurrentUser: (user: UpdateUserResponse["data"]) => void; }; export const useAuthStore = create()( @@ -11,6 +14,8 @@ export const useAuthStore = create()( (set) => ({ isLoggedIn: false, setLoggedIn: (value) => set({ isLoggedIn: value }), + currentUser: null, + setCurrentUser: (user) => set({ currentUser: user }), }), { name: "auth-storage", From 650b50aba32562f7aa7d40cbb8ff53de4b305454 Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Mon, 22 Jul 2024 11:24:20 +0200 Subject: [PATCH 03/16] chore: add lodash-es as a new dependency --- .../apps/web/app/api/generated-api.ts | 7 ++++++- .../common_nestjs_remix/apps/web/package.json | 2 ++ examples/common_nestjs_remix/pnpm-lock.yaml | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/examples/common_nestjs_remix/apps/web/app/api/generated-api.ts b/examples/common_nestjs_remix/apps/web/app/api/generated-api.ts index b542859..bcb7096 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/generated-api.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/generated-api.ts @@ -88,7 +88,12 @@ export interface ChangePasswordBody { * @minLength 8 * @maxLength 64 */ - password: string; + newPassword: string; + /** + * @minLength 8 + * @maxLength 64 + */ + oldPassword: string; } export type ChangePasswordResponse = null; diff --git a/examples/common_nestjs_remix/apps/web/package.json b/examples/common_nestjs_remix/apps/web/package.json index 76f6ba7..fc5ac83 100644 --- a/examples/common_nestjs_remix/apps/web/package.json +++ b/examples/common_nestjs_remix/apps/web/package.json @@ -30,6 +30,7 @@ "compression": "^1.7.4", "express": "^4.18.2", "isbot": "^4.1.0", + "lodash-es": "^4.17.21", "lucide-react": "^0.408.0", "morgan": "^1.10.0", "react": "^18.3.1", @@ -46,6 +47,7 @@ "@remix-run/dev": "^2.9.2", "@types/compression": "^1.7.5", "@types/express": "^4.17.20", + "@types/lodash-es": "^4.17.12", "@types/morgan": "^1.9.9", "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7", diff --git a/examples/common_nestjs_remix/pnpm-lock.yaml b/examples/common_nestjs_remix/pnpm-lock.yaml index bc8ea00..40b34a0 100644 --- a/examples/common_nestjs_remix/pnpm-lock.yaml +++ b/examples/common_nestjs_remix/pnpm-lock.yaml @@ -254,6 +254,9 @@ importers: isbot: specifier: ^4.1.0 version: 4.1.0 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 lucide-react: specifier: ^0.408.0 version: 0.408.0(react@18.3.1) @@ -297,6 +300,9 @@ importers: '@types/express': specifier: ^4.17.20 version: 4.17.20 + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 '@types/morgan': specifier: ^1.9.9 version: 1.9.9 @@ -3566,6 +3572,12 @@ packages: dependencies: '@types/node': 20.14.10 + /@types/lodash-es@4.17.12: + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + dependencies: + '@types/lodash': 4.17.6 + dev: true + /@types/lodash@4.17.6: resolution: {integrity: sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==} dev: true @@ -8579,6 +8591,10 @@ packages: p-locate: 5.0.0 dev: true + /lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: false + /lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} dev: true From b09d11cacbcdfa3f94d752bc2b5aaab9cd5b627b Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Mon, 22 Jul 2024 11:24:20 +0200 Subject: [PATCH 04/16] style: add smooth scrolling to the application --- .../Dashboard/Settings/Settings.layout.tsx | 39 +++++++++++-------- .../common_nestjs_remix/apps/web/app/root.tsx | 2 +- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/Settings.layout.tsx b/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/Settings.layout.tsx index 0858f64..0600efb 100644 --- a/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/Settings.layout.tsx +++ b/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/Settings.layout.tsx @@ -6,27 +6,32 @@ export default function SettingsLayout() { const { hash } = useLocation(); const currentTab = replace(hash, "#", "") || "user-info"; + const navigationItems = [ + { + id: "user-info", + title: "User Info", + }, + { + id: "change-password", + title: "Change Password", + }, + ]; + return (
diff --git a/examples/common_nestjs_remix/apps/web/app/root.tsx b/examples/common_nestjs_remix/apps/web/app/root.tsx index d3b5c10..29c66bd 100644 --- a/examples/common_nestjs_remix/apps/web/app/root.tsx +++ b/examples/common_nestjs_remix/apps/web/app/root.tsx @@ -10,7 +10,7 @@ import { Toaster } from "./components/ui/sonner"; export function Layout({ children }: { children: React.ReactNode }) { return ( - + From bc5aec56dada19e14b60a350e03593436db23683 Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Mon, 22 Jul 2024 14:42:05 +0200 Subject: [PATCH 05/16] feat: add currentUser method and /auth/me endpoint --- .../apps/api/src/auth/api/auth.controller.ts | 14 ++++++ .../apps/api/src/auth/auth.service.ts | 12 ++++- .../apps/api/src/swagger/api-schema.json | 48 +++++++++++++++++++ .../web/app/api/mutations/useLoginUser.ts | 10 ++-- 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/examples/common_nestjs_remix/apps/api/src/auth/api/auth.controller.ts b/examples/common_nestjs_remix/apps/api/src/auth/api/auth.controller.ts index 3216165..e644a5f 100644 --- a/examples/common_nestjs_remix/apps/api/src/auth/api/auth.controller.ts +++ b/examples/common_nestjs_remix/apps/api/src/auth/api/auth.controller.ts @@ -6,6 +6,7 @@ import { Res, UnauthorizedException, UseGuards, + Get, } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; import { Static } from "@sinclair/typebox"; @@ -23,6 +24,7 @@ import { } from "../schemas/create-account.schema"; import { LoginBody, loginSchema } from "../schemas/login.schema"; import { TokenService } from "../token.service"; +import { CurrentUser } from "src/common/decorators/user.decorator"; @Controller("auth") export class AuthController { @@ -97,4 +99,16 @@ export class AuthController { return null; } + + @Get("me") + @Validate({ + response: baseResponse(commonUserSchema), + }) + async me( + @CurrentUser() currentUser: { userId: string }, + ): Promise>> { + const account = await this.authService.currentUser(currentUser.userId); + + return new BaseResponse(account); + } } diff --git a/examples/common_nestjs_remix/apps/api/src/auth/auth.service.ts b/examples/common_nestjs_remix/apps/api/src/auth/auth.service.ts index a8d472f..685d64a 100644 --- a/examples/common_nestjs_remix/apps/api/src/auth/auth.service.ts +++ b/examples/common_nestjs_remix/apps/api/src/auth/auth.service.ts @@ -8,7 +8,7 @@ import { ConfigService } from "@nestjs/config"; import { JwtService } from "@nestjs/jwt"; import * as bcrypt from "bcrypt"; import { eq } from "drizzle-orm"; -import { DatabasePg } from "src/common"; +import { DatabasePg, UUIDType } from "src/common"; import { credentials, users } from "../storage/schema"; import { UsersService } from "../users/users.service"; import hashPassword from "src/common/helpers/hashPassword"; @@ -63,6 +63,16 @@ export class AuthService { }; } + public async currentUser(id: UUIDType) { + const user = await this.usersService.getUserById(id); + + if (!user) { + throw new UnauthorizedException("User not found"); + } + + return user; + } + public async refreshTokens(refreshToken: string) { try { const payload = await this.jwtService.verifyAsync(refreshToken, { diff --git a/examples/common_nestjs_remix/apps/api/src/swagger/api-schema.json b/examples/common_nestjs_remix/apps/api/src/swagger/api-schema.json index fc3d61b..528fe07 100644 --- a/examples/common_nestjs_remix/apps/api/src/swagger/api-schema.json +++ b/examples/common_nestjs_remix/apps/api/src/swagger/api-schema.json @@ -89,6 +89,23 @@ } } }, + "/auth/me": { + "get": { + "operationId": "AuthController_me", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeResponse" + } + } + } + } + } + } + }, "/users": { "get": { "operationId": "UsersController_getUsers", @@ -345,6 +362,37 @@ "RefreshTokensResponse": { "type": "null" }, + "MeResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "email": { + "type": "string" + } + }, + "required": [ + "id", + "createdAt", + "updatedAt", + "email" + ] + } + }, + "required": [ + "data" + ] + }, "GetUsersResponse": { "type": "object", "properties": { diff --git a/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts b/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts index eab6ace..1a7f5cd 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts @@ -1,9 +1,9 @@ import { useMutation } from "@tanstack/react-query"; +import { AxiosError } from "axios"; +import { toast } from "sonner"; +import { useAuthStore } from "~/modules/Auth/authStore"; import { ApiClient } from "../api-client"; import { LoginBody } from "../generated-api"; -import { useAuthStore } from "~/modules/Auth/authStore"; -import { toast } from "sonner"; -import { AxiosError } from "axios"; type LoginUserOptions = { data: LoginBody; @@ -15,7 +15,9 @@ export function useLoginUser() { mutationFn: async (options: LoginUserOptions) => { const response = await ApiClient.auth.authControllerLogin(options.data); - if (!response) throw new Error("Invalid username or password"); + if (!response) { + throw new Error("Invalid username or password"); + } return response.data; }, From 76bcd8f5713e168b44db4f3e2e0139efbddc3913 Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Mon, 22 Jul 2024 14:42:05 +0200 Subject: [PATCH 06/16] style: rearrange imports in useLoginUser.ts --- .../src/auth/__tests__/auth.service.spec.ts | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.service.spec.ts b/examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.service.spec.ts index 3ffcff8..b23c989 100644 --- a/examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.service.spec.ts +++ b/examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.service.spec.ts @@ -1,4 +1,8 @@ -import { ConflictException, UnauthorizedException } from "@nestjs/common"; +import { + ConflictException, + NotFoundException, + UnauthorizedException, +} from "@nestjs/common"; import * as bcrypt from "bcrypt"; import { eq } from "drizzle-orm"; import { AuthService } from "src/auth/auth.service"; @@ -136,4 +140,24 @@ describe("AuthService", () => { expect(result).toBeNull(); }); }); + + describe("currentUser", () => { + it("should return current user data", async () => { + const user = await userFactory.create(); + + const result = await authService.currentUser(user.id); + + expect(result).toBeDefined(); + expect(result.id).toBe(user.id); + expect(result.email).toBe(user.email); + }); + + it("should throw UnauthorizedException for non-existent user", async () => { + const nonExistentUserId = crypto.randomUUID(); + + await expect(authService.currentUser(nonExistentUserId)).rejects.toThrow( + NotFoundException, + ); + }); + }); }); From ddea8500072f857c9296dfaf3f9923afa24f4af9 Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Mon, 22 Jul 2024 14:42:06 +0200 Subject: [PATCH 07/16] feat: add /auth/me endpoint tests --- .../__tests__/auth.controller.e2e-spec.ts | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.controller.e2e-spec.ts b/examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.controller.e2e-spec.ts index f45207c..9429725 100644 --- a/examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.controller.e2e-spec.ts +++ b/examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.controller.e2e-spec.ts @@ -137,6 +137,7 @@ describe("AuthController (e2e)", () => { const user = await userFactory.build(); const password = "password123"; await authService.register(user.email, password); + let refreshToken = ""; const loginResponse = await request(app.getHttpServer()) .post("/auth/login") @@ -148,8 +149,6 @@ describe("AuthController (e2e)", () => { const cookies = loginResponse.headers["set-cookie"]; - let refreshToken = ""; - if (isArray(cookies)) { cookies.forEach((cookie) => { if (cookie.startsWith("refresh_token=")) { @@ -174,4 +173,44 @@ describe("AuthController (e2e)", () => { .expect(401); }); }); + + describe("GET /auth/me", () => { + it("should return current user data for authenticated user", async () => { + let accessToken = ""; + + const user = await userFactory + .withCredentials({ password: "password123" }) + .create(); + + const loginResponse = await request(app.getHttpServer()) + .post("/auth/login") + .send({ + email: user.email, + password: "password123", + }); + + const cookies = loginResponse.headers["set-cookie"]; + + if (Array.isArray(cookies)) { + cookies.forEach((cookieString) => { + const parsedCookie = cookie.parse(cookieString); + if ("access_token" in parsedCookie) { + accessToken = parsedCookie.access_token; + } + }); + } + + const response = await request(app.getHttpServer()) + .get("/auth/me") + .set("Cookie", `access_token=${accessToken};`) + .expect(200); + + expect(response.body.data).toHaveProperty("id"); + expect(response.body.data.email).toBe(user.email); + }); + + it("should return 401 for unauthenticated request", async () => { + await request(app.getHttpServer()).get("/auth/me").expect(401); + }); + }); }); From 586bd2192245fc0769333b3c0ca8bf81324cbd78 Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Mon, 22 Jul 2024 15:13:56 +0200 Subject: [PATCH 08/16] feat: add MeResponse and useSafeCurrentUser --- .../apps/web/app/api/generated-api.ts | 23 +++++++++++++++++++ .../web/app/api/mutations/useLoginUser.ts | 3 ++- .../web/app/api/queries/useCurrentUser.ts | 20 ++++++++++++++++ .../apps/web/app/modules/Auth/authStore.ts | 5 ---- .../Dashboard/Settings/forms/UserForm.tsx | 4 ++-- 5 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 examples/common_nestjs_remix/apps/web/app/api/queries/useCurrentUser.ts diff --git a/examples/common_nestjs_remix/apps/web/app/api/generated-api.ts b/examples/common_nestjs_remix/apps/web/app/api/generated-api.ts index bcb7096..f5995eb 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/generated-api.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/generated-api.ts @@ -51,6 +51,15 @@ export type LogoutResponse = null; export type RefreshTokensResponse = null; +export interface MeResponse { + data: { + id: string; + createdAt: string; + updatedAt: string; + email: string; + }; +} + export interface GetUsersResponse { data: { id: string; @@ -302,6 +311,20 @@ export class API extends HttpClient + this.request({ + path: `/auth/me`, + method: "GET", + format: "json", + ...params, + }), }; users = { /** diff --git a/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts b/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts index 1a7f5cd..4811ad8 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts @@ -10,7 +10,8 @@ type LoginUserOptions = { }; export function useLoginUser() { - const { setLoggedIn, setCurrentUser } = useAuthStore(); + const setLoggedIn = useAuthStore.getState().setLoggedIn; + return useMutation({ mutationFn: async (options: LoginUserOptions) => { const response = await ApiClient.auth.authControllerLogin(options.data); diff --git a/examples/common_nestjs_remix/apps/web/app/api/queries/useCurrentUser.ts b/examples/common_nestjs_remix/apps/web/app/api/queries/useCurrentUser.ts new file mode 100644 index 0000000..08bb091 --- /dev/null +++ b/examples/common_nestjs_remix/apps/web/app/api/queries/useCurrentUser.ts @@ -0,0 +1,20 @@ +import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; +import { ApiClient } from "../api-client"; + +export const currentUserQueryOptions = { + queryKey: ["currentUser"], + queryFn: async () => { + const response = await ApiClient.auth.authControllerMe(); + return response.data; + }, +}; + +export function useCurrentUser() { + return useQuery(currentUserQueryOptions); +} + +export function useSafeCurrentUser() { + const { data, ...rest } = useSuspenseQuery(currentUserQueryOptions); + + return { data: data?.data, ...rest }; +} diff --git a/examples/common_nestjs_remix/apps/web/app/modules/Auth/authStore.ts b/examples/common_nestjs_remix/apps/web/app/modules/Auth/authStore.ts index aeb39e1..daab72e 100644 --- a/examples/common_nestjs_remix/apps/web/app/modules/Auth/authStore.ts +++ b/examples/common_nestjs_remix/apps/web/app/modules/Auth/authStore.ts @@ -1,12 +1,9 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; -import { UpdateUserResponse } from "~/api/generated-api"; type AuthStore = { isLoggedIn: boolean; setLoggedIn: (value: boolean) => void; - currentUser: UpdateUserResponse["data"] | null; - setCurrentUser: (user: UpdateUserResponse["data"]) => void; }; export const useAuthStore = create()( @@ -14,8 +11,6 @@ export const useAuthStore = create()( (set) => ({ isLoggedIn: false, setLoggedIn: (value) => set({ isLoggedIn: value }), - currentUser: null, - setCurrentUser: (user) => set({ currentUser: user }), }), { name: "auth-storage", diff --git a/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/forms/UserForm.tsx b/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/forms/UserForm.tsx index c230977..f80742e 100644 --- a/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/forms/UserForm.tsx +++ b/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/forms/UserForm.tsx @@ -3,6 +3,7 @@ import { useForm } from "react-hook-form"; import { z } from "zod"; import { UpdateUserBody } from "~/api/generated-api"; import { useUpdateUser } from "~/api/mutations/useUpdateUser"; +import { useSafeCurrentUser } from "~/api/queries/useCurrentUser"; import { Button } from "~/components/ui/button"; import { Card, @@ -15,7 +16,6 @@ import { import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { cn } from "~/lib/utils"; -import { useAuthStore } from "~/modules/Auth/authStore"; const updateUserSchema = z.object({ email: z.string().email(), @@ -23,7 +23,7 @@ const updateUserSchema = z.object({ export default function UserForm() { const { mutate: updateUser } = useUpdateUser(); - const currentUser = useAuthStore.getState().currentUser; + const { data: currentUser } = useSafeCurrentUser(); const { register, From b57e8338bb5abd9668d72d5677d899d3502ea1fb Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Mon, 22 Jul 2024 15:13:56 +0200 Subject: [PATCH 09/16] refactor: update useLoginUser and useUpdateUser --- .../apps/web/app/api/mutations/useLoginUser.ts | 3 +-- .../apps/web/app/api/mutations/useUpdateUser.ts | 15 ++++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts b/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts index 4811ad8..e6a1e8e 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts @@ -22,8 +22,7 @@ export function useLoginUser() { return response.data; }, - onSuccess: (data) => { - setCurrentUser(data.data); + onSuccess: () => { setLoggedIn(true); }, onError: (error) => { diff --git a/examples/common_nestjs_remix/apps/web/app/api/mutations/useUpdateUser.ts b/examples/common_nestjs_remix/apps/web/app/api/mutations/useUpdateUser.ts index 745c1fc..6263468 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/mutations/useUpdateUser.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/mutations/useUpdateUser.ts @@ -1,19 +1,20 @@ import { useMutation } from "@tanstack/react-query"; import { AxiosError } from "axios"; import { toast } from "sonner"; -import { useAuthStore } from "~/modules/Auth/authStore"; import { ApiClient } from "../api-client"; import { UpdateUserBody } from "../generated-api"; +import { queryClient } from "../queryClient"; +import { + currentUserQueryOptions, + useSafeCurrentUser, +} from "../queries/useCurrentUser"; type UpdateUserOptions = { data: UpdateUserBody; }; export function useUpdateUser() { - const { setCurrentUser, currentUser } = useAuthStore(); - if (!currentUser) { - throw new Error("User is not logged in"); - } + const { data: currentUser } = useSafeCurrentUser(); return useMutation({ mutationFn: async (options: UpdateUserOptions) => { @@ -24,8 +25,8 @@ export function useUpdateUser() { return response.data; }, - onSuccess: (data) => { - setCurrentUser(data.data); + onSuccess: () => { + queryClient.invalidateQueries(currentUserQueryOptions); toast.success("User updated successfully"); }, onError: (error) => { From 8348d2ee3618f98c20eaf94defe17302ad540126 Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Mon, 22 Jul 2024 15:19:24 +0200 Subject: [PATCH 10/16] feat: update /auth/me expect --- .../apps/api/src/auth/__tests__/auth.controller.e2e-spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.controller.e2e-spec.ts b/examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.controller.e2e-spec.ts index 9429725..d00376f 100644 --- a/examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.controller.e2e-spec.ts +++ b/examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.controller.e2e-spec.ts @@ -1,6 +1,6 @@ import { DatabasePg } from "../../common/index"; import { INestApplication } from "@nestjs/common"; -import { isArray } from "lodash"; +import { isArray, omit } from "lodash"; import request from "supertest"; import { createUserFactory } from "../../../test/factory/user.factory"; import { createE2ETest } from "../../../test/create-e2e-test"; @@ -205,8 +205,7 @@ describe("AuthController (e2e)", () => { .set("Cookie", `access_token=${accessToken};`) .expect(200); - expect(response.body.data).toHaveProperty("id"); - expect(response.body.data.email).toBe(user.email); + expect(response.body.data).toStrictEqual(omit(user, "credentials")); }); it("should return 401 for unauthenticated request", async () => { From d9c8061b845f69beab7cb8d1438805cf66b26da0 Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Mon, 22 Jul 2024 17:10:35 +0200 Subject: [PATCH 11/16] feat: add lodash-es functions and bypass refresh endpoints --- .../apps/web/app/api/api-client.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/examples/common_nestjs_remix/apps/web/app/api/api-client.ts b/examples/common_nestjs_remix/apps/web/app/api/api-client.ts index 849dfca..8671f46 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/api-client.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/api-client.ts @@ -1,5 +1,6 @@ import { useAuthStore } from "~/modules/Auth/authStore"; import { API } from "./generated-api"; +import { includes, some } from "lodash-es"; export const ApiClient = new API({ baseURL: import.meta.env.VITE_API_URL, @@ -7,12 +8,22 @@ export const ApiClient = new API({ withCredentials: true, }); +const bypassRefreshEndpoints = ["/login"]; + ApiClient.instance.interceptors.response.use( (response) => response, async (error) => { const isLoggedIn = useAuthStore.getState().isLoggedIn; const originalRequest = error.config; + if ( + some(bypassRefreshEndpoints, (endpoint) => + includes(originalRequest.url, endpoint) + ) + ) { + return Promise.reject(error); + } + if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; if (!isLoggedIn) return; From 72d024f9c4c42afe449b268806d82b369e8e9880 Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Mon, 22 Jul 2024 17:10:35 +0200 Subject: [PATCH 12/16] feat: update Toaster component props --- .../apps/web/app/api/mutations/useLoginUser.ts | 4 ---- examples/common_nestjs_remix/apps/web/app/root.tsx | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts b/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts index e6a1e8e..76666cf 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts @@ -16,10 +16,6 @@ export function useLoginUser() { mutationFn: async (options: LoginUserOptions) => { const response = await ApiClient.auth.authControllerLogin(options.data); - if (!response) { - throw new Error("Invalid username or password"); - } - return response.data; }, onSuccess: () => { diff --git a/examples/common_nestjs_remix/apps/web/app/root.tsx b/examples/common_nestjs_remix/apps/web/app/root.tsx index 29c66bd..ab249bc 100644 --- a/examples/common_nestjs_remix/apps/web/app/root.tsx +++ b/examples/common_nestjs_remix/apps/web/app/root.tsx @@ -23,6 +23,8 @@ export function Layout({ children }: { children: React.ReactNode }) { Date: Mon, 22 Jul 2024 19:15:47 +0200 Subject: [PATCH 13/16] feat: update useLoginUser and useCurrentUserSuspense --- .../apps/web/app/api/mutations/useLoginUser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts b/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts index 76666cf..2e72f20 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/mutations/useLoginUser.ts @@ -10,7 +10,7 @@ type LoginUserOptions = { }; export function useLoginUser() { - const setLoggedIn = useAuthStore.getState().setLoggedIn; + const setLoggedIn = useAuthStore((state) => state.setLoggedIn); return useMutation({ mutationFn: async (options: LoginUserOptions) => { From 68ecab229753c15a92c4d96f7a2c8eb73e744659 Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Mon, 22 Jul 2024 19:15:47 +0200 Subject: [PATCH 14/16] refactor: replace useSafeCurrentUser with useCurrentUserSuspense --- .../apps/api/src/swagger/api-schema.json | 2 +- .../common_nestjs_remix/apps/web/app/api/api-client.ts | 1 - .../apps/web/app/api/mutations/useChangePassword.ts | 7 ++----- .../apps/web/app/api/mutations/useUpdateUser.ts | 4 ++-- .../apps/web/app/api/queries/useCurrentUser.ts | 2 +- .../web/app/modules/Dashboard/Settings/forms/UserForm.tsx | 4 ++-- 6 files changed, 8 insertions(+), 12 deletions(-) diff --git a/examples/common_nestjs_remix/apps/api/src/swagger/api-schema.json b/examples/common_nestjs_remix/apps/api/src/swagger/api-schema.json index 528fe07..b29201f 100644 --- a/examples/common_nestjs_remix/apps/api/src/swagger/api-schema.json +++ b/examples/common_nestjs_remix/apps/api/src/swagger/api-schema.json @@ -89,7 +89,7 @@ } } }, - "/auth/me": { + "/auth/current-user": { "get": { "operationId": "AuthController_me", "parameters": [], diff --git a/examples/common_nestjs_remix/apps/web/app/api/api-client.ts b/examples/common_nestjs_remix/apps/web/app/api/api-client.ts index 8671f46..291a2bd 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/api-client.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/api-client.ts @@ -1,6 +1,5 @@ import { useAuthStore } from "~/modules/Auth/authStore"; import { API } from "./generated-api"; -import { includes, some } from "lodash-es"; export const ApiClient = new API({ baseURL: import.meta.env.VITE_API_URL, diff --git a/examples/common_nestjs_remix/apps/web/app/api/mutations/useChangePassword.ts b/examples/common_nestjs_remix/apps/web/app/api/mutations/useChangePassword.ts index 260800b..393daf5 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/mutations/useChangePassword.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/mutations/useChangePassword.ts @@ -1,19 +1,16 @@ import { useMutation } from "@tanstack/react-query"; import { AxiosError } from "axios"; import { toast } from "sonner"; -import { useAuthStore } from "~/modules/Auth/authStore"; import { ApiClient } from "../api-client"; import { ChangePasswordBody } from "../generated-api"; +import { useCurrentUserSuspense } from "../queries/useCurrentUser"; type ChangePasswordOptions = { data: ChangePasswordBody; }; export function useChangePassword() { - const { currentUser } = useAuthStore(); - if (!currentUser) { - throw new Error("User is not logged in"); - } + const { data: currentUser } = useCurrentUserSuspense(); return useMutation({ mutationFn: async (options: ChangePasswordOptions) => { diff --git a/examples/common_nestjs_remix/apps/web/app/api/mutations/useUpdateUser.ts b/examples/common_nestjs_remix/apps/web/app/api/mutations/useUpdateUser.ts index 6263468..6202f25 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/mutations/useUpdateUser.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/mutations/useUpdateUser.ts @@ -6,7 +6,7 @@ import { UpdateUserBody } from "../generated-api"; import { queryClient } from "../queryClient"; import { currentUserQueryOptions, - useSafeCurrentUser, + useCurrentUserSuspense, } from "../queries/useCurrentUser"; type UpdateUserOptions = { @@ -14,7 +14,7 @@ type UpdateUserOptions = { }; export function useUpdateUser() { - const { data: currentUser } = useSafeCurrentUser(); + const { data: currentUser } = useCurrentUserSuspense(); return useMutation({ mutationFn: async (options: UpdateUserOptions) => { diff --git a/examples/common_nestjs_remix/apps/web/app/api/queries/useCurrentUser.ts b/examples/common_nestjs_remix/apps/web/app/api/queries/useCurrentUser.ts index 08bb091..c724989 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/queries/useCurrentUser.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/queries/useCurrentUser.ts @@ -13,7 +13,7 @@ export function useCurrentUser() { return useQuery(currentUserQueryOptions); } -export function useSafeCurrentUser() { +export function useCurrentUserSuspense() { const { data, ...rest } = useSuspenseQuery(currentUserQueryOptions); return { data: data?.data, ...rest }; diff --git a/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/forms/UserForm.tsx b/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/forms/UserForm.tsx index f80742e..a21b284 100644 --- a/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/forms/UserForm.tsx +++ b/examples/common_nestjs_remix/apps/web/app/modules/Dashboard/Settings/forms/UserForm.tsx @@ -3,7 +3,7 @@ import { useForm } from "react-hook-form"; import { z } from "zod"; import { UpdateUserBody } from "~/api/generated-api"; import { useUpdateUser } from "~/api/mutations/useUpdateUser"; -import { useSafeCurrentUser } from "~/api/queries/useCurrentUser"; +import { useCurrentUserSuspense } from "~/api/queries/useCurrentUser"; import { Button } from "~/components/ui/button"; import { Card, @@ -23,7 +23,7 @@ const updateUserSchema = z.object({ export default function UserForm() { const { mutate: updateUser } = useUpdateUser(); - const { data: currentUser } = useSafeCurrentUser(); + const { data: currentUser } = useCurrentUserSuspense(); const { register, From 4f3dd258c8aef6ad76578d4784cdc661252730fb Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Mon, 22 Jul 2024 19:15:47 +0200 Subject: [PATCH 15/16] fix: update API endpoint paths --- .../src/auth/__tests__/auth.controller.e2e-spec.ts | 6 +++--- .../apps/api/src/auth/api/auth.controller.ts | 2 +- .../apps/web/app/api/api-client.ts | 12 ++---------- .../apps/web/app/api/generated-api.ts | 4 ++-- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.controller.e2e-spec.ts b/examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.controller.e2e-spec.ts index d00376f..6b48812 100644 --- a/examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.controller.e2e-spec.ts +++ b/examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.controller.e2e-spec.ts @@ -174,7 +174,7 @@ describe("AuthController (e2e)", () => { }); }); - describe("GET /auth/me", () => { + describe("GET /auth/current-user", () => { it("should return current user data for authenticated user", async () => { let accessToken = ""; @@ -201,7 +201,7 @@ describe("AuthController (e2e)", () => { } const response = await request(app.getHttpServer()) - .get("/auth/me") + .get("/auth/current-user") .set("Cookie", `access_token=${accessToken};`) .expect(200); @@ -209,7 +209,7 @@ describe("AuthController (e2e)", () => { }); it("should return 401 for unauthenticated request", async () => { - await request(app.getHttpServer()).get("/auth/me").expect(401); + await request(app.getHttpServer()).get("/auth/current-user").expect(401); }); }); }); diff --git a/examples/common_nestjs_remix/apps/api/src/auth/api/auth.controller.ts b/examples/common_nestjs_remix/apps/api/src/auth/api/auth.controller.ts index e644a5f..802023f 100644 --- a/examples/common_nestjs_remix/apps/api/src/auth/api/auth.controller.ts +++ b/examples/common_nestjs_remix/apps/api/src/auth/api/auth.controller.ts @@ -100,7 +100,7 @@ export class AuthController { return null; } - @Get("me") + @Get("current-user") @Validate({ response: baseResponse(commonUserSchema), }) diff --git a/examples/common_nestjs_remix/apps/web/app/api/api-client.ts b/examples/common_nestjs_remix/apps/web/app/api/api-client.ts index 291a2bd..8df673b 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/api-client.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/api-client.ts @@ -7,29 +7,21 @@ export const ApiClient = new API({ withCredentials: true, }); -const bypassRefreshEndpoints = ["/login"]; - ApiClient.instance.interceptors.response.use( (response) => response, async (error) => { - const isLoggedIn = useAuthStore.getState().isLoggedIn; const originalRequest = error.config; + const isLoggedIn = useAuthStore.getState().isLoggedIn; - if ( - some(bypassRefreshEndpoints, (endpoint) => - includes(originalRequest.url, endpoint) - ) - ) { + if (!isLoggedIn) { return Promise.reject(error); } if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; - if (!isLoggedIn) return; try { await ApiClient.auth.authControllerRefreshTokens(); - return ApiClient.instance(originalRequest); } catch (error) { return Promise.reject(error); diff --git a/examples/common_nestjs_remix/apps/web/app/api/generated-api.ts b/examples/common_nestjs_remix/apps/web/app/api/generated-api.ts index f5995eb..49b2239 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/generated-api.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/generated-api.ts @@ -316,11 +316,11 @@ export class API extends HttpClient this.request({ - path: `/auth/me`, + path: `/auth/current-user`, method: "GET", format: "json", ...params, From 1566cfecc70cd111f9f2169e29ed1db547f9e941 Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Wed, 24 Jul 2024 11:09:33 +0200 Subject: [PATCH 16/16] feat: update current user naming and response handling --- .../apps/api/src/auth/api/auth.controller.ts | 2 +- .../apps/api/src/swagger/api-schema.json | 6 +++--- .../common_nestjs_remix/apps/web/app/api/generated-api.ts | 8 ++++---- .../apps/web/app/api/mutations/useLogoutUser.ts | 2 ++ .../apps/web/app/api/queries/useCurrentUser.ts | 8 ++++---- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/examples/common_nestjs_remix/apps/api/src/auth/api/auth.controller.ts b/examples/common_nestjs_remix/apps/api/src/auth/api/auth.controller.ts index 802023f..304f063 100644 --- a/examples/common_nestjs_remix/apps/api/src/auth/api/auth.controller.ts +++ b/examples/common_nestjs_remix/apps/api/src/auth/api/auth.controller.ts @@ -104,7 +104,7 @@ export class AuthController { @Validate({ response: baseResponse(commonUserSchema), }) - async me( + async currentUser( @CurrentUser() currentUser: { userId: string }, ): Promise>> { const account = await this.authService.currentUser(currentUser.userId); diff --git a/examples/common_nestjs_remix/apps/api/src/swagger/api-schema.json b/examples/common_nestjs_remix/apps/api/src/swagger/api-schema.json index b29201f..c23b8c1 100644 --- a/examples/common_nestjs_remix/apps/api/src/swagger/api-schema.json +++ b/examples/common_nestjs_remix/apps/api/src/swagger/api-schema.json @@ -91,14 +91,14 @@ }, "/auth/current-user": { "get": { - "operationId": "AuthController_me", + "operationId": "AuthController_currentUser", "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MeResponse" + "$ref": "#/components/schemas/CurrentUserResponse" } } } @@ -362,7 +362,7 @@ "RefreshTokensResponse": { "type": "null" }, - "MeResponse": { + "CurrentUserResponse": { "type": "object", "properties": { "data": { diff --git a/examples/common_nestjs_remix/apps/web/app/api/generated-api.ts b/examples/common_nestjs_remix/apps/web/app/api/generated-api.ts index 49b2239..ecdb4e7 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/generated-api.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/generated-api.ts @@ -51,7 +51,7 @@ export type LogoutResponse = null; export type RefreshTokensResponse = null; -export interface MeResponse { +export interface CurrentUserResponse { data: { id: string; createdAt: string; @@ -315,11 +315,11 @@ export class API extends HttpClient - this.request({ + authControllerCurrentUser: (params: RequestParams = {}) => + this.request({ path: `/auth/current-user`, method: "GET", format: "json", diff --git a/examples/common_nestjs_remix/apps/web/app/api/mutations/useLogoutUser.ts b/examples/common_nestjs_remix/apps/web/app/api/mutations/useLogoutUser.ts index 624107c..0789f5e 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/mutations/useLogoutUser.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/mutations/useLogoutUser.ts @@ -3,6 +3,7 @@ import { useMutation } from "@tanstack/react-query"; import { ApiClient } from "../api-client"; import { toast } from "sonner"; import { AxiosError } from "axios"; +import { queryClient } from "../queryClient"; export function useLogoutUser() { const { setLoggedIn } = useAuthStore(); @@ -13,6 +14,7 @@ export function useLogoutUser() { return response.data; }, onSuccess: () => { + queryClient.clear(); setLoggedIn(false); }, onError: (error) => { diff --git a/examples/common_nestjs_remix/apps/web/app/api/queries/useCurrentUser.ts b/examples/common_nestjs_remix/apps/web/app/api/queries/useCurrentUser.ts index c724989..e58c83f 100644 --- a/examples/common_nestjs_remix/apps/web/app/api/queries/useCurrentUser.ts +++ b/examples/common_nestjs_remix/apps/web/app/api/queries/useCurrentUser.ts @@ -4,17 +4,17 @@ import { ApiClient } from "../api-client"; export const currentUserQueryOptions = { queryKey: ["currentUser"], queryFn: async () => { - const response = await ApiClient.auth.authControllerMe(); + const response = await ApiClient.auth.authControllerCurrentUser(); return response.data; }, }; export function useCurrentUser() { - return useQuery(currentUserQueryOptions); + const { data, ...rest } = useQuery(currentUserQueryOptions); + return { data: data?.data, ...rest }; } export function useCurrentUserSuspense() { const { data, ...rest } = useSuspenseQuery(currentUserQueryOptions); - - return { data: data?.data, ...rest }; + return { data: data.data, ...rest }; }