diff --git a/packages/data-services/src/auth.ts b/packages/data-services/src/auth.ts index c8d9e0a301..8fdd10be18 100644 --- a/packages/data-services/src/auth.ts +++ b/packages/data-services/src/auth.ts @@ -7,8 +7,7 @@ import { signInWithPopup, } from 'firebase/auth'; import type { User } from 'firebase/auth'; -import { login } from './sdks'; -import { setToken } from './request'; +import { token } from './request'; /** * firebaseConfig reference: https://firebase.google.com/docs/web/setup#add_firebase_to_your_app @@ -37,11 +36,7 @@ const googleAuthProvider = new 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, - }); + await token.initToken(idToken); }; export const onAuthStateChanged = (callback: (user: User | null) => void) => { diff --git a/packages/data-services/src/request/index.ts b/packages/data-services/src/request/index.ts index bbd2f2e73d..e7b83ac13e 100644 --- a/packages/data-services/src/request/index.ts +++ b/packages/data-services/src/request/index.ts @@ -1,11 +1,5 @@ import ky from 'ky'; -import { - getToken, - setToken, - isAccessTokenExpired, - refreshToken, - authorizationEvent, -} from './token'; +import { token } from './token'; export const request = ky.extend({ retry: 1, @@ -13,13 +7,9 @@ export const request = ky.extend({ beforeRequest: [ async request => { if (!request.headers.has('token') || request.headers.get('token')) { - const token = getToken(); - if (token) { - if (isAccessTokenExpired(token.accessToken)) { - await refreshToken(); - } - - request.headers.set('Authorization', token.accessToken); + if (token.isLogin) { + if (token.isExpired) await token.refreshToken(); + request.headers.set('Authorization', token.token); } } }, @@ -39,12 +29,12 @@ export const request = ky.extend({ // ], beforeRetry: [ async ({ request }) => { - const token = await refreshToken(); - request.headers.set('Authorization', token.accessToken); + await token.refreshToken(); + request.headers.set('Authorization', token.token); }, ], }, }); export type { AccessTokenMessage } from './token'; -export { authorizationEvent, getToken, setToken }; +export { token }; diff --git a/packages/data-services/src/request/token.ts b/packages/data-services/src/request/token.ts index 057bae13f0..318982738e 100644 --- a/packages/data-services/src/request/token.ts +++ b/packages/data-services/src/request/token.ts @@ -1,5 +1,5 @@ +import { request } from '.'; import { AuthorizationEvent } from './events'; -import { login } from '../sdks'; export interface AccessTokenMessage { create_at: number; @@ -12,6 +12,30 @@ export interface AccessTokenMessage { const TOKEN_KEY = 'affine_token'; +type LoginParams = { + type: 'Google' | 'Refresh'; + token: string; +}; + +type LoginResponse = { + /** + * JWT, expires in a very short time + */ + token: string; + /** + * Refresh token + */ + refresh: string; +}; + +const login = (params: LoginParams): Promise => + request + .post('/api/user/token', { + json: params, + headers: { token: undefined }, + }) + .json(); + function b64DecodeUnicode(str: string) { // Going backwards: from byte stream, to percent-encoding, to original string. return decodeURIComponent( @@ -25,76 +49,74 @@ function b64DecodeUnicode(str: string) { ); } -function parseAccessToken(token: string): AccessTokenMessage | null { - try { - const message: AccessTokenMessage = JSON.parse( - b64DecodeUnicode(token.split('.')[1]) - ); - message.id = message.id.toString(); - return message; - } catch (error) { - return null; +class Token { + private readonly _event: AuthorizationEvent; + private _accessToken: string; + private _refreshToken: string; + + private _user: AccessTokenMessage | null; + private _padding?: Promise; + + constructor(refreshToken?: string) { + this._accessToken = ''; + this._refreshToken = refreshToken || localStorage.getItem(TOKEN_KEY) || ''; + this._event = new AuthorizationEvent(); + + this._user = Token.parse(this._accessToken); + this._event.triggerChange(this._user); + } + + private _setToken(login: LoginResponse) { + this._accessToken = login.token; + this._refreshToken = login.refresh; + this._user = Token.parse(login.token); + + window.localStorage.setItem(TOKEN_KEY, login.refresh); + } + + async initToken(token: string) { + this._setToken(await login({ token, type: 'Google' })); + } + + async refreshToken() { + if (!this._refreshToken) { + throw new Error('No authorization token.'); + } + if (!this._padding) { + this._padding = login({ + type: 'Refresh', + token: this._refreshToken, + }); + } + this._setToken(await this._padding); + this._padding = undefined; + + this._event.triggerChange(this._user); + } + + get token() { + return this._accessToken; + } + + get isLogin() { + return !!this._refreshToken; + } + + get isExpired() { + if (!this._user) return true; + return Date.now() - this._user.create_at > this._user.exp; + } + + static parse(token: string): AccessTokenMessage | null { + try { + const message: AccessTokenMessage = JSON.parse( + b64DecodeUnicode(token.split('.')[1]) + ); + return message; + } catch (error) { + return null; + } } } -export function isAccessTokenExpired(token: string) { - const message = parseAccessToken(token); - if (!message) { - return true; - } - return Date.now() - message.create_at > message.exp; -} - -export function getToken(): { - accessToken: string; - refreshToken: string; -} | null { - try { - return JSON.parse(window.localStorage.getItem(TOKEN_KEY) || ''); - } catch (error) { - return null; - } -} - -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 | undefined; - -export const refreshToken = async () => { - let token = getToken(); - if (!token) { - throw new Error('No authorization token.'); - } - if (!refreshingToken) { - refreshingToken = login({ - type: 'Refresh', - token: token.refreshToken, - }); - } - const newToken = await refreshingToken; - token = { - accessToken: newToken.token, - refreshToken: newToken.refresh, - }; - setToken(token); - refreshingToken = undefined; - return token; -}; +export const token = new Token(); diff --git a/packages/data-services/src/sdks/user.hook.ts b/packages/data-services/src/sdks/user.hook.ts index e6997c4765..86792a4f79 100644 --- a/packages/data-services/src/sdks/user.hook.ts +++ b/packages/data-services/src/sdks/user.hook.ts @@ -1,29 +1,7 @@ import useSWR from 'swr'; import type { SWRConfiguration } from 'swr'; -import { login, getUserByEmail } from './user'; -import type { - LoginParams, - LoginResponse, - GetUserByEmailParams, - User, -} from './user'; - -export const LOGIN_SWR_KEY = 'user.token'; -export function useLogin(params: LoginParams, config?: SWRConfiguration) { - const { data, error, isValidating, isLoading, mutate } = - useSWR( - [LOGIN_SWR_KEY, params], - ([_, params]) => login(params), - config - ); - - return { - loading: isLoading, - data, - error, - mutate, - }; -} +import { getUserByEmail } from './user'; +import type { GetUserByEmailParams, User } from './user'; export const GET_USER_BY_EMAIL_SWR_TOKEN = 'user.getUserByEmail'; export function useGetUserByEmail( diff --git a/packages/data-services/src/sdks/user.ts b/packages/data-services/src/sdks/user.ts index 1b319e0385..c873b44042 100644 --- a/packages/data-services/src/sdks/user.ts +++ b/packages/data-services/src/sdks/user.ts @@ -1,37 +1,5 @@ import { request } from '../request'; -export interface ExchangeToken { - type: 'Google'; - /** - * Token from firebase. - */ - token: string; -} - -export interface RefreshToken { - type: 'Refresh'; - token: string; -} - -export type LoginParams = ExchangeToken | RefreshToken; - -export interface LoginResponse { - /** - * JWT, expires in a very short time - */ - token: string; - /** - * Refresh token - */ - refresh: string; -} - -export async function login(params: LoginParams): Promise { - return request - .post('/api/user/token', { json: params, headers: { token: undefined } }) - .json(); -} - export interface GetUserByEmailParams { email: string; workspaceId: string; diff --git a/packages/data-services/src/sdks/workspace.ts b/packages/data-services/src/sdks/workspace.ts index 421f278fe7..1e6a1b823d 100644 --- a/packages/data-services/src/sdks/workspace.ts +++ b/packages/data-services/src/sdks/workspace.ts @@ -156,13 +156,6 @@ export async function downloadWorkspace( export async function uploadBlob(params: { blob: Blob }): Promise { return request.post('/api/blob', { body: params.blob }).text(); - - // const data = await request({ - // url: '/api/blob', - // method: 'PUT', - // data: params.blob, - // }); - // return data.data; } export async function getBlob(params: {