feat: new workspace apis (#2825)

This commit is contained in:
DarkSky 2023-06-26 22:12:58 +08:00 committed by GitHub
parent e3ffd04804
commit d46b6c4863
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 341 additions and 0 deletions

View File

@ -11,4 +11,6 @@ import { AuthService } from './service';
controllers: [NextAuthController],
})
export class AuthModule {}
export * from './guard';
export { TokenType } from './resolver';

View File

@ -40,6 +40,18 @@ export class AuthResolver {
};
}
@Mutation(() => UserType)
async register(
@Context() ctx: { req: Request },
@Args('name') name: string,
@Args('email') email: string,
@Args('password') password: string
) {
const user = await this.auth.register(name, email, password);
ctx.req.user = user;
return user;
}
@Mutation(() => UserType)
async signIn(
@Context() ctx: { req: Request },

View File

@ -8,3 +8,5 @@ import { UserResolver } from './resolver';
providers: [UserResolver],
})
export class UsersModule {}
export { UserType } from './resolver';

View File

@ -117,6 +117,21 @@ export class PermissionService {
});
}
async accept(ws: string, user: string) {
const result = await this.prisma.userWorkspacePermission.updateMany({
where: {
workspaceId: ws,
userId: user,
accepted: false,
},
data: {
accepted: true,
},
});
return result.count > 0;
}
async revoke(ws: string, user: string) {
const result = await this.prisma.userWorkspacePermission.deleteMany({
where: {

View File

@ -111,6 +111,29 @@ export class WorkspaceResolver {
return data.user;
}
@ResolveField(() => [UserType], {
description: 'Members of workspace',
complexity: 2,
})
async members(
@CurrentUser() user: UserType,
@Parent() workspace: WorkspaceType
) {
const data = await this.prisma.userWorkspacePermission.findMany({
where: {
workspaceId: workspace.id,
accepted: true,
userId: {
not: user.id,
},
},
include: {
user: true,
},
});
return data.map(({ user }) => user);
}
@Query(() => [WorkspaceType], {
description: 'Get all accessible workspaces for current user',
complexity: 2,
@ -203,4 +226,61 @@ export class WorkspaceResolver {
return true;
}
@Mutation(() => Boolean)
async invite(
@CurrentUser() user: User,
@Args('workspaceId') workspaceId: string,
@Args('email') email: string,
@Args('permission', { type: () => Permission }) permission: Permission
) {
await this.permissionProvider.check(workspaceId, user.id, Permission.Admin);
if (permission === Permission.Owner) {
throw new ForbiddenException('Cannot change owner');
}
const target = await this.prisma.user.findUnique({
where: {
email,
},
});
if (!target) {
throw new NotFoundException("User doesn't exist");
}
await this.permissionProvider.grant(workspaceId, target.id, permission);
return true;
}
@Mutation(() => Boolean)
async revoke(
@CurrentUser() user: User,
@Args('workspaceId') workspaceId: string,
@Args('userId') userId: string
) {
await this.permissionProvider.check(workspaceId, user.id, Permission.Admin);
return this.permissionProvider.revoke(workspaceId, userId);
}
@Mutation(() => Boolean)
async acceptInvite(
@CurrentUser() user: User,
@Args('workspaceId') workspaceId: string
) {
return this.permissionProvider.accept(workspaceId, user.id);
}
@Mutation(() => Boolean)
async leaveWorkspace(
@CurrentUser() user: User,
@Args('workspaceId') workspaceId: string
) {
await this.permissionProvider.check(workspaceId, user.id);
return this.permissionProvider.revoke(workspaceId, user.id);
}
}

View File

@ -64,6 +64,11 @@ type WorkspaceType {
Owner of workspace
"""
owner: UserType!
"""
Members of workspace
"""
members: [UserType!]!
}
"""
@ -94,6 +99,7 @@ type Query {
}
type Mutation {
register(name: String!, email: String!, password: String!): UserType!
signIn(email: String!, password: String!): UserType!
signUp(email: String!, password: String!, name: String!): UserType!
@ -107,6 +113,14 @@ type Mutation {
"""
updateWorkspace(input: UpdateWorkspaceInput!): WorkspaceType!
deleteWorkspace(id: String!): Boolean!
invite(
workspaceId: String!
email: String!
permission: Permission!
): Boolean!
revoke(workspaceId: String!, userId: String!): Boolean!
acceptInvite(workspaceId: String!): Boolean!
leaveWorkspace(workspaceId: String!): Boolean!
"""
Upload user avatar

View File

@ -0,0 +1,216 @@
import { ok } from 'node:assert';
import { afterEach, beforeEach, describe, test } from 'node:test';
import type { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import request from 'supertest';
import { AppModule } from '../app';
import { getDefaultAFFiNEConfig } from '../config/default';
import type { TokenType } from '../modules/auth';
import type { UserType } from '../modules/users';
import type { WorkspaceType } from '../modules/workspaces';
const gql = '/graphql';
globalThis.AFFiNE = getDefaultAFFiNEConfig();
describe('AppModule', () => {
let app: INestApplication;
// cleanup database before each test
beforeEach(async () => {
const client = new PrismaClient();
await client.$connect();
await client.user.deleteMany({});
await client.$disconnect();
});
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = module.createNestApplication();
await app.init();
});
afterEach(async () => {
await app.close();
});
async function registerUser(
name: string,
email: string,
password: string
): Promise<UserType & { token: TokenType }> {
const res = await request(app.getHttpServer())
.post(gql)
.send({
query: `
mutation {
register(name: "${name}", email: "${email}", password: "${password}") {
id, name, email, token { token }
}
}
`,
})
.expect(200);
return res.body.data.register;
}
async function createWorkspace(token: string): Promise<WorkspaceType> {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.send({
query: `
mutation {
createWorkspace {
id
}
}
`,
})
.expect(200);
return res.body.data.createWorkspace;
}
async function inviteUser(
token: string,
workspaceId: string,
email: string,
permission: string
): Promise<boolean> {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.send({
query: `
mutation {
invite(workspaceId: "${workspaceId}", email: "${email}", permission: ${permission})
}
`,
})
.expect(200);
return res.body.data.invite;
}
async function acceptInvite(
token: string,
workspaceId: string
): Promise<boolean> {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.send({
query: `
mutation {
acceptInvite(workspaceId: "${workspaceId}")
}
`,
})
.expect(200);
return res.body.data.acceptInvite;
}
async function leaveWorkspace(
token: string,
workspaceId: string
): Promise<boolean> {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.send({
query: `
mutation {
leaveWorkspace(workspaceId: "${workspaceId}")
}
`,
})
.expect(200);
return res.body.data.leaveWorkspace;
}
async function revokeUser(
token: string,
workspaceId: string,
userId: string
): Promise<boolean> {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.send({
query: `
mutation {
revoke(workspaceId: "${workspaceId}", userId: "${userId}")
}
`,
})
.expect(200);
return res.body.data.revoke;
}
test('should register a user', async () => {
const user = await registerUser('u1', 'u1@affine.pro', '123456');
ok(typeof user.id === 'string', 'user.id is not a string');
ok(user.name === 'u1', 'user.name is not valid');
ok(user.email === 'u1@affine.pro', 'user.email is not valid');
});
test('should create a workspace', async () => {
const user = await registerUser('u1', 'u1@affine.pro', '1');
const workspace = await createWorkspace(user.token.token);
ok(typeof workspace.id === 'string', 'workspace.id is not a string');
});
test('should invite a user', async () => {
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
const workspace = await createWorkspace(u1.token.token);
const invite = await inviteUser(
u1.token.token,
workspace.id,
u2.email,
'Admin'
);
ok(invite === true, 'failed to invite user');
});
test('should accept an invite', async () => {
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
const workspace = await createWorkspace(u1.token.token);
await inviteUser(u1.token.token, workspace.id, u2.email, 'Admin');
const accept = await acceptInvite(u2.token.token, workspace.id);
ok(accept === true, 'failed to accept invite');
});
test('should leave a workspace', async () => {
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
const workspace = await createWorkspace(u1.token.token);
await inviteUser(u1.token.token, workspace.id, u2.email, 'Admin');
await acceptInvite(u2.token.token, workspace.id);
const leave = await leaveWorkspace(u2.token.token, workspace.id);
ok(leave === true, 'failed to leave workspace');
});
test('should revoke a user', async () => {
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
const workspace = await createWorkspace(u1.token.token);
await inviteUser(u1.token.token, workspace.id, u2.email, 'Admin');
const revoke = await revokeUser(u1.token.token, workspace.id, u2.id);
ok(revoke === true, 'failed to revoke user');
});
});