feat: cleanup fetcher for prepare refactor

This commit is contained in:
DarkSky
2022-12-22 15:09:49 +08:00
committed by DarkSky
parent b4b8a25742
commit b666fd8d0a
16 changed files with 121 additions and 161 deletions
@@ -1,4 +1,4 @@
import { signInWithGoogle, login, setToken } from '@pathfinder/data-services';
import { signInWithGoogle } from '@pathfinder/data-services';
import { styled } from '@/styles';
import { Button } from '@/ui/button';
import { useModal } from '@/providers/global-modal-provider';
@@ -10,13 +10,7 @@ export const GoogleLoginButton = () => {
<StyledGoogleButton
onClick={() => {
signInWithGoogle()
.then(async user => {
const idToken = await user.user.getIdToken();
const token = await login({ token: idToken, type: 'Google' });
setToken({
accessToken: token.token,
refreshToken: token.refresh,
});
.then(async => {
triggerLoginModal();
})
.catch(error => {
@@ -94,7 +94,7 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
const editor = createEditorHandler?.(currentPage) || null;
if (editor) {
const pageMeta = currentWorkspace?.meta.pageMetas.find(
const pageMeta = currentWorkspace.meta.pageMetas.find(
p => p.id === currentPage.pageId
);
if (pageMeta?.mode) {
@@ -133,13 +133,13 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
const callback = async (user: AccessTokenMessage | null) => {
const workspacesMeta = user ? await getWorkspaces() : [];
const workspacesList = await Promise.all(
workspacesMeta?.map(async ({ id }) => {
workspacesMeta.map(async ({ id }) => {
const workspace = (await loadWorkspaceHandler?.(id)) || null;
return { id, workspace };
})
);
let workspaces: Record<string, Workspace | null> = {};
const workspaces: Record<string, Workspace | null> = {};
workspacesList.forEach(({ id, workspace }) => {
workspaces[id] = workspace;
+1
View File
@@ -25,6 +25,7 @@
"axios": "^1.2.0",
"encoding": "^0.1.13",
"firebase": "^9.13.0",
"ky": "^0.33.0",
"swr": "^2.0.0"
}
}
+10 -2
View File
@@ -7,6 +7,8 @@ import {
signInWithPopup,
} from 'firebase/auth';
import type { User } from 'firebase/auth';
import { login } from './sdks';
import { setToken } from './request';
/**
* firebaseConfig reference: https://firebase.google.com/docs/web/setup#add_firebase_to_your_app
@@ -32,8 +34,14 @@ const signIn = (email: string, password: string) => {
};
const googleAuthProvider = new GoogleAuthProvider();
export const signInWithGoogle = () => {
return signInWithPopup(firebaseAuth, googleAuthProvider);
export const signInWithGoogle = async () => {
const user = await signInWithPopup(firebaseAuth, googleAuthProvider);
const idToken = await user.user.getIdToken();
const token = await login({ token: idToken, type: 'Google' });
setToken({
accessToken: token.token,
refreshToken: token.refresh,
});
};
export const onAuthStateChanged = (callback: (user: User | null) => void) => {
@@ -1,9 +0,0 @@
export class ServiceError extends Error {
public message: string;
public code: string;
constructor(code: string, message: string) {
super(message);
this.message = message;
this.code = code;
}
}
+49 -11
View File
@@ -1,13 +1,8 @@
import axios from 'axios';
import { setAuthorization } from './request';
import { handleResponseError, handleResponse } from './response';
import { getToken, isAccessTokenExpired, refreshToken } from './token';
declare module 'axios' {
interface AxiosRequestConfig {
/**
* If true, request will send with Authorization header.
*/
withAuthorization?: boolean;
/**
* If true, indicate this request is refreshing token.
*/
@@ -15,9 +10,52 @@ declare module 'axios' {
}
}
export const request = axios.create({
withAuthorization: true,
});
export const request = axios.create();
request.interceptors.request.use(setAuthorization);
request.interceptors.response.use(handleResponse, handleResponseError);
request.interceptors.request.use(async config => {
if (!config.headers) {
config.headers = {};
}
const token = getToken();
if (token) {
if (isAccessTokenExpired(token.accessToken)) {
await refreshToken();
}
if (!config.headers) {
config.headers = {};
}
config.headers['Authorization'] = token.accessToken;
}
return config;
});
request.interceptors.response.use(
response => {
const { status, data } = response;
if (status === 200) {
if (data.error) {
// TODO - common error handling
throw new Error(data.error.message, { cause: data.error.code });
}
return response;
}
return response;
},
async error => {
const { response, config, message } = error;
const status = response?.status;
if (status === 401) {
if (!config) {
throw new Error(message, { cause: status.toString() });
}
if (config.refreshTokenRequest) {
throw new Error(message, { cause: status.toString() });
}
await refreshToken();
return request({ ...config, refreshTokenRequest: true });
}
return response;
}
);
@@ -1,4 +1,4 @@
import type { AccessTokenMessage } from './types';
import { AccessTokenMessage } from './token';
type Callback = (user: AccessTokenMessage | null) => void;
+2 -3
View File
@@ -1,4 +1,3 @@
export { request } from './axios';
export { ServiceError } from './ServiceError';
export { setToken, getToken, authorizationEvent } from './request';
export type { AccessTokenMessage } from './request';
export type { AccessTokenMessage } from './token';
export { authorizationEvent, getToken, setToken } from './token';
@@ -1,2 +0,0 @@
export * from './authorization';
export type { AccessTokenMessage } from './types';
@@ -1,8 +0,0 @@
export interface AccessTokenMessage {
create_at: number;
exp: number;
email: string;
id: string;
name: string;
avatar_url: string;
}
@@ -1,7 +0,0 @@
export {
setAuthorization,
setToken,
getToken,
authorizationEvent,
} from './authorization';
export type { AccessTokenMessage } from './authorization';
@@ -1,34 +0,0 @@
import type { AxiosResponse, AxiosError } from 'axios';
import { ServiceError } from '../ServiceError';
import { refreshToken } from '../request/authorization';
import { request } from '../axios';
export async function handleResponseError(error: AxiosError<any, any>) {
const { response, config, message } = error;
const status = response?.status;
if (status === 401) {
if (!config) {
throw new ServiceError(message, status.toString());
}
if (config.refreshTokenRequest) {
throw new ServiceError(message, status.toString());
}
await refreshToken();
return request({ ...config, refreshTokenRequest: true });
}
return response;
}
export async function handleResponse(response: AxiosResponse<any, any>) {
const { status, data } = response;
if (status === 200) {
if (data.error) {
// TODO - common error handling
const error = new ServiceError(data.error.message, data.error.code);
throw error;
}
return response;
}
return response;
}
@@ -1 +0,0 @@
export { handleResponseError, handleResponse } from './handle-error';
@@ -1,44 +1,17 @@
import type { AxiosRequestConfig } from 'axios';
import { login } from '../../../sdks';
import { AuthorizationEvent } from './events';
import type { AccessTokenMessage } from './types';
import { login } from '../sdks';
export interface AccessTokenMessage {
create_at: number;
exp: number;
email: string;
id: string;
name: string;
avatar_url: string;
}
const TOKEN_KEY = 'affine_token';
export const authorizationEvent = new AuthorizationEvent();
authorizationEvent.triggerChange(
parseAccessToken(getToken()?.accessToken || '')
);
interface Token {
accessToken: string;
refreshToken: string;
}
/**
* set null to clear token
*/
export function setToken(token: Token | null): void {
if (token === null) {
window.localStorage.removeItem(TOKEN_KEY);
authorizationEvent.triggerChange(null);
return;
}
window.localStorage.setItem(TOKEN_KEY, JSON.stringify(token));
authorizationEvent.triggerChange(parseAccessToken(token.accessToken));
}
export function getToken(): {
accessToken: string;
refreshToken: string;
} | null {
try {
return JSON.parse(window.localStorage.getItem(TOKEN_KEY) || '');
} catch (error) {
return null;
}
}
function b64DecodeUnicode(str: string) {
// Going backwards: from byte stream, to percent-encoding, to original string.
return decodeURIComponent(
@@ -64,7 +37,7 @@ function parseAccessToken(token: string): AccessTokenMessage | null {
}
}
function isAccessTokenExpired(token: string) {
export function isAccessTokenExpired(token: string) {
const message = parseAccessToken(token);
if (!message) {
return true;
@@ -72,41 +45,39 @@ function isAccessTokenExpired(token: string) {
return Date.now() - message.create_at > message.exp;
}
async function isLoggedIn(): Promise<boolean> {
const token = getToken();
if (!token) {
return false;
export function getToken(): {
accessToken: string;
refreshToken: string;
} | null {
try {
return JSON.parse(window.localStorage.getItem(TOKEN_KEY) || '');
} catch (error) {
return null;
}
return isAccessTokenExpired(token.accessToken);
}
export const authorizationEvent = new AuthorizationEvent();
authorizationEvent.triggerChange(
parseAccessToken(getToken()?.accessToken || '')
);
interface Token {
accessToken: string;
refreshToken: string;
}
export function setToken(token: Token | null): void {
if (token === null) {
window.localStorage.removeItem(TOKEN_KEY);
authorizationEvent.triggerChange(null);
return;
}
window.localStorage.setItem(TOKEN_KEY, JSON.stringify(token));
authorizationEvent.triggerChange(parseAccessToken(token.accessToken));
}
let refreshingToken: ReturnType<typeof login> | undefined;
export async function setAuthorization(config: AxiosRequestConfig<unknown>) {
if (!config.headers) {
config.headers = {};
}
if (config.withAuthorization) {
let token = getToken();
if (!token) {
throw new Error('No authorization token.');
}
if (isAccessTokenExpired(token.accessToken)) {
await refreshToken();
token = getToken();
}
if (!token) {
throw new Error('No authorization token.');
}
if (!config.headers) {
config.headers = {};
}
config.headers['Authorization'] = token.accessToken;
}
return config;
}
export const refreshToken = async () => {
let token = getToken();
if (!token) {
+1 -1
View File
@@ -32,7 +32,7 @@ export async function login(params: LoginParams): Promise<LoginResponse> {
method: 'POST',
data: params,
withAuthorization: false,
});
} as any);
return data.data;
}
+13 -3
View File
@@ -117,12 +117,14 @@ importers:
axios: ^1.2.0
encoding: ^0.1.13
firebase: ^9.13.0
ky: ^0.33.0
swr: ^2.0.0
typescript: ^4.8.4
dependencies:
axios: 1.2.0
axios: 1.2.1
encoding: 0.1.13
firebase: 9.13.0_encoding@0.1.13
ky: 0.33.0
swr: 2.0.0
devDependencies:
typescript: 4.9.3
@@ -3586,10 +3588,10 @@ packages:
engines: { node: '>=4' }
dev: true
/axios/1.2.0:
/axios/1.2.1:
resolution:
{
integrity: sha512-zT7wZyNYu3N5Bu0wuZ6QccIf93Qk1eV8LOewxgjOZFd2DenOs98cJ7+Y6703d0wkaXGY6/nZd4EweJaHz9uzQw==,
integrity: sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==,
}
dependencies:
follow-redirects: 1.15.2
@@ -6675,6 +6677,14 @@ packages:
engines: { node: '>=14.16' }
dev: false
/ky/0.33.0:
resolution:
{
integrity: sha512-peKzuOlN/q3Q3jOgi4t0cp6DOgif5rVnmiSIsjsmkiOcdnSjkrKSUqQmRWYCTqjUtR9b3xQQr8aj7KwSW1r49A==,
}
engines: { node: '>=14.16' }
dev: false
/language-subtag-registry/0.3.22:
resolution:
{