feat: login token state

This commit is contained in:
DarkSky
2022-12-22 18:22:38 +08:00
parent cf95f0abaf
commit 8e17d56a51
6 changed files with 104 additions and 158 deletions
+2 -7
View File
@@ -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) => {
+7 -17
View File
@@ -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 };
+93 -71
View File
@@ -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<LoginResponse> =>
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<LoginResponse>;
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<typeof login> | 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();
+2 -24
View File
@@ -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<LoginResponse>(
[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(
-32
View File
@@ -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<LoginResponse> {
return request
.post('/api/user/token', { json: params, headers: { token: undefined } })
.json<LoginResponse>();
}
export interface GetUserByEmailParams {
email: string;
workspaceId: string;
@@ -156,13 +156,6 @@ export async function downloadWorkspace(
export async function uploadBlob(params: { blob: Blob }): Promise<string> {
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: {