refactor: login method (#1676)

This commit is contained in:
Himself65
2023-03-23 16:29:29 -05:00
committed by GitHub
parent a415e4aa5c
commit 56acb2bdeb
22 changed files with 626 additions and 61 deletions

View File

@@ -0,0 +1,86 @@
/**
* @vitest-environment happy-dom
*/
import 'fake-indexeddb/auto';
import userA from '@affine-test/fixtures/userA.json';
import { Workspace } from '@blocksuite/store';
import { beforeAll, beforeEach, describe, expect, test } from 'vitest';
import { createWorkspaceApis, createWorkspaceResponseSchema } from '../api';
import { loginResponseSchema, setLoginStorage } from '../login';
let workspaceApis: ReturnType<typeof createWorkspaceApis>;
beforeAll(() => {
workspaceApis = createWorkspaceApis('http://localhost:3000/');
});
beforeEach(async () => {
let data;
// first step: try to log in
const response = await fetch('http://localhost:3000/api/user/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'DebugLoginUser',
email: userA.email,
password: userA.password,
}),
});
if (!response.ok) {
data = await fetch('http://localhost:3000/api/user/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'DebugCreateUser',
...userA,
}),
}).then(r => r.json());
setLoginStorage(data);
} else {
setLoginStorage((data = await response.json()));
}
loginResponseSchema.parse(data);
});
describe('api', () => {
test(
'create workspace',
async () => {
const workspace = new Workspace({
id: 'test',
});
const binary = Workspace.Y.encodeStateAsUpdate(workspace.doc);
const data = await workspaceApis.createWorkspace(new Blob([binary]));
createWorkspaceResponseSchema.parse(data);
},
{
timeout: 30000,
}
);
test(
'delete workspace',
async () => {
const workspace = new Workspace({
id: 'test',
});
const binary = Workspace.Y.encodeStateAsUpdate(workspace.doc);
const data = await workspaceApis.createWorkspace(new Blob([binary]));
createWorkspaceResponseSchema.parse(data);
const id = data.id;
const response = await workspaceApis.deleteWorkspace({
id,
});
expect(response).toBe(true);
},
{
timeout: 30000,
}
);
});

View File

@@ -0,0 +1,293 @@
import { assertExists } from '@blocksuite/global/utils';
import { z } from 'zod';
import { getLoginStorage } from '../login';
export interface User {
id: string;
name: string;
email: string;
avatar_url: string;
create_at: string;
}
export interface GetUserByEmailParams {
email: string;
workspace_id: string;
}
export function createUserApis(prefixUrl = '/') {
return {
getUserByEmail: async (
params: GetUserByEmailParams
): Promise<User[] | null> => {
const auth = getLoginStorage();
assertExists(auth);
const target = new URL(prefixUrl + 'api/user', window.location.origin);
target.searchParams.append('email', params.email);
target.searchParams.append('workspace_id', params.workspace_id);
return fetch(target, {
method: 'GET',
headers: {
Authorization: auth.token,
},
}).then(r => r.json());
},
} as const;
}
export interface GetWorkspaceDetailParams {
id: string;
}
export enum WorkspaceType {
Private = 0,
Normal = 1,
}
export enum PermissionType {
Read = 0,
Write = 1,
Admin = 10,
Owner = 99,
}
export interface Workspace {
id: string;
type: WorkspaceType;
public: boolean;
permission: PermissionType;
}
export interface WorkspaceDetail extends Workspace {
owner: User;
member_count: number;
}
export interface Permission {
id: string;
type: PermissionType;
workspace_id: string;
user_id: string;
user_email: string;
accepted: boolean;
create_at: number;
}
export interface RegisteredUser extends User {
type: 'Registered';
}
export interface UnregisteredUser {
type: 'Unregistered';
email: string;
}
export interface Member extends Permission {
user: RegisteredUser | UnregisteredUser;
}
export interface GetWorkspaceMembersParams {
id: string;
}
export interface CreateWorkspaceParams {
name: string;
}
export interface UpdateWorkspaceParams {
id: string;
public: boolean;
}
export interface DeleteWorkspaceParams {
id: string;
}
export interface InviteMemberParams {
id: string;
email: string;
}
export interface RemoveMemberParams {
permissionId: number;
}
export interface AcceptInvitingParams {
invitingCode: string;
}
export interface LeaveWorkspaceParams {
id: number | string;
}
export const createWorkspaceResponseSchema = z.object({
id: z.string(),
public: z.boolean(),
type: z.nativeEnum(WorkspaceType),
created_at: z.number(),
});
export function createWorkspaceApis(prefixUrl = '/') {
return {
getWorkspaces: async (): Promise<Workspace[]> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + 'api/workspace', {
method: 'GET',
headers: {
Authorization: auth.token,
'Cache-Control': 'no-cache',
},
}).then(r => r.json());
},
getWorkspaceDetail: async (
params: GetWorkspaceDetailParams
): Promise<WorkspaceDetail | null> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${params.id}`, {
method: 'GET',
headers: {
Authorization: auth.token,
},
}).then(r => r.json());
},
getWorkspaceMembers: async (
params: GetWorkspaceDetailParams
): Promise<Member[]> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${params.id}/permission`, {
method: 'GET',
headers: {
Authorization: auth.token,
},
}).then(r => r.json());
},
createWorkspace: async (encodedYDoc: Blob): Promise<{ id: string }> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + 'api/workspace', {
method: 'POST',
body: encodedYDoc,
headers: {
Authorization: auth.token,
},
}).then(r => r.json());
},
updateWorkspace: async (
params: UpdateWorkspaceParams
): Promise<{ public: boolean | null }> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${params.id}`, {
method: 'POST',
body: JSON.stringify({
public: params.public,
}),
headers: {
'Content-Type': 'application/json',
Authorization: auth.token,
},
}).then(r => r.json());
},
deleteWorkspace: async (
params: DeleteWorkspaceParams
): Promise<boolean> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${params.id}`, {
method: 'DELETE',
headers: {
Authorization: auth.token,
},
}).then(r => r.ok);
},
/**
* Notice: Only support normal(contrast to private) workspace.
*/
inviteMember: async (params: InviteMemberParams): Promise<void> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${params.id}/permission`, {
method: 'POST',
body: JSON.stringify({
email: params.email,
}),
headers: {
'Content-Type': 'application/json',
Authorization: auth.token,
},
}).then(r => r.json());
},
removeMember: async (params: RemoveMemberParams): Promise<void> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/permission/${params.permissionId}`, {
method: 'DELETE',
headers: {
Authorization: auth.token,
},
}).then(r => r.json());
},
acceptInviting: async (
params: AcceptInvitingParams
): Promise<Permission> => {
return fetch(prefixUrl + `api/invitation/${params.invitingCode}`, {
method: 'POST',
}).then(r => r.json());
},
uploadBlob: async (params: { blob: Blob }): Promise<string> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + 'api/blob', {
method: 'PUT',
body: params.blob,
headers: {
Authorization: auth.token,
},
}).then(r => r.text());
},
getBlob: async (params: { blobId: string }): Promise<ArrayBuffer> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/blob/${params.blobId}`, {
method: 'GET',
headers: {
Authorization: auth.token,
},
}).then(r => r.arrayBuffer());
},
leaveWorkspace: async ({ id }: LeaveWorkspaceParams) => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${id}/permission`, {
method: 'DELETE',
headers: {
Authorization: auth.token,
},
}).then(r => r.json());
},
downloadWorkspace: async (
workspaceId: string,
published = false
): Promise<ArrayBuffer> => {
if (published) {
return fetch(prefixUrl + `api/public/doc/${workspaceId}`, {
method: 'GET',
}).then(r => r.arrayBuffer());
} else {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${workspaceId}/doc`, {
method: 'GET',
headers: {
Authorization: auth.token,
},
}).then(r => r.arrayBuffer());
}
},
} as const;
}

View File

@@ -1,4 +1,7 @@
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
export const currentAffineUserAtom = atom<AccessTokenMessage | null>(null);
export const currentAffineUserAtom = atomWithStorage<AccessTokenMessage | null>(
'affine-user-atom',
null
);

View File

@@ -10,6 +10,7 @@ import {
signInWithPopup,
} from 'firebase/auth';
import { decode } from 'js-base64';
import { z } from 'zod';
// Connect emulators based on env vars
const envConnectEmulators = process.env.REACT_APP_FIREBASE_EMULATORS === 'true';
@@ -27,12 +28,12 @@ export type LoginParams = {
token: string;
};
export type LoginResponse = {
// access token, expires in a very short time
token: string;
// Refresh token
refresh: string;
};
export const loginResponseSchema = z.object({
token: z.string(),
refresh: z.string(),
});
export type LoginResponse = z.infer<typeof loginResponseSchema>;
const logger = new DebugLogger('token');