├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------