mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-16 17:46:18 +08:00
feat: login token state
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user