mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
@@ -22,6 +22,8 @@ export class QuotaManagementService {
|
||||
expiredAt: quota.expiredAt,
|
||||
blobLimit: quota.feature.blobLimit,
|
||||
storageQuota: quota.feature.storageQuota,
|
||||
historyPeriod: quota.feature.historyPeriod,
|
||||
memberLimit: quota.feature.memberLimit,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
ForbiddenException,
|
||||
HttpStatus,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
} from '@nestjs/graphql';
|
||||
import type { User } from '@prisma/client';
|
||||
import { getStreamAsBuffer } from 'get-stream';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||
import { applyUpdate, Doc } from 'yjs';
|
||||
|
||||
@@ -26,6 +28,7 @@ import type { FileUpload } from '../../../types';
|
||||
import { Auth, CurrentUser, Public } from '../../auth';
|
||||
import { MailService } from '../../auth/mailer';
|
||||
import { AuthService } from '../../auth/service';
|
||||
import { QuotaManagementService } from '../../quota';
|
||||
import { WorkspaceBlobStorage } from '../../storage';
|
||||
import { UsersService, UserType } from '../../users';
|
||||
import { PermissionService } from '../permission';
|
||||
@@ -54,6 +57,7 @@ export class WorkspaceResolver {
|
||||
private readonly mailer: MailService,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly permissions: PermissionService,
|
||||
private readonly quota: QuotaManagementService,
|
||||
private readonly users: UsersService,
|
||||
private readonly event: EventEmitter,
|
||||
private readonly blobStorage: WorkspaceBlobStorage
|
||||
@@ -321,8 +325,23 @@ export class WorkspaceResolver {
|
||||
throw new ForbiddenException('Cannot change owner');
|
||||
}
|
||||
|
||||
const target = await this.users.findUserByEmail(email);
|
||||
// member limit check
|
||||
const [memberCount, quota] = await Promise.all([
|
||||
this.prisma.workspaceUserPermission.count({
|
||||
where: { workspaceId },
|
||||
}),
|
||||
this.quota.getUserQuota(user.id),
|
||||
]);
|
||||
if (memberCount >= quota.memberLimit) {
|
||||
throw new GraphQLError('Workspace member limit reached', {
|
||||
extensions: {
|
||||
status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE],
|
||||
code: HttpStatus.PAYLOAD_TOO_LARGE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let target = await this.users.findUserByEmail(email);
|
||||
if (target) {
|
||||
const originRecord = await this.prisma.workspaceUserPermission.findFirst({
|
||||
where: {
|
||||
@@ -330,94 +349,52 @@ export class WorkspaceResolver {
|
||||
userId: target.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (originRecord) {
|
||||
return originRecord.id;
|
||||
}
|
||||
|
||||
const inviteId = await this.permissions.grant(
|
||||
workspaceId,
|
||||
target.id,
|
||||
permission
|
||||
);
|
||||
if (sendInviteMail) {
|
||||
const inviteInfo = await this.getInviteInfo(inviteId);
|
||||
|
||||
try {
|
||||
await this.mailer.sendInviteEmail(email, inviteId, {
|
||||
workspace: {
|
||||
id: inviteInfo.workspace.id,
|
||||
name: inviteInfo.workspace.name,
|
||||
avatar: inviteInfo.workspace.avatar,
|
||||
},
|
||||
user: {
|
||||
avatar: inviteInfo.user?.avatarUrl || '',
|
||||
name: inviteInfo.user?.name || '',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const ret = await this.permissions.revokeWorkspace(
|
||||
workspaceId,
|
||||
target.id
|
||||
);
|
||||
|
||||
if (!ret) {
|
||||
this.logger.fatal(
|
||||
`failed to send ${workspaceId} invite email to ${email} and failed to revoke permission: ${inviteId}, ${e}`
|
||||
);
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`failed to send ${workspaceId} invite email to ${email}, but successfully revoked permission: ${e}`
|
||||
);
|
||||
}
|
||||
|
||||
return new InternalServerErrorException(e);
|
||||
}
|
||||
}
|
||||
return inviteId;
|
||||
// only invite if the user is not already in the workspace
|
||||
if (originRecord) return originRecord.id;
|
||||
} else {
|
||||
const user = await this.auth.createAnonymousUser(email);
|
||||
const inviteId = await this.permissions.grant(
|
||||
workspaceId,
|
||||
user.id,
|
||||
permission
|
||||
);
|
||||
if (sendInviteMail) {
|
||||
const inviteInfo = await this.getInviteInfo(inviteId);
|
||||
|
||||
try {
|
||||
await this.mailer.sendInviteEmail(email, inviteId, {
|
||||
workspace: {
|
||||
id: inviteInfo.workspace.id,
|
||||
name: inviteInfo.workspace.name,
|
||||
avatar: inviteInfo.workspace.avatar,
|
||||
},
|
||||
user: {
|
||||
avatar: inviteInfo.user?.avatarUrl || '',
|
||||
name: inviteInfo.user?.name || '',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const ret = await this.permissions.revokeWorkspace(
|
||||
workspaceId,
|
||||
user.id
|
||||
);
|
||||
|
||||
if (!ret) {
|
||||
this.logger.fatal(
|
||||
`failed to send ${workspaceId} invite email to ${email} and failed to revoke permission: ${inviteId}, ${e}`
|
||||
);
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`failed to send ${workspaceId} invite email to ${email}, but successfully revoked permission: ${e}`
|
||||
);
|
||||
}
|
||||
|
||||
return new InternalServerErrorException(e);
|
||||
}
|
||||
}
|
||||
return inviteId;
|
||||
target = await this.auth.createAnonymousUser(email);
|
||||
}
|
||||
|
||||
const inviteId = await this.permissions.grant(
|
||||
workspaceId,
|
||||
target.id,
|
||||
permission
|
||||
);
|
||||
if (sendInviteMail) {
|
||||
const inviteInfo = await this.getInviteInfo(inviteId);
|
||||
|
||||
try {
|
||||
await this.mailer.sendInviteEmail(email, inviteId, {
|
||||
workspace: {
|
||||
id: inviteInfo.workspace.id,
|
||||
name: inviteInfo.workspace.name,
|
||||
avatar: inviteInfo.workspace.avatar,
|
||||
},
|
||||
user: {
|
||||
avatar: inviteInfo.user?.avatarUrl || '',
|
||||
name: inviteInfo.user?.name || '',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const ret = await this.permissions.revokeWorkspace(
|
||||
workspaceId,
|
||||
target.id
|
||||
);
|
||||
|
||||
if (!ret) {
|
||||
this.logger.fatal(
|
||||
`failed to send ${workspaceId} invite email to ${email} and failed to revoke permission: ${inviteId}, ${e}`
|
||||
);
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`failed to send ${workspaceId} invite email to ${email}, but successfully revoked permission: ${e}`
|
||||
);
|
||||
}
|
||||
|
||||
return new InternalServerErrorException(e);
|
||||
}
|
||||
}
|
||||
return inviteId;
|
||||
}
|
||||
|
||||
@Throttle({
|
||||
|
||||
@@ -9,7 +9,8 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
|
||||
import { AppModule } from '../src/app';
|
||||
import { MailService } from '../src/modules/auth/mailer';
|
||||
import { FeatureManagementService } from '../src/modules/features';
|
||||
import { FeatureKind, FeatureManagementService } from '../src/modules/features';
|
||||
import { Quotas } from '../src/modules/quota';
|
||||
import { PrismaService } from '../src/prisma';
|
||||
import { createWorkspace, getInviteInfo, inviteUser, signUp } from './utils';
|
||||
|
||||
@@ -88,6 +89,32 @@ const FakePrisma = {
|
||||
},
|
||||
};
|
||||
},
|
||||
get features() {
|
||||
return {
|
||||
async findFirst() {
|
||||
return {
|
||||
id: 1,
|
||||
type: FeatureKind.Quota,
|
||||
feature: Quotas[0].feature,
|
||||
configs: Quotas[0].configs,
|
||||
version: Quotas[0].version,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
get userFeatures() {
|
||||
return {
|
||||
async findFirst() {
|
||||
return {
|
||||
createdAt: new Date(),
|
||||
featureId: 1,
|
||||
reason: '',
|
||||
expiredAt: null,
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const test = ava as TestFn<{
|
||||
|
||||
Reference in New Issue
Block a user