├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public ├── axios-umi-request-y-interceptors.png ├── token-management.png └── vite.svg ├── src ├── App.css ├── App.tsx ├── apis │ ├── apollo-client │ │ └── apollo-client.ts │ ├── axios-gentype │ │ ├── api-axios.ts │ │ └── request.ts │ ├── axios │ │ └── request.ts │ ├── brainless-token-management │ │ └── request.ts │ ├── token-management │ │ ├── request.ts │ │ └── tokenManagement.ts │ └── umirequest │ │ └── request.ts ├── assets │ └── react.svg ├── components │ ├── RefreshByInterceptor │ │ └── RefreshByInterceptor.tsx │ └── RefreshByTokenManager │ │ └── RefreshByTokenManager.tsx ├── index.css ├── main.tsx └── vite-env.d.ts ├── swagger-typescript-api.config.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | 17 | [COMMIT_EDITMSG] 18 | max_line_length = 0 19 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_APP_API=https://nestjs-vercel-197.vercel.app 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | 5 | node_modules 6 | 7 | # Ignore all HTML files: 8 | *.html 9 | 10 | .github 11 | 12 | .next 13 | 14 | .swc 15 | 16 | next.config.js 17 | 18 | next-i18next.config.js 19 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "rules": { 4 | "react/react-in-jsx-scope": "off", 5 | "react/display-name": "off", 6 | "react/prop-types": "off", 7 | "react/jsx-key": "error", 8 | "no-console": 1, 9 | "no-unused-vars": "off", 10 | "@typescript-eslint/no-unused-vars": "error" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | # .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | 5 | node_modules 6 | 7 | # Ignore all HTML files: 8 | *.html 9 | 10 | .github 11 | 12 | .next 13 | 14 | .swc 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "jsxBracketSameLine": false, 7 | "endOfLine": "auto", 8 | "jsxSingleQuote": true, 9 | "trailingComma": "all", 10 | "arrowParens": "always" 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.rulers": [100], 4 | "editor.formatOnSave": true, 5 | "git.ignoreLimitWarning": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Handle refresh token with `axios`, `umi-request` using interceptors, `apollo-client`, `token-management`, `brainless-token-manager` 2 | 3 | ### 1. Axios interceptors, Apollo-client 4 | - After all requests failed, we will call a request to take a new access token after that retry all requests which failed 5 | 6 | ![alt text](./public/axios-umi-request-y-interceptors.png) 7 | 8 | ### 2. `brainless-token-manager` 9 | - Check access token expire if token expire will call a request to take a new access token after that call requests 10 | 11 | [brainless-token-manager](https://www.npmjs.com/package/brainless-token-manager) 12 | 13 | ![alt text](./public/token-management.png) 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Handle Refresh Token 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjs-refresh-token", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "start": "npm run build && vite preview", 11 | "lint": "eslint --ext .ts,.tsx src --color", 12 | "format": "prettier --write \"./src/**/*.{ts,tsx,json}\"", 13 | "analyze": "source-map-explorer 'dist/assets/*.js'", 14 | "g": "swagger-typescript-api-es", 15 | "verify-commit": "verify-commit-msg", 16 | "prepare": "git-scm-hooks" 17 | }, 18 | "dependencies": { 19 | "antd-dayjs-vite-plugin": "^1.2.2", 20 | "axios": "^1.7.4", 21 | "brainless-token-manager": "^1.3.3", 22 | "jwt-decode": "^3.1.2", 23 | "react": "^18.3.1", 24 | "react-dom": "^18.3.1", 25 | "react-gh-corners": "^1.3.6", 26 | "umi-request": "^1.4.0" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^18.19.44", 30 | "@types/react": "^18.3.3", 31 | "@types/react-dom": "^18.3.0", 32 | "@vitejs/plugin-react": "^3.1.0", 33 | "eslint": "^8.57.0", 34 | "eslint-config-react-app": "^7.0.1", 35 | "git-scm-hooks": "^0.2.0", 36 | "husky": "^8.0.3", 37 | "prettier": "^2.8.8", 38 | "sass": "^1.77.8", 39 | "source-map-explorer": "^2.5.3", 40 | "swagger-typescript-api-es": "^0.0.5", 41 | "typescript": "^4.9.5", 42 | "verify-commit-msg": "^0.1.0", 43 | "vite": "^4.5.3", 44 | "vite-plugin-checker": "^0.5.6", 45 | "vite-plugin-environment": "^1.1.3" 46 | }, 47 | "packageManager": "pnpm@9.7.1", 48 | "git-hooks": { 49 | "pre-commit": "npm run lint", 50 | "commit-msg": "npm run verify-commit" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/axios-umi-request-y-interceptors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunghg255/reactjs-handle-refresh-token/50ea1e54ba2d57bc71b72efd4a8364aa293001c3/public/axios-umi-request-y-interceptors.png -------------------------------------------------------------------------------- /public/token-management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunghg255/reactjs-handle-refresh-token/50ea1e54ba2d57bc71b72efd4a8364aa293001c3/public/token-management.png -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | body { 9 | background: black; 10 | } 11 | 12 | .logo { 13 | height: 6em; 14 | padding: 1.5em; 15 | will-change: filter; 16 | } 17 | .logo:hover { 18 | filter: drop-shadow(0 0 2em #646cffaa); 19 | } 20 | .logo.react:hover { 21 | filter: drop-shadow(0 0 2em #61dafbaa); 22 | } 23 | 24 | @keyframes logo-spin { 25 | from { 26 | transform: rotate(0deg); 27 | } 28 | to { 29 | transform: rotate(360deg); 30 | } 31 | } 32 | 33 | @media (prefers-reduced-motion: no-preference) { 34 | a:nth-of-type(2) .logo { 35 | animation: logo-spin infinite 20s linear; 36 | } 37 | } 38 | 39 | .card { 40 | padding: 2em; 41 | } 42 | 43 | .read-the-docs { 44 | color: #888; 45 | } 46 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { GithubCorners } from 'react-gh-corners'; 2 | 3 | import './App.css'; 4 | // import RefreshByInterceptor from '@/components/RefreshByInterceptor/RefreshByInterceptor'; 5 | import RefreshByTokenManager from '@/components/RefreshByTokenManager/RefreshByTokenManager'; 6 | // import RefreshByTokenManager from '@/components/RefreshByTokenManager/RefreshByTokenManager'; 7 | 8 | function App() { 9 | return ( 10 |
11 | {/* */} 12 | 13 | 14 | 19 |
20 | ); 21 | } 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /src/apis/apollo-client/apollo-client.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /* eslint-disable no-loop-func */ 3 | // @ts-nocheck 4 | import { ApolloClient, ApolloLink, fromPromise, InMemoryCache, split } from '@apollo/client'; 5 | import { setContext } from '@apollo/client/link/context'; 6 | import { onError } from '@apollo/client/link/error'; 7 | import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; 8 | import { getMainDefinition } from '@apollo/client/utilities'; 9 | import { createUploadLink } from 'apollo-upload-client'; 10 | import { createClient } from 'graphql-ws'; 11 | import Cookies from 'js-cookie'; 12 | 13 | import { API_URL, WS_URL } from 'constant'; 14 | import { authKeys } from 'utils/cookie'; 15 | 16 | const authLink = setContext((_, { headers }) => { 17 | const token = Cookies.get(authKeys.accessToken) || ''; 18 | 19 | return { 20 | headers: { 21 | ...headers, 22 | authorization: token ? `Bearer ${token}` : '', 23 | }, 24 | }; 25 | }); 26 | 27 | const uploadLink = createUploadLink({ 28 | uri: API_URL, 29 | credentials: 'include', 30 | }); 31 | 32 | const wsLink = new GraphQLWsLink( 33 | createClient({ 34 | url: WS_URL, 35 | keepAlive: 5000, 36 | connectionParams() { 37 | const token = Cookies.get(authKeys.accessToken) || ''; 38 | 39 | return { 40 | connectionParams: { 41 | authorization: token, 42 | }, 43 | }; 44 | }, 45 | }), 46 | ); 47 | 48 | const REFRESH_TOKEN_MUTATION = gql` 49 | mutation RefreshUserToken($data: RefreshUserTokenInput!) { 50 | refreshUserToken(data: $data) { 51 | accessToken 52 | expiresIn 53 | } 54 | } 55 | `; 56 | 57 | export const onRefreshToken = async () => { 58 | try { 59 | if (!Cookies.get(authKeys.refreshToken)) throw new Error('No refresh token'); 60 | 61 | const refreshResolverResponse = await client.mutate({ 62 | mutation: REFRESH_TOKEN_MUTATION, 63 | variables: { 64 | data: { 65 | refreshToken: Cookies.get(authKeys.refreshToken), 66 | }, 67 | }, 68 | }); 69 | 70 | Cookies.set(authKeys.accessToken, refreshResolverResponse.data.refreshUserToken.accessToken); 71 | Cookies.set(authKeys.expiresIn, refreshResolverResponse.data.refreshUserToken.expiresIn); 72 | 73 | return refreshResolverResponse.data.refreshUserToken.accessToken; 74 | } catch (error) { 75 | Cookies.remove(authKeys.accessToken); 76 | Cookies.remove(authKeys.refreshToken); 77 | Cookies.remove(authKeys.lastLoginTime); 78 | Cookies.remove(authKeys.refreshTokenExpiresIn); 79 | Cookies.remove(authKeys.expiresIn); 80 | } 81 | }; 82 | 83 | let isRefreshing = false; 84 | let pendingRequests = [] as any[]; 85 | 86 | const resolvePendingRequests = (newToken) => { 87 | pendingRequests.map((callback: (v: string) => void) => callback(newToken)); 88 | pendingRequests = []; 89 | }; 90 | 91 | const errorLink = onError(({ graphQLErrors, operation, forward }) => { 92 | if (graphQLErrors) { 93 | for (const err of graphQLErrors) { 94 | // Pass through if the error is not an authentication error 95 | if ((err as any).extensions?.response?.message !== 'Unauthorized') { 96 | forward(operation); 97 | continue; 98 | } 99 | 100 | if (operation.operationName === 'refreshUserToken') return; 101 | 102 | let forward$; 103 | 104 | if (!isRefreshing) { 105 | isRefreshing = true; 106 | forward$ = fromPromise( 107 | onRefreshToken() 108 | .then((accessToken) => { 109 | // Store the new tokens for your auth link 110 | resolvePendingRequests(accessToken); 111 | return accessToken; 112 | }) 113 | .catch(() => { 114 | pendingRequests = []; 115 | // Handle token refresh errors e.g clear stored tokens, redirect to login, ... 116 | return; 117 | }) 118 | .finally(() => { 119 | isRefreshing = false; 120 | }), 121 | ).filter((value) => Boolean(value)); 122 | } else { 123 | // Will only emit once the Promise is resolved 124 | forward$ = fromPromise( 125 | new Promise((resolve) => { 126 | pendingRequests.push((newToken: string) => resolve(newToken)); 127 | }), 128 | ); 129 | } 130 | 131 | return forward$.flatMap((newToken) => { 132 | if (newToken) { 133 | const oldHeaders = operation.getContext().headers; 134 | // modify the operation context with a new token 135 | operation.setContext({ 136 | headers: { 137 | ...oldHeaders, 138 | authorization: `Bearer ${newToken}`, 139 | }, 140 | }); 141 | } 142 | 143 | return forward(operation); 144 | }); 145 | } 146 | } 147 | }); 148 | 149 | // const splitLink = split( 150 | // ({ query }) => { 151 | // const definition = getMainDefinition(query); 152 | 153 | // return definition.kind === 'OperationDefinition' && definition.operation === 'subscription'; 154 | // }, 155 | // wsLink, 156 | // ApolloLink.from([errorLink, authLink, uploadLink]), 157 | // ); 158 | 159 | export const client = new ApolloClient({ 160 | link: ApolloLink.from([authLink, errorLink, uploadLink, wsLink]), 161 | cache: new InMemoryCache(), 162 | name: 'web-' + process.env.REACT_APP_MODE, 163 | }); 164 | -------------------------------------------------------------------------------- /src/apis/axios-gentype/api-axios.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* 3 | * ---------------------------------------------------------------------- 4 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API-ES ## 5 | * ## SOURCE: https://github.com/hunghg255/swagger-typescript-api-es ## 6 | * ---------------------------------------------------------------------- 7 | */ 8 | 9 | export interface Post { 10 | id: string; 11 | title: string; 12 | description: string; 13 | tags: string[]; 14 | } 15 | 16 | export interface GetPostsDtoRes { 17 | posts: Post; 18 | current_page: number; 19 | total_page: number; 20 | page_size: number; 21 | total: number; 22 | } 23 | 24 | export interface CreatePostsDtoReq { 25 | title: string; 26 | description: string; 27 | /** @example ["Html"] */ 28 | tags?: string[]; 29 | } 30 | 31 | export interface LoginDtoReq { 32 | /** @example "admin" */ 33 | username: string; 34 | } 35 | 36 | export interface LoginDtoRes { 37 | accessToken: string; 38 | refreshToken: string; 39 | } 40 | 41 | export interface RefreshTokenDtoReq { 42 | refreshToken: string; 43 | } 44 | 45 | export interface RefreshTokenDtoRes { 46 | accessToken: string; 47 | refreshToken: string; 48 | } 49 | 50 | export interface GalleriesRes { 51 | id: string; 52 | imageUrl: string; 53 | description: string; 54 | } 55 | 56 | import type { 57 | AxiosInstance, 58 | AxiosRequestConfig, 59 | AxiosResponse, 60 | HeadersDefaults, 61 | ResponseType, 62 | } from 'axios'; 63 | import axios from 'axios'; 64 | 65 | export type QueryParamsType = Record; 66 | 67 | export interface FullRequestParams 68 | extends Omit { 69 | /** set parameter to `true` for call `securityWorker` for this request */ 70 | secure?: boolean; 71 | /** request path */ 72 | path: string; 73 | /** content type of request body */ 74 | type?: ContentType; 75 | /** query params */ 76 | query?: QueryParamsType; 77 | /** format of response (i.e. response.json() -> format: "json") */ 78 | format?: ResponseType; 79 | /** request body */ 80 | body?: unknown; 81 | } 82 | 83 | export type RequestParams = Omit; 84 | 85 | export interface ApiConfig 86 | extends Omit { 87 | securityWorker?: ( 88 | securityData: SecurityDataType | null, 89 | ) => Promise | AxiosRequestConfig | void; 90 | secure?: boolean; 91 | format?: ResponseType; 92 | 93 | instance?: AxiosInstance; 94 | injectHeaders?: (data: any) => any; 95 | } 96 | 97 | export enum ContentType { 98 | Json = 'application/json', 99 | FormData = 'multipart/form-data', 100 | UrlEncoded = 'application/x-www-form-urlencoded', 101 | Text = 'text/plain', 102 | } 103 | 104 | export class HttpClient { 105 | public instance: AxiosInstance; 106 | private securityData: SecurityDataType | null = null; 107 | private securityWorker?: ApiConfig['securityWorker']; 108 | private secure?: boolean; 109 | private format?: ResponseType; 110 | private injectHeaders?: (data: any) => any; 111 | 112 | constructor({ 113 | securityWorker, 114 | secure, 115 | format, 116 | instance, 117 | injectHeaders, 118 | ...axiosConfig 119 | }: ApiConfig = {}) { 120 | this.instance = 121 | instance ?? axios.create({ ...axiosConfig, baseURL: axiosConfig.baseURL || '' }); 122 | this.secure = secure; 123 | this.format = format; 124 | this.securityWorker = securityWorker; 125 | this.injectHeaders = injectHeaders; 126 | } 127 | 128 | public setSecurityData = (data: SecurityDataType | null) => { 129 | this.securityData = data; 130 | }; 131 | 132 | protected mergeRequestParams( 133 | params1: AxiosRequestConfig, 134 | params2?: AxiosRequestConfig, 135 | ): AxiosRequestConfig { 136 | const method = params1.method || (params2 && params2.method); 137 | 138 | return { 139 | ...this.instance.defaults, 140 | ...params1, 141 | ...(params2 || {}), 142 | headers: { 143 | ...((method && 144 | this.instance.defaults.headers[method.toLowerCase() as keyof HeadersDefaults]) || 145 | {}), 146 | ...(params1.headers || {}), 147 | ...((params2 && params2.headers) || {}), 148 | }, 149 | }; 150 | } 151 | 152 | protected stringifyFormItem(formItem: unknown) { 153 | if (typeof formItem === 'object' && formItem !== null) { 154 | return JSON.stringify(formItem); 155 | } else { 156 | return `${formItem}`; 157 | } 158 | } 159 | 160 | protected createFormData(input: Record): FormData { 161 | return Object.keys(input || {}).reduce((formData, key) => { 162 | const property = input[key]; 163 | const propertyContent: any[] = property instanceof Array ? property : [property]; 164 | 165 | for (const formItem of propertyContent) { 166 | const isFileType = formItem instanceof Blob || formItem instanceof File; 167 | formData.append(key, isFileType ? formItem : this.stringifyFormItem(formItem)); 168 | } 169 | 170 | return formData; 171 | }, new FormData()); 172 | } 173 | 174 | public request = async ({ 175 | secure, 176 | path, 177 | type, 178 | query, 179 | format, 180 | body, 181 | ...params 182 | }: FullRequestParams): Promise> => { 183 | const secureParams = 184 | ((typeof secure === 'boolean' ? secure : this.secure) && 185 | this.securityWorker && 186 | (await this.securityWorker(this.securityData))) || 187 | {}; 188 | const requestParams = this.mergeRequestParams(params, secureParams); 189 | const responseFormat = format || this.format || undefined; 190 | 191 | if (type === ContentType.FormData && body && body !== null && typeof body === 'object') { 192 | body = this.createFormData(body as Record); 193 | } 194 | 195 | if (type === ContentType.Text && body && body !== null && typeof body !== 'string') { 196 | body = JSON.stringify(body); 197 | } 198 | 199 | let headers = { 200 | ...(requestParams.headers || {}), 201 | ...(type && type !== ContentType.FormData ? { 'Content-Type': type } : {}), 202 | }; 203 | 204 | if (this.injectHeaders) { 205 | headers = await this.injectHeaders(headers); 206 | } 207 | 208 | return this.instance.request({ 209 | ...requestParams, 210 | headers, 211 | params: query, 212 | responseType: responseFormat, 213 | data: body, 214 | url: path, 215 | }); 216 | }; 217 | } 218 | 219 | /** 220 | * @title Agiletech test 221 | * @version 1.0 222 | * @contact 223 | */ 224 | export class Api extends HttpClient { 225 | /** 226 | * No description 227 | * 228 | * @name AppControllerGetHello 229 | * @request GET:/ 230 | */ 231 | appControllerGetHello = (params: RequestParams = {}) => 232 | this.request({ 233 | path: `/`, 234 | method: 'GET', 235 | ...params, 236 | }); 237 | 238 | posts = { 239 | /** 240 | * @description Get tags 241 | * 242 | * @tags Posts 243 | * @name Tags 244 | * @summary Get tags 245 | * @request GET:/posts/tags 246 | * @secure 247 | */ 248 | tags: (params: RequestParams = {}) => 249 | this.request({ 250 | path: `/posts/tags`, 251 | method: 'GET', 252 | secure: true, 253 | ...params, 254 | }), 255 | 256 | /** 257 | * @description Get post 258 | * 259 | * @tags Posts 260 | * @name Posts 261 | * @summary Get posts 262 | * @request GET:/posts 263 | * @secure 264 | */ 265 | posts: ( 266 | query?: { 267 | /** @example "1" */ 268 | page?: string; 269 | /** @example "title" */ 270 | title?: string; 271 | /** @example "Html" */ 272 | tags?: string; 273 | }, 274 | params: RequestParams = {}, 275 | ) => 276 | this.request({ 277 | path: `/posts`, 278 | method: 'GET', 279 | query: query, 280 | secure: true, 281 | ...params, 282 | }), 283 | 284 | /** 285 | * @description Create new post 286 | * 287 | * @tags Posts 288 | * @name CraetePosts 289 | * @summary Create new post 290 | * @request POST:/posts 291 | * @secure 292 | */ 293 | craetePosts: (data: CreatePostsDtoReq, params: RequestParams = {}) => 294 | this.request({ 295 | path: `/posts`, 296 | method: 'POST', 297 | body: data, 298 | secure: true, 299 | type: ContentType.Json, 300 | ...params, 301 | }), 302 | 303 | /** 304 | * @description Edit post 305 | * 306 | * @tags Posts 307 | * @name EditPosts 308 | * @summary Edit post 309 | * @request PATCH:/posts/{postId} 310 | * @secure 311 | */ 312 | editPosts: (postId: any, params: RequestParams = {}) => 313 | this.request({ 314 | path: `/posts/${postId}`, 315 | method: 'PATCH', 316 | secure: true, 317 | ...params, 318 | }), 319 | 320 | /** 321 | * @description Delete post 322 | * 323 | * @tags Posts 324 | * @name DeletePost 325 | * @summary Delete post 326 | * @request DELETE:/posts/{postId} 327 | * @secure 328 | */ 329 | deletePost: (postId: any, params: RequestParams = {}) => 330 | this.request({ 331 | path: `/posts/${postId}`, 332 | method: 'DELETE', 333 | secure: true, 334 | ...params, 335 | }), 336 | }; 337 | auth = { 338 | /** 339 | * @description Account: admin, admin1, admin2, adminRefresh, adminRefresh1, adminRefresh2 340 | * 341 | * @tags Auth 342 | * @name Login 343 | * @summary Login 344 | * @request POST:/auth/login 345 | */ 346 | login: (data: LoginDtoReq, params: RequestParams = {}) => 347 | this.request({ 348 | path: `/auth/login`, 349 | method: 'POST', 350 | body: data, 351 | type: ContentType.Json, 352 | ...params, 353 | }), 354 | 355 | /** 356 | * No description 357 | * 358 | * @tags Auth 359 | * @name RefreshToken 360 | * @summary Refresh token 361 | * @request POST:/auth/refresh-token 362 | */ 363 | refreshToken: (data: RefreshTokenDtoReq, params: RequestParams = {}) => 364 | this.request({ 365 | path: `/auth/refresh-token`, 366 | method: 'POST', 367 | body: data, 368 | type: ContentType.Json, 369 | ...params, 370 | }), 371 | 372 | /** 373 | * No description 374 | * 375 | * @tags Auth 376 | * @name Logout 377 | * @summary Logout 378 | * @request DELETE:/auth/logout 379 | * @secure 380 | */ 381 | logout: (params: RequestParams = {}) => 382 | this.request({ 383 | path: `/auth/logout`, 384 | method: 'DELETE', 385 | secure: true, 386 | ...params, 387 | }), 388 | }; 389 | galleries = { 390 | /** 391 | * No description 392 | * 393 | * @tags Galleries 394 | * @name Galleries 395 | * @summary Get galleries 396 | * @request GET:/galleries 397 | */ 398 | galleries: (params: RequestParams = {}) => 399 | this.request({ 400 | path: `/galleries`, 401 | method: 'GET', 402 | format: 'json', 403 | ...params, 404 | }), 405 | }; 406 | } 407 | -------------------------------------------------------------------------------- /src/apis/axios-gentype/request.ts: -------------------------------------------------------------------------------- 1 | import TokenManagement from 'brainless-token-manager'; 2 | import axios from 'axios'; 3 | import { Api } from '@/apis/axios-gentype/api-axios'; 4 | 5 | export const axiosInstant = axios.create({ 6 | baseURL: process.env.VITE_APP_API, 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | }, 10 | }); 11 | 12 | export const TokenManager = new TokenManagement({ 13 | getAccessToken: async () => { 14 | const token = localStorage.getItem('accessToken'); 15 | 16 | return `${token}`; 17 | }, 18 | getRefreshToken: async () => { 19 | const refreshToken = localStorage.getItem('refreshToken'); 20 | 21 | return `${refreshToken}`; 22 | }, 23 | onInvalidRefreshToken: () => { 24 | // Logout, redirect to login 25 | localStorage.removeItem('accessToken'); 26 | localStorage.removeItem('refreshToken'); 27 | }, 28 | executeRefreshToken: async () => { 29 | const refreshToken = localStorage.getItem('refreshToken'); 30 | 31 | if (!refreshToken) { 32 | return { 33 | token: '', 34 | refresh_token: '', 35 | }; 36 | } 37 | 38 | const r = await axiosInstant.post('/auth/refresh-token', { 39 | refreshToken: refreshToken, 40 | }); 41 | 42 | return { 43 | token: r?.data?.accessToken, 44 | refresh_token: r?.data?.refreshToken, 45 | }; 46 | }, 47 | onRefreshTokenSuccess: ({ token, refresh_token }) => { 48 | if (token && refresh_token) { 49 | localStorage.setItem('accessToken', token); 50 | localStorage.setItem('refreshToken', refresh_token); 51 | } 52 | }, 53 | }); 54 | 55 | export const injectHeaders = async (headers: any) => { 56 | const token: string = (await TokenManager.getToken()) as string; 57 | 58 | if (!headers) { 59 | return { 60 | Authorization: `Bearer ${token}`, 61 | }; 62 | } 63 | 64 | if (headers?.Authorization) { 65 | return { 66 | ...headers, 67 | }; 68 | } 69 | 70 | if (headers) { 71 | return { 72 | ...headers, 73 | Authorization: `Bearer ${token}`, 74 | }; 75 | } 76 | 77 | return { 78 | ...headers, 79 | Authorization: `Bearer ${token}`, 80 | }; 81 | }; 82 | 83 | // const successHandler = async (response: AxiosResponse) => { 84 | // return response; 85 | // }; 86 | 87 | // const errorHandler = (error: AxiosError) => { 88 | // const resError: AxiosResponse | undefined = error.response; 89 | 90 | // return Promise.reject({ ...resError?.data }); 91 | // }; 92 | 93 | // axiosInstant.interceptors.request.use( 94 | // async (request: any) => { 95 | // return request; 96 | // }, 97 | // (error) => { 98 | // Promise.reject(error); 99 | // }, 100 | // ); 101 | 102 | // axiosInstant.interceptors.response.use( 103 | // (response: any) => successHandler(response), 104 | // (error: any) => errorHandler(error), 105 | // ); 106 | 107 | const api = new Api({ 108 | instance: axiosInstant, 109 | injectHeaders, 110 | }); 111 | 112 | export { api }; 113 | -------------------------------------------------------------------------------- /src/apis/axios/request.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError, AxiosResponse } from 'axios'; 2 | 3 | export const axiosInstant = axios.create({ 4 | baseURL: process.env.VITE_APP_API, 5 | headers: { 6 | 'Content-Type': 'application/json', 7 | }, 8 | }); 9 | 10 | let isRefreshing = false; 11 | const refreshSubscribers: any[] = []; 12 | function subscribeTokenRefresh(cb: any) { 13 | refreshSubscribers.push(cb); 14 | } 15 | 16 | function onRefreshed(token: any) { 17 | refreshSubscribers.forEach((cb) => cb(token)); 18 | } 19 | 20 | axiosInstant.interceptors.request.use( 21 | async (config: any) => { 22 | const accessToken = localStorage.getItem('accessToken'); 23 | 24 | config.headers = { 25 | Authorization: `Bearer ${accessToken}`, 26 | Accept: 'application/json', 27 | }; 28 | return config; 29 | }, 30 | (error) => { 31 | Promise.reject(error); 32 | }, 33 | ); 34 | 35 | const onRefreshToken = async () => { 36 | let refreshToken = localStorage.getItem('refreshToken'); 37 | 38 | return axios.post(process.env.VITE_APP_API + '/auth/refresh-token', { 39 | refreshToken, 40 | }); 41 | }; 42 | 43 | const successHandler = async (response: AxiosResponse) => { 44 | return response; 45 | }; 46 | 47 | const errorHandler = (error: AxiosError) => { 48 | const resError: AxiosResponse | undefined = error.response; 49 | const originalRequest: any = error.config; 50 | 51 | if (resError?.status === 403) { 52 | if (!isRefreshing) { 53 | isRefreshing = true; 54 | onRefreshToken().then((data: any) => { 55 | isRefreshing = false; 56 | if (data?.data?.accessToken) { 57 | localStorage.setItem('accessToken', data?.data?.accessToken); 58 | localStorage.setItem('refreshToken', data?.data?.refreshToken); 59 | onRefreshed(data?.data?.accessToken); 60 | } 61 | }); 62 | } 63 | return new Promise((resolve) => { 64 | subscribeTokenRefresh(async (token: string) => { 65 | originalRequest.headers['Authorization'] = 'Bearer ' + token; 66 | resolve(axiosInstant.request(originalRequest)); 67 | }); 68 | }); 69 | } 70 | 71 | return Promise.reject({ ...resError?.data }); 72 | }; 73 | 74 | axiosInstant.interceptors.response.use( 75 | (response: any) => successHandler(response), 76 | (error: any) => errorHandler(error), 77 | ); 78 | -------------------------------------------------------------------------------- /src/apis/brainless-token-management/request.ts: -------------------------------------------------------------------------------- 1 | import { extend } from 'umi-request'; 2 | import TokenManager, { injectBearer } from 'brainless-token-manager'; 3 | 4 | // Can implement by umi-request, axios, fetch.... 5 | export const requestNew = extend({ 6 | prefix: process.env.VITE_APP_API, 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | }, 10 | errorHandler: (error) => { 11 | throw error?.data || error?.response; 12 | }, 13 | }); 14 | 15 | const tokenManager = new TokenManager({ 16 | getAccessToken: async () => { 17 | const token = localStorage.getItem('accessToken'); 18 | 19 | return `${token}`; 20 | }, 21 | getRefreshToken: async () => { 22 | const refreshToken = localStorage.getItem('refreshToken'); 23 | 24 | return `${refreshToken}`; 25 | }, 26 | onInvalidRefreshToken: () => { 27 | // Logout, redirect to login 28 | localStorage.removeItem('accessToken'); 29 | localStorage.removeItem('refreshToken'); 30 | }, 31 | executeRefreshToken: async () => { 32 | const refreshToken = localStorage.getItem('refreshToken'); 33 | 34 | if (!refreshToken) { 35 | return { 36 | token: '', 37 | refresh_token: '', 38 | }; 39 | } 40 | 41 | const r = await requestNew.post('/auth/refresh-token', { 42 | data: { 43 | refreshToken: refreshToken, 44 | }, 45 | }); 46 | 47 | return { 48 | token: r?.accessToken, 49 | refresh_token: r?.refreshToken, 50 | }; 51 | }, 52 | onRefreshTokenSuccess: ({ token, refresh_token }) => { 53 | if (token && refresh_token) { 54 | localStorage.setItem('accessToken', token); 55 | localStorage.setItem('refreshToken', refresh_token); 56 | } 57 | }, 58 | }); 59 | 60 | export const privateRequestNew = async (request: any, suffixUrl: string, configs?: any) => { 61 | const token: string = configs?.token 62 | ? configs?.token 63 | : ((await tokenManager.getToken()) as string); 64 | 65 | return request(suffixUrl, injectBearer(token, configs)); 66 | }; 67 | -------------------------------------------------------------------------------- /src/apis/token-management/request.ts: -------------------------------------------------------------------------------- 1 | import { extend } from 'umi-request'; 2 | import TokenManagement, { parseJwt } from './tokenManagement'; 3 | 4 | // Can implement by umi-request, axios, fetch.... 5 | export const request = extend({ 6 | prefix: process.env.VITE_APP_API, 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | }, 10 | errorHandler: (error) => { 11 | throw error?.data || error?.response; 12 | }, 13 | }); 14 | 15 | const injectBearer = (token: string, configs: any) => { 16 | if (!configs) { 17 | return { 18 | headers: { 19 | Authorization: `Bearer ${token}`, 20 | }, 21 | }; 22 | } 23 | 24 | if (configs?.headers?.Authorization) { 25 | return { 26 | ...configs, 27 | headers: { 28 | ...configs.headers, 29 | }, 30 | }; 31 | } 32 | 33 | if (configs?.headers) { 34 | return { 35 | ...configs, 36 | headers: { 37 | ...configs.headers, 38 | Authorization: `Bearer ${token}`, 39 | }, 40 | }; 41 | } 42 | 43 | return { 44 | ...configs, 45 | headers: { 46 | Authorization: `Bearer ${token}`, 47 | }, 48 | }; 49 | }; 50 | 51 | const TokenManager = new TokenManagement({ 52 | isTokenValid: () => { 53 | try { 54 | const token = localStorage.getItem('accessToken'); 55 | 56 | const decoded = parseJwt(token); 57 | const { exp } = decoded; 58 | 59 | const currentTime = Date.now() / 1000; 60 | 61 | if (exp - 5 > currentTime) { 62 | return true; 63 | } 64 | 65 | return false; 66 | } catch (error) { 67 | return false; 68 | } 69 | }, 70 | getAccessToken: () => { 71 | const token = localStorage.getItem('accessToken'); 72 | 73 | return `${token}`; 74 | }, 75 | onRefreshToken(done) { 76 | const refreshToken = localStorage.getItem('refreshToken'); 77 | if (!refreshToken) { 78 | return done(null); 79 | } 80 | 81 | request 82 | .post('/auth/refresh-token', { 83 | data: { 84 | refreshToken: refreshToken, 85 | }, 86 | }) 87 | .then((result) => { 88 | if (result?.accessToken && result?.refreshToken) { 89 | localStorage.setItem('accessToken', result?.accessToken); 90 | localStorage.setItem('refreshToken', result?.refreshToken); 91 | 92 | done(result.accessToken); 93 | 94 | return; 95 | } 96 | done(null); 97 | }) 98 | .catch((err) => { 99 | done(null); 100 | }); 101 | }, 102 | }); 103 | 104 | export const privateRequest = async (request: any, suffixUrl: string, configs?: any) => { 105 | const token: string = configs?.token 106 | ? configs?.token 107 | : ((await TokenManager.getToken()) as string); 108 | 109 | return request(suffixUrl, injectBearer(token, configs)); 110 | }; 111 | -------------------------------------------------------------------------------- /src/apis/token-management/tokenManagement.ts: -------------------------------------------------------------------------------- 1 | export const parseJwt = (token: any) => { 2 | try { 3 | return JSON.parse(atob(token.split('.')[1])); 4 | } catch (e) { 5 | return null; 6 | } 7 | }; 8 | 9 | class EventEmitter { 10 | events: any; 11 | constructor() { 12 | this.events = {}; 13 | } 14 | 15 | _getEventListByName(eventName: string) { 16 | if (typeof this.events[eventName] === 'undefined') { 17 | this.events[eventName] = new Set(); 18 | } 19 | return this.events[eventName]; 20 | } 21 | 22 | on(eventName: string, fn: (...args: any[]) => void) { 23 | this._getEventListByName(eventName).add(fn); 24 | } 25 | 26 | once(eventName: string, fn: (...args: any[]) => void) { 27 | const onceFn = (...args: any[]) => { 28 | this.removeListener(eventName, onceFn); 29 | fn.apply(this, args); 30 | }; 31 | this.on(eventName, onceFn); 32 | } 33 | 34 | emit(eventName: string, ...args: any[]) { 35 | this._getEventListByName(eventName).forEach((fn: (...args: any[]) => void) => { 36 | fn.apply(this, args); 37 | }); 38 | } 39 | 40 | removeListener(eventName: string, fn: (...args: any[]) => void) { 41 | this._getEventListByName(eventName).delete(fn); 42 | } 43 | } 44 | 45 | export default class TokenManagement { 46 | event: any = null; 47 | 48 | isRefreshing: boolean = false; 49 | refreshTimeout: number = 3000; 50 | 51 | constructor({ 52 | isTokenValid, 53 | getAccessToken, 54 | onRefreshToken, 55 | refreshTimeout = 3000, 56 | }: { 57 | isTokenValid: (token: string) => boolean; 58 | getAccessToken: () => string; 59 | onRefreshToken?: (cb: (token: string | null) => void) => void; 60 | refreshTimeout?: number; 61 | }) { 62 | const event = new EventEmitter(); 63 | this.refreshTimeout = refreshTimeout; 64 | 65 | event.on('refresh', () => { 66 | (async () => { 67 | try { 68 | const token: string = await getAccessToken(); 69 | if (isTokenValid(token)) { 70 | event.emit('refreshDone', token); 71 | } else { 72 | event.emit('refreshing'); 73 | } 74 | } catch (e) {} 75 | })(); 76 | }); 77 | 78 | event.on('refreshing', () => { 79 | if (this.isRefreshing) { 80 | return; 81 | } 82 | 83 | // fetch 84 | this.isRefreshing = true; 85 | 86 | const evtFire = false; 87 | onRefreshToken?.((newToken: any) => { 88 | this.event.emit('refreshDone', newToken); 89 | this.isRefreshing = false; 90 | }); 91 | 92 | if (this.refreshTimeout) { 93 | setTimeout(() => { 94 | if (!evtFire) { 95 | this.event.emit('refreshDone', null); 96 | this.isRefreshing = false; 97 | } 98 | }, this.refreshTimeout); 99 | } 100 | }); 101 | 102 | this.event = event; 103 | } 104 | 105 | getToken() { 106 | return new Promise((resolve) => { 107 | let isCalled = false; 108 | 109 | const refreshDoneHandler = (token: string) => { 110 | resolve(token); 111 | isCalled = true; 112 | }; 113 | 114 | this.event.once('refreshDone', refreshDoneHandler); 115 | 116 | if (!isCalled) { 117 | this.event.emit('refresh'); 118 | } 119 | }); 120 | } 121 | 122 | inject(service: (token: string, params: any) => any) { 123 | return async (...args: any) => { 124 | const token = await this.getToken(); 125 | //@ts-ignore 126 | const response = await service(token, ...args); 127 | 128 | return response; 129 | }; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/apis/umirequest/request.ts: -------------------------------------------------------------------------------- 1 | import { extend } from 'umi-request'; 2 | 3 | export const umiRequestInstant = extend({ 4 | prefix: process.env.VITE_APP_API, 5 | headers: { 6 | 'Content-Type': 'application/json', 7 | }, 8 | errorHandler: (error: any) => { 9 | throw error?.data || error?.response; 10 | }, 11 | }); 12 | 13 | let isRefreshing = false; 14 | const refreshSubscribers: any[] = []; 15 | function subscribeTokenRefresh(cb: any) { 16 | refreshSubscribers.push(cb); 17 | } 18 | 19 | function onRefreshed(token: any) { 20 | refreshSubscribers.forEach((cb) => cb(token)); 21 | } 22 | 23 | const onRefreshToken = async () => { 24 | let refreshToken = localStorage.getItem('refreshToken'); 25 | 26 | return umiRequestInstant.post('/auth/refresh-token', { 27 | data: { 28 | refreshToken, 29 | }, 30 | }); 31 | }; 32 | 33 | umiRequestInstant.interceptors.request.use((url, options) => { 34 | const accessToken = localStorage.getItem('accessToken'); 35 | 36 | return { 37 | url, 38 | options: { 39 | ...options, 40 | headers: { 41 | Authorization: `Bearer ${accessToken}`, 42 | Accept: 'application/json', 43 | }, 44 | }, 45 | }; 46 | }); 47 | 48 | umiRequestInstant.interceptors.response.use(async (response, options) => { 49 | if (response?.status === 403) { 50 | if (!isRefreshing) { 51 | isRefreshing = true; 52 | onRefreshToken().then((data: any) => { 53 | isRefreshing = false; 54 | if (data?.accessToken) { 55 | localStorage.setItem('accessToken', data?.accessToken); 56 | localStorage.setItem('refreshToken', data?.refreshToken); 57 | onRefreshed(data?.accessToken); 58 | } 59 | }); 60 | } 61 | 62 | return new Promise((resolve) => { 63 | subscribeTokenRefresh(async (token: string) => { 64 | resolve(umiRequestInstant(options.url, { ...options, Authorization: `Bearer ${token}` })); 65 | }); 66 | }); 67 | } 68 | 69 | return response; 70 | }); 71 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/RefreshByInterceptor/RefreshByInterceptor.tsx: -------------------------------------------------------------------------------- 1 | import { axiosInstant } from '@/apis/axios/request'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | const Post1 = () => { 5 | useEffect(() => { 6 | const token = localStorage.getItem('accessToken'); 7 | 8 | token && axiosInstant.get('/posts'); 9 | }, []); 10 | 11 | return

Component 1

; 12 | }; 13 | 14 | const Post2 = () => { 15 | useEffect(() => { 16 | const token = localStorage.getItem('accessToken'); 17 | 18 | token && axiosInstant.get('/posts?page=2'); 19 | }, []); 20 | 21 | return

Component 2

; 22 | }; 23 | 24 | function RefreshByInterceptor() { 25 | const [login, setLogin] = useState(false); 26 | 27 | useEffect(() => { 28 | const token = localStorage.getItem('accessToken'); 29 | 30 | token && axiosInstant.get('/posts?page=1'); 31 | }, []); 32 | 33 | useEffect(() => { 34 | setLogin(!!localStorage.getItem('accessToken')); 35 | }, []); 36 | 37 | const onLogin = async () => { 38 | const r = await fetch(`${process.env.VITE_APP_API}/auth/login`, { 39 | method: 'post', 40 | body: JSON.stringify({ 41 | username: 'adminRefresh2', 42 | }), 43 | headers: { 44 | 'Content-Type': 'application/json', 45 | }, 46 | }).then((r) => r.json()); 47 | 48 | if (r?.accessToken) { 49 | localStorage.setItem('accessToken', r?.accessToken); 50 | localStorage.setItem('refreshToken', r?.refreshToken); 51 | setLogin(true); 52 | } 53 | }; 54 | 55 | return ( 56 |
57 | 58 |
59 | 60 |
61 | 62 |
63 | {login ? ( 64 | <> 65 | 74 |

Token will expire after 1m. Please check network

75 | 76 | ) : ( 77 | 78 | )} 79 |
80 |
81 | ); 82 | } 83 | 84 | export default RefreshByInterceptor; 85 | -------------------------------------------------------------------------------- /src/components/RefreshByTokenManager/RefreshByTokenManager.tsx: -------------------------------------------------------------------------------- 1 | import { privateRequestNew, requestNew } from '@/apis/brainless-token-management/request'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | const Post1 = () => { 5 | useEffect(() => { 6 | const token = localStorage.getItem('accessToken'); 7 | 8 | if (token) { 9 | privateRequestNew(requestNew.get, '/posts'); 10 | } 11 | }, []); 12 | 13 | return

Component 1

; 14 | }; 15 | 16 | const Post2 = () => { 17 | useEffect(() => { 18 | const token = localStorage.getItem('accessToken'); 19 | 20 | token && privateRequestNew(requestNew.get, '/posts?page=2'); 21 | }, []); 22 | 23 | return

Component 2

; 24 | }; 25 | 26 | function RefreshByTokenManager() { 27 | const [login, setLogin] = useState(false); 28 | 29 | useEffect(() => { 30 | setLogin(!!localStorage.getItem('accessToken')); 31 | }, []); 32 | 33 | useEffect(() => { 34 | const token = localStorage.getItem('accessToken'); 35 | 36 | console.log({ 37 | token, 38 | }); 39 | 40 | token && privateRequestNew(requestNew.get, '/posts?page=1'); 41 | }, []); 42 | 43 | const onLogin = async () => { 44 | const r = await fetch(`${process.env.VITE_APP_API}/auth/login`, { 45 | method: 'post', 46 | body: JSON.stringify({ 47 | username: 'adminRefresh2', 48 | }), 49 | headers: { 50 | 'Content-Type': 'application/json', 51 | }, 52 | }).then((r) => r.json()); 53 | 54 | if (r?.accessToken) { 55 | localStorage.setItem('accessToken', r?.accessToken); 56 | localStorage.setItem('refreshToken', r?.refreshToken); 57 | setLogin(true); 58 | } 59 | }; 60 | 61 | return ( 62 |
63 | 64 |
65 | 66 |
67 | 68 |
69 | {login ? ( 70 | <> 71 | 80 |

Token will expire after 1m. Please check network

81 | 82 | ) : ( 83 | 84 | )} 85 |
86 |
87 | ); 88 | } 89 | 90 | export default RefreshByTokenManager; 91 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import './index.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(); 7 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /swagger-typescript-api.config.ts: -------------------------------------------------------------------------------- 1 | import { defaultConfig } from 'swagger-typescript-api-es'; 2 | 3 | export default defaultConfig({ 4 | name: 'api-axios.ts', 5 | output: './src/apis/axios-gentype', 6 | url: 'https://nestjs-vercel-197.vercel.app/backend-json', 7 | httpClientType: 'axios', 8 | }); 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": ".", 19 | "paths": { 20 | "@/*": ["./src/*"] 21 | } 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import EnvironmentPlugin from 'vite-plugin-environment'; 4 | import checker from 'vite-plugin-checker'; 5 | //@ts-ignore 6 | import * as path from 'path'; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [ 11 | react(), 12 | EnvironmentPlugin('all'), 13 | // resolve({ "react-codemirror2": ` 14 | // const UnControlled = {}; 15 | // export { 16 | // UnControlled, 17 | // }` 18 | // } 19 | checker({ 20 | typescript: true, 21 | }), 22 | ], 23 | optimizeDeps: { 24 | include: ['react'], 25 | }, 26 | css: { 27 | devSourcemap: true, 28 | }, 29 | build: { 30 | commonjsOptions: { 31 | include: [/node_modules/], 32 | }, 33 | // sourcemap: true // Check analyze 34 | }, 35 | resolve: { 36 | //@ts-ignore 37 | alias: [{ find: '@', replacement: path.resolve(__dirname, 'src') }], 38 | }, 39 | esbuild: { 40 | sourcemap: true, 41 | }, 42 | // server: { 43 | // port: 5001, 44 | // }, 45 | // preview: { 46 | // port: 5001, 47 | // }, 48 | }); 49 | --------------------------------------------------------------------------------