feat: adapted user quota for member api (#5521)

fix AFF-494 TOV-337
This commit is contained in:
DarkSky 2024-01-10 07:28:46 +00:00
parent 275ea74772
commit a59fe1b49e
No known key found for this signature in database
GPG Key ID: 97B7D036B1566E9D
3 changed files with 94 additions and 88 deletions

View File

@ -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,
};
}

View File

@ -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({

View File

@ -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<{