mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-01 17:39:55 +08:00
@@ -22,6 +22,8 @@ export class QuotaManagementService {
|
|||||||
expiredAt: quota.expiredAt,
|
expiredAt: quota.expiredAt,
|
||||||
blobLimit: quota.feature.blobLimit,
|
blobLimit: quota.feature.blobLimit,
|
||||||
storageQuota: quota.feature.storageQuota,
|
storageQuota: quota.feature.storageQuota,
|
||||||
|
historyPeriod: quota.feature.historyPeriod,
|
||||||
|
memberLimit: quota.feature.memberLimit,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ForbiddenException,
|
ForbiddenException,
|
||||||
|
HttpStatus,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
Logger,
|
Logger,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
@@ -16,6 +17,7 @@ import {
|
|||||||
} from '@nestjs/graphql';
|
} from '@nestjs/graphql';
|
||||||
import type { User } from '@prisma/client';
|
import type { User } from '@prisma/client';
|
||||||
import { getStreamAsBuffer } from 'get-stream';
|
import { getStreamAsBuffer } from 'get-stream';
|
||||||
|
import { GraphQLError } from 'graphql';
|
||||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||||
import { applyUpdate, Doc } from 'yjs';
|
import { applyUpdate, Doc } from 'yjs';
|
||||||
|
|
||||||
@@ -26,6 +28,7 @@ import type { FileUpload } from '../../../types';
|
|||||||
import { Auth, CurrentUser, Public } from '../../auth';
|
import { Auth, CurrentUser, Public } from '../../auth';
|
||||||
import { MailService } from '../../auth/mailer';
|
import { MailService } from '../../auth/mailer';
|
||||||
import { AuthService } from '../../auth/service';
|
import { AuthService } from '../../auth/service';
|
||||||
|
import { QuotaManagementService } from '../../quota';
|
||||||
import { WorkspaceBlobStorage } from '../../storage';
|
import { WorkspaceBlobStorage } from '../../storage';
|
||||||
import { UsersService, UserType } from '../../users';
|
import { UsersService, UserType } from '../../users';
|
||||||
import { PermissionService } from '../permission';
|
import { PermissionService } from '../permission';
|
||||||
@@ -54,6 +57,7 @@ export class WorkspaceResolver {
|
|||||||
private readonly mailer: MailService,
|
private readonly mailer: MailService,
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly permissions: PermissionService,
|
private readonly permissions: PermissionService,
|
||||||
|
private readonly quota: QuotaManagementService,
|
||||||
private readonly users: UsersService,
|
private readonly users: UsersService,
|
||||||
private readonly event: EventEmitter,
|
private readonly event: EventEmitter,
|
||||||
private readonly blobStorage: WorkspaceBlobStorage
|
private readonly blobStorage: WorkspaceBlobStorage
|
||||||
@@ -321,8 +325,23 @@ export class WorkspaceResolver {
|
|||||||
throw new ForbiddenException('Cannot change owner');
|
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) {
|
if (target) {
|
||||||
const originRecord = await this.prisma.workspaceUserPermission.findFirst({
|
const originRecord = await this.prisma.workspaceUserPermission.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@@ -330,94 +349,52 @@ export class WorkspaceResolver {
|
|||||||
userId: target.id,
|
userId: target.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// only invite if the user is not already in the workspace
|
||||||
if (originRecord) {
|
if (originRecord) return originRecord.id;
|
||||||
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;
|
|
||||||
} else {
|
} else {
|
||||||
const user = await this.auth.createAnonymousUser(email);
|
target = 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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({
|
@Throttle({
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
|||||||
|
|
||||||
import { AppModule } from '../src/app';
|
import { AppModule } from '../src/app';
|
||||||
import { MailService } from '../src/modules/auth/mailer';
|
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 { PrismaService } from '../src/prisma';
|
||||||
import { createWorkspace, getInviteInfo, inviteUser, signUp } from './utils';
|
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<{
|
const test = ava as TestFn<{
|
||||||
|
|||||||
Reference in New Issue
Block a user