refactor(client): split client to cms/mobile/party
Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
44
client/cms/src/lib/axios.ts
Normal file
44
client/cms/src/lib/axios.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import { router } from '@/lib/router';
|
||||
import { doRefreshToken, getRefreshToken, getToken, setRefreshToken, setToken } from './token';
|
||||
|
||||
export const axiosClient = axios.create({
|
||||
baseURL: '/api/v1/',
|
||||
});
|
||||
|
||||
axiosClient.interceptors.request.use((config) => {
|
||||
const token = getToken();
|
||||
if (token !== null) {
|
||||
config.headers = config.headers ?? {};
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
type RetryConfig = AxiosRequestConfig & { _retry?: boolean };
|
||||
|
||||
axiosClient.interceptors.response.use(undefined, async (error: AxiosError) => {
|
||||
const originalRequest = error.config as RetryConfig | undefined;
|
||||
if (!error.response || error.response.status !== 401 || !originalRequest) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (error.response.status === 401 && getRefreshToken() !== null) {
|
||||
try {
|
||||
const maybeRefreshTokenValue = await doRefreshToken();
|
||||
const { access_token, refresh_token } = maybeRefreshTokenValue.data;
|
||||
originalRequest.headers = originalRequest.headers ?? {};
|
||||
originalRequest.headers.Authorization = `Bearer ${access_token}`;
|
||||
setToken(access_token);
|
||||
setRefreshToken(refresh_token);
|
||||
return await axiosClient(originalRequest);
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof AxiosError && e.status === 401) {
|
||||
await router.navigate({ to: '/authorize' });
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
21
client/cms/src/lib/navData.ts
Normal file
21
client/cms/src/lib/navData.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
IconDashboard,
|
||||
IconUser,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
export const navData = {
|
||||
navMain: [
|
||||
{
|
||||
title: '工作台',
|
||||
url: '/',
|
||||
icon: IconDashboard,
|
||||
},
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: '个人资料',
|
||||
url: '/profile',
|
||||
icon: IconUser,
|
||||
},
|
||||
],
|
||||
};
|
||||
14
client/cms/src/lib/random.ts
Normal file
14
client/cms/src/lib/random.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Generate a cryptographically secure OAuth2 state string
|
||||
* base64url encoded, URL-safe
|
||||
*/
|
||||
export function generateOAuthState(bytes: number = 32): string {
|
||||
const random = new Uint8Array(bytes);
|
||||
crypto.getRandomValues(random);
|
||||
|
||||
// base64url encode
|
||||
return btoa(String.fromCharCode(...random))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '');
|
||||
}
|
||||
13
client/cms/src/lib/router.ts
Normal file
13
client/cms/src/lib/router.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createRouter } from '@tanstack/react-router';
|
||||
// Import the generated route tree
|
||||
import { routeTree } from '../routeTree.gen';
|
||||
|
||||
// Create a new router instance
|
||||
export const router = createRouter({ routeTree });
|
||||
|
||||
// Register the router instance for type safety
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
40
client/cms/src/lib/token.ts
Normal file
40
client/cms/src/lib/token.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export function setToken(token: string) {
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
|
||||
export function getToken() {
|
||||
return localStorage.getItem('token');
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
|
||||
export function hasToken() {
|
||||
return getToken() !== null;
|
||||
}
|
||||
|
||||
export function setRefreshToken(refreshToken: string) {
|
||||
localStorage.setItem('refreshToken', refreshToken);
|
||||
}
|
||||
|
||||
export function getRefreshToken() {
|
||||
return localStorage.getItem('refreshToken');
|
||||
}
|
||||
|
||||
export function clearTokens() {
|
||||
removeToken();
|
||||
setRefreshToken('');
|
||||
}
|
||||
|
||||
export async function doSetTokenByCode(code: string) {
|
||||
const { data } = await axios.post<{ access_token: string; refresh_token: string }>('/api/v1/auth/token', { code });
|
||||
setToken(data.access_token);
|
||||
setRefreshToken(data.refresh_token);
|
||||
}
|
||||
|
||||
export async function doRefreshToken() {
|
||||
return axios.post<{ access_token: string; refresh_token: string }>('/api/v1/auth/refresh', { refresh_token: getRefreshToken() });
|
||||
}
|
||||
19
client/cms/src/lib/utils.ts
Normal file
19
client/cms/src/lib/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { ClassValue } from 'clsx';
|
||||
// eslint-disable-next-line unicorn/prefer-node-protocol
|
||||
import { Buffer } from 'buffer';
|
||||
import { clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function base64ToUtf8(base64: string): string {
|
||||
return new TextDecoder('utf-8').decode(
|
||||
Uint8Array.from(Buffer.from(base64, 'base64')),
|
||||
);
|
||||
}
|
||||
|
||||
export function utf8ToBase64(utf8: string): string {
|
||||
return Buffer.from(utf8, 'utf-8').toString('base64');
|
||||
}
|
||||
Reference in New Issue
Block a user