Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 8 additions & 163 deletions src/lib/http/client.ts
Original file line number Diff line number Diff line change
@@ -1,170 +1,15 @@
import type {
AxiosError,
AxiosRequestConfig,
AxiosResponse,
ResponseType,
} from "axios";
import axios from "axios";

export type HttpMethod = "GET" | "PUT" | "PATCH" | "POST" | "DELETE";

export interface RequestConfig<TData = unknown> {
url: string;
method: HttpMethod;
params?: unknown;
data?: TData;
responseType?: ResponseType;
signal?: AbortSignal;
headers?: AxiosRequestConfig["headers"];
baseURL?: string;
}

export interface ResponseConfig<TData = unknown> {
data: TData;
status: number;
statusText: string;
headers?: AxiosResponse["headers"];
}

export type ResponseErrorConfig<TError = unknown> = AxiosError<TError>;
BaseFetch,
RequestConfig,
ResponseErrorConfig,
} from "@/shared/fetch/BaseFetch";
import { AxiosFetchStrategy } from "@/shared/fetch/AxiosFetch";

const defaultBaseURL =
(import.meta as any).env?.VITE_API_BASE_URL || "/backend/api/web";

export const axiosInstance = axios.create({
baseURL: defaultBaseURL,
headers: {
"Content-Type": "application/json",
},
paramsSerializer: {
serialize(params) {
const search = new URLSearchParams();
if (params && typeof params === "object") {
for (const [key, value] of Object.entries(params)) {
if (value == null) continue;
if (Array.isArray(value)) {
for (const v of value) {
if (v != null) search.append(key, String(v));
}
} else {
search.append(key, String(value));
}
}
}
return search.toString();
},
},
});

// TODO: AUTHORIZATION LOGIC
// axiosInstance.interceptors.request.use((config) => {
// const accessToken = getAccessToken();

// if (accessToken) {
// config.headers.Authorization = `Bearer ${accessToken}`;
// }

// return config;
// });

// let isRefreshing = false;
// let refreshSubscribers: ((token: string) => void)[] = [];

// const subscribeTokenRefresh = (cb: (token: string) => void) => {
// refreshSubscribers.push(cb);
// };

// const onRefreshed = (token: string) => {
// refreshSubscribers.forEach((cb) => cb(token));
// refreshSubscribers = [];
// };

// axiosInstance.interceptors.response.use(
// (response) => response,
// async (error: AxiosError) => {
// const originalRequest = error.config as AxiosRequestConfig & {
// _retry?: boolean;
// };

// if (error.response?.status === 401 && !originalRequest._retry) {
// originalRequest._retry = true;

// if (!isRefreshing) {
// isRefreshing = true;
// try {
// const accessToken = await refreshAccessToken();
// axiosInstance.defaults.headers.Authorization = `Bearer ${accessToken}`;

// const result = await axiosInstance(originalRequest);
// onRefreshed(accessToken);

// isRefreshing = false;
// return result;
// } catch (refreshError) {
// isRefreshing = false;
// router.navigate({ to: "/signin" });
// return Promise.reject(refreshError);
// }
// }

// return new Promise((resolve) => {
// subscribeTokenRefresh(async (token: string) => {
// if (originalRequest.headers) {
// originalRequest.headers.Authorization = `Bearer ${token}`;
// }

// resolve(axiosInstance(originalRequest));
// });
// });
// }

// return Promise.reject(error);
// },
// );

function normalizeParams(params: unknown): unknown {
if (
params &&
typeof params === "object" &&
"params" in (params as any) &&
Object.keys(params as any).length === 1
) {
return (params as any).params;
}
return params;
}
export const client: BaseFetch = new AxiosFetchStrategy(defaultBaseURL);

export default async function client<
TData,
TError = unknown,
TVariables = unknown,
>(
config: RequestConfig<TVariables | FormData>,
): Promise<ResponseConfig<TData>> {
try {
const response = await axiosInstance.request<
TData,
AxiosResponse<TData>,
TVariables
>({
method: config.method,
url: config.url,
params: normalizeParams(config.params) as any,
data: (config as unknown as RequestConfig).data as any,
responseType: config.responseType,
signal: config.signal,
headers: config.headers,
baseURL: config.baseURL ?? axiosInstance.defaults.baseURL,
});
export default client.request.bind(client);

return {
data: response.data,
status: response.status,
statusText: response.statusText,
headers: response.headers,
};
} catch (error) {
// eslint-disable-next-line
throw error as ResponseErrorConfig<TError>;
}
}
export type { RequestConfig, ResponseErrorConfig };
4 changes: 4 additions & 0 deletions src/lib/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { BaseStorage } from "@/shared/storage/BaseStorage";
import { LocalStorage } from "@/shared/storage/LocalStorage";

export const storage: BaseStorage = new LocalStorage();
2 changes: 2 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { ClassValue } from "clsx";
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export type AlwaysNullable<T> = Exclude<T, null> | null;

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Expand Down
11 changes: 7 additions & 4 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import ReactDOM from "react-dom/client";

import { AuthProvider } from "@/shared/auth/AuthContext";

// Import the generated route tree
import { routeTree } from "./routeTree.gen";

import "./styles.css";

// Create a new router instance
Expand Down Expand Up @@ -38,8 +39,10 @@ const rootElement = document.getElementById("app");
if (rootElement && !rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<QueryClientProvider client={queryClient}>
<InnerApp />
</QueryClientProvider>,
<AuthProvider>
<QueryClientProvider client={queryClient}>
<InnerApp />
</QueryClientProvider>
</AuthProvider>,
);
}
122 changes: 122 additions & 0 deletions src/shared/auth/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { Dispatch, FC, JSX, SetStateAction } from "react";
import { createContext, use, useState } from "react";
import { storage } from "@/lib/storage";

interface IAuthContext {
isAuth: boolean;
setIsAuth: Dispatch<SetStateAction<boolean>>;
}

const AuthContext = createContext<IAuthContext>({
isAuth: false,
setIsAuth: (_) => {},
});

interface AuthProviderProps {
children: JSX.Element | JSX.Element[];
}

enum TOKENS_KEYS {
ACCESS = "ACCESS_TOKEN",
REFRESH = "REFRESH_TOKEN",
}

const setAccessToken = (token: string) =>
storage.set(TOKENS_KEYS.ACCESS, token);
const setRefreshToken = (token: string) =>
storage.set(TOKENS_KEYS.REFRESH, token);

const deleteAccessToken = () => storage.delete(TOKENS_KEYS.ACCESS);
const deleteRefreshToken = () => storage.delete(TOKENS_KEYS.REFRESH);

export const getAccessToken = () => storage.get<string>(TOKENS_KEYS.ACCESS);
export const getRefreshToken = () => storage.get<string>(TOKENS_KEYS.REFRESH);

export const AuthProvider: FC<AuthProviderProps> = ({ children }) => {
const [isAuth, setIsAuth] = useState(false);

return (
<AuthContext
value={{
isAuth,
setIsAuth,
}}
>
{children}
</AuthContext>
);
};

export const refreshAccessToken = async () => {
try {
const access_token = "";

return access_token as string;
} catch (e) {
deleteAccessToken();
deleteRefreshToken();
console.log(e);
throw e;
}
};

export const useAuth = () => {
const ctx = use(AuthContext);

if (!ctx) {
throw new Error("useAuth must be used within <AuthProvider>");
}

const { isAuth, setIsAuth } = ctx;

const me = async () => {
try {
const token = getAccessToken();

if (!token) return false;

// TODO: add fetch /me

setIsAuth(true);

return true;
} catch (e) {
console.error(e);

setIsAuth(false);

return false;
}
};

const signin = async (data: unknown) => {
try {
// TODO: add create token fetch

const access_token = "";
const refresh_token = "";

setAccessToken(access_token ?? "");
setRefreshToken(refresh_token ?? "");

setIsAuth(true);

// TODO: add return response data
return data;
} catch (e) {
deleteAccessToken();
deleteRefreshToken();

throw e;
}
};

const signup = async () => {};

return {
isAuth,
me,
signin,
signup,
};
};
Loading