feat: new workspace apis (#2825)
This commit is contained in:
@@ -11,4 +11,6 @@ import { AuthService } from './service';
|
|||||||
controllers: [NextAuthController],
|
controllers: [NextAuthController],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|
||||||
export * from './guard';
|
export * from './guard';
|
||||||
|
export { TokenType } from './resolver';
|
||||||
|
|||||||
@@ -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)
|
@Mutation(() => UserType)
|
||||||
async signIn(
|
async signIn(
|
||||||
@Context() ctx: { req: Request },
|
@Context() ctx: { req: Request },
|
||||||
|
|||||||
@@ -8,3 +8,5 @@ import { UserResolver } from './resolver';
|
|||||||
providers: [UserResolver],
|
providers: [UserResolver],
|
||||||
})
|
})
|
||||||
export class UsersModule {}
|
export class UsersModule {}
|
||||||
|
|
||||||
|
export { UserType } from './resolver';
|
||||||
|
|||||||
@@ -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) {
|
async revoke(ws: string, user: string) {
|
||||||
const result = await this.prisma.userWorkspacePermission.deleteMany({
|
const result = await this.prisma.userWorkspacePermission.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -111,6 +111,29 @@ export class WorkspaceResolver {
|
|||||||
return data.user;
|
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], {
|
@Query(() => [WorkspaceType], {
|
||||||
description: 'Get all accessible workspaces for current user',
|
description: 'Get all accessible workspaces for current user',
|
||||||
complexity: 2,
|
complexity: 2,
|
||||||
@@ -203,4 +226,61 @@ export class WorkspaceResolver {
|
|||||||
|
|
||||||
return true;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ type WorkspaceType {
|
|||||||
Owner of workspace
|
Owner of workspace
|
||||||
"""
|
"""
|
||||||
owner: UserType!
|
owner: UserType!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Members of workspace
|
||||||
|
"""
|
||||||
|
members: [UserType!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -94,6 +99,7 @@ type Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
|
register(name: String!, email: String!, password: String!): UserType!
|
||||||
signIn(email: String!, password: String!): UserType!
|
signIn(email: String!, password: String!): UserType!
|
||||||
signUp(email: String!, password: String!, name: String!): UserType!
|
signUp(email: String!, password: String!, name: String!): UserType!
|
||||||
|
|
||||||
@@ -107,6 +113,14 @@ type Mutation {
|
|||||||
"""
|
"""
|
||||||
updateWorkspace(input: UpdateWorkspaceInput!): WorkspaceType!
|
updateWorkspace(input: UpdateWorkspaceInput!): WorkspaceType!
|
||||||
deleteWorkspace(id: String!): Boolean!
|
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
|
Upload user avatar
|
||||||
|
|||||||
216
apps/server/src/tests/workspace.spec.ts
Normal file
216
apps/server/src/tests/workspace.spec.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user