From 52d86f738a34bb4c91a0db8f29bfbcb15ded251b Mon Sep 17 00:00:00 2001 From: Noa Virellia Date: Tue, 20 Jan 2026 19:37:33 +0800 Subject: [PATCH] feat: sync api changes and fix auth-related bugs Signed-off-by: Noa Virellia --- client/cms/package.json | 3 ++ client/cms/pnpm-lock.yaml | 40 ++++++++++++++++++++++++ client/cms/src/lib/axios.ts | 47 +++++++++++++++++++++-------- client/cms/src/lib/token.ts | 6 ++-- client/cms/src/routes/authorize.tsx | 26 ++++++++++------ client/cms/vite.config.ts | 22 +++++++------- 6 files changed, 109 insertions(+), 35 deletions(-) diff --git a/client/cms/package.json b/client/cms/package.json index 54952dd..469d6f8 100644 --- a/client/cms/package.json +++ b/client/cms/package.json @@ -44,6 +44,7 @@ "clsx": "^2.1.1", "culori": "^4.0.2", "immer": "^11.1.0", + "lodash-es": "^4.17.22", "lucide-react": "^0.562.0", "next-themes": "^0.4.6", "qrcode": "^1.5.4", @@ -69,6 +70,7 @@ "@tanstack/router-plugin": "^1.141.7", "@types/base-64": "^1.0.2", "@types/culori": "^4.0.1", + "@types/lodash-es": "^4.17.12", "@types/node": "^25.0.3", "@types/qrcode": "^1.5.6", "@types/react": "^19.2.5", @@ -82,6 +84,7 @@ "lint-staged": "^16.2.7", "simple-git-hooks": "^2.13.1", "tw-animate-css": "^1.4.0", + "type-fest": "^5.4.1", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4", diff --git a/client/cms/pnpm-lock.yaml b/client/cms/pnpm-lock.yaml index 1cd08d3..b5a11e7 100644 --- a/client/cms/pnpm-lock.yaml +++ b/client/cms/pnpm-lock.yaml @@ -110,6 +110,9 @@ importers: immer: specifier: ^11.1.0 version: 11.1.3 + lodash-es: + specifier: ^4.17.22 + version: 4.17.22 lucide-react: specifier: ^0.562.0 version: 0.562.0(react@19.2.3) @@ -180,6 +183,9 @@ importers: '@types/culori': specifier: ^4.0.1 version: 4.0.1 + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 '@types/node': specifier: ^25.0.3 version: 25.0.9 @@ -219,6 +225,9 @@ importers: tw-animate-css: specifier: ^1.4.0 version: 1.4.0 + type-fest: + specifier: ^5.4.1 + version: 5.4.1 typescript: specifier: ~5.9.3 version: 5.9.3 @@ -1730,6 +1739,12 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.23': + resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -2981,6 +2996,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.17.22: + resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -3650,6 +3668,10 @@ packages: resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} engines: {node: ^14.18.0 || >=16.0.0} + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + tailwind-merge@3.4.0: resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} @@ -3721,6 +3743,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@5.4.1: + resolution: {integrity: sha512-xygQcmneDyzsEuKZrFbRMne5HDqMs++aFzefrJTgEIKjQ3rekM+RPfFCVq2Gp1VIDqddoYeppCj4Pcb+RZW0GQ==} + engines: {node: '>=20'} + typescript-eslint@8.53.1: resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5392,6 +5418,12 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.23 + + '@types/lodash@4.17.23': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -6733,6 +6765,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.17.22: {} + lodash.merge@4.6.2: {} lodash@4.17.21: {} @@ -7639,6 +7673,8 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 + tagged-tag@1.0.0: {} + tailwind-merge@3.4.0: {} tailwindcss@4.1.18: {} @@ -7699,6 +7735,10 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@5.4.1: + dependencies: + tagged-tag: 1.0.0 + typescript-eslint@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) diff --git a/client/cms/src/lib/axios.ts b/client/cms/src/lib/axios.ts index f0858a6..f4d8db5 100644 --- a/client/cms/src/lib/axios.ts +++ b/client/cms/src/lib/axios.ts @@ -1,10 +1,17 @@ -import type { AxiosRequestConfig } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosRequestConfig } from 'axios'; +import type { JsonValue } from 'type-fest'; +import axios from 'axios'; +import { isNil } from 'lodash-es'; import { router } from '@/lib/router'; -import { doRefreshToken, getRefreshToken, getToken, setRefreshToken, setToken } from './token'; +import { clearTokens, doRefreshToken, getRefreshToken, getToken, setRefreshToken, setToken } from './token'; + +export const HEADER_API_VERSION = { + 'X-Api-Version': 'latest', +}; export const axiosClient = axios.create({ baseURL: '/api/v1/', + headers: HEADER_API_VERSION, }); axiosClient.interceptors.request.use((config) => { @@ -18,27 +25,43 @@ axiosClient.interceptors.request.use((config) => { type RetryConfig = AxiosRequestConfig & { _retry?: boolean }; -axiosClient.interceptors.response.use(undefined, async (error: AxiosError) => { +interface ResponseData { + code: number; + error_id: string; + status: string; + data: JsonValue; +} + +axiosClient.interceptors.response.use(async (response) => { + const data = response.data as ResponseData; + if (data.code !== 200) { + return Promise.reject(data); + } + response.data = data.data; + return response; +}, async (error: AxiosError) => { const originalRequest = error.config as RetryConfig | undefined; if (!error.response || error.response.status !== 401 || !originalRequest) { return Promise.reject(error); } - - if (error.response.status === 401 && getRefreshToken() !== null) { + if (error.response.status === 401 && originalRequest.url !== '/auth/refresh' && !isNil(getRefreshToken())) { try { - const maybeRefreshTokenValue = await doRefreshToken(); - const { access_token, refresh_token } = maybeRefreshTokenValue.data; + const maybeRefreshTokenResponse = await doRefreshToken(); + if (maybeRefreshTokenResponse.status !== 200) { + throw new Error('Failed to refresh token'); + } + const { access_token, refresh_token } = maybeRefreshTokenResponse.data; originalRequest.headers = originalRequest.headers ?? {}; originalRequest.headers.Authorization = `Bearer ${access_token}`; setToken(access_token); setRefreshToken(refresh_token); return await axiosClient(originalRequest); } + // eslint-disable-next-line unused-imports/no-unused-vars catch (e) { - if (e instanceof AxiosError && e.status === 401) { - await router.navigate({ to: '/authorize' }); - return Promise.reject(error); - } + // Should remove token (tokens are out of date) + clearTokens(); + await router.navigate({ to: '/authorize' }); } } }); diff --git a/client/cms/src/lib/token.ts b/client/cms/src/lib/token.ts index 67b9a2b..2ead7a1 100644 --- a/client/cms/src/lib/token.ts +++ b/client/cms/src/lib/token.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import { axiosClient, HEADER_API_VERSION } from './axios'; export function setToken(token: string) { localStorage.setItem('token', token); @@ -30,11 +30,11 @@ export function clearTokens() { } export async function doSetTokenByCode(code: string) { - const { data } = await axios.post<{ access_token: string; refresh_token: string }>('/api/v1/auth/token', { code }); + const { data } = await axiosClient.post<{ access_token: string; refresh_token: string }>('/auth/token', { code }, { headers: HEADER_API_VERSION }); setToken(data.access_token); setRefreshToken(data.refresh_token); } export async function doRefreshToken() { - return axios.post<{ access_token: string; refresh_token: string }>('/api/v1/auth/refresh', { refresh_token: getRefreshToken() }); + return axiosClient.post<{ access_token: string; refresh_token: string }>('/auth/refresh', { refresh_token: getRefreshToken() }, { headers: HEADER_API_VERSION }); } diff --git a/client/cms/src/routes/authorize.tsx b/client/cms/src/routes/authorize.tsx index 3239bbe..7f3c164 100644 --- a/client/cms/src/routes/authorize.tsx +++ b/client/cms/src/routes/authorize.tsx @@ -1,7 +1,9 @@ import { createFileRoute } from '@tanstack/react-router'; import { zodValidator } from '@tanstack/zod-adapter'; +import { isNil } from 'lodash-es'; import z from 'zod'; import { LoginForm } from '@/components/login-form'; +import { axiosClient } from '@/lib/axios'; import { generateOAuthState } from '@/lib/random'; import { getToken } from '@/lib/token'; @@ -22,15 +24,21 @@ export const Route = createFileRoute('/authorize')({ function RouteComponent() { const token = getToken(); const oauthParams = Route.useSearch(); - if (token !== null) { - const base = new URL(window.location.origin); - const url = new URL('/api/v1/auth/redirect', base); - url.searchParams.set('client_id', oauthParams.client_id); - url.searchParams.set('response_type', oauthParams.response_type); - url.searchParams.set('redirect_uri', oauthParams.redirect_uri); - url.searchParams.set('state', oauthParams.state); - window.location.href = url.toString(); - return null; + /** + * Auth by Token Flow + */ + if (!isNil(token)) { + axiosClient.post<{ redirect_uri: string }>('/auth/exchange', { + client_id: oauthParams.client_id, + redirect_uri: oauthParams.redirect_uri, + state: oauthParams.state, + }).then((res) => { + window.location.href = res.data.redirect_uri; + }).catch((e) => { + console.error(e); + return 'Token exchange failed'; + }); + return 'Redirecting'; } return (
diff --git a/client/cms/vite.config.ts b/client/cms/vite.config.ts index 655fbc8..06cd972 100644 --- a/client/cms/vite.config.ts +++ b/client/cms/vite.config.ts @@ -1,15 +1,15 @@ -import path from "node:path"; -import tailwindcss from "@tailwindcss/vite"; -import { tanstackRouter } from "@tanstack/router-plugin/vite"; -import react from "@vitejs/plugin-react"; -import { defineConfig } from "vite"; -import svgr from "vite-plugin-svgr"; +import path from 'node:path'; +import tailwindcss from '@tailwindcss/vite'; +import { tanstackRouter } from '@tanstack/router-plugin/vite'; +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; +import svgr from 'vite-plugin-svgr'; // https://vite.dev/config/ export default defineConfig({ plugins: [ tanstackRouter({ - target: "react", + target: 'react', autoCodeSplitting: true, }), react(), @@ -18,15 +18,15 @@ export default defineConfig({ ], resolve: { alias: { - "@": path.resolve(__dirname, "./src"), + '@': path.resolve(__dirname, './src'), }, }, server: { proxy: { - "/api": "http://10.0.0.250:8000", + '/api': 'http://10.0.0.10:8000', }, - host: "0.0.0.0", + host: '0.0.0.0', port: 5173, - allowedHosts: ["nix.org.cn", "nixos.party"], + allowedHosts: ['nix.org.cn', 'nixos.party'], }, });