mirror of
https://github.com/OneUptime/oneuptime
synced 2024-11-22 23:30:10 +00:00
c8a23df5b6
Update error message for user limit reached in free plan Remove unused import statement Fix indentation in Home/Index.ts Fix indentation and add line breaks in HardDeleteItemsInDatabase.ts
475 lines
15 KiB
TypeScript
475 lines
15 KiB
TypeScript
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
|
|
import DatabaseService from './DatabaseService';
|
|
import { OnCreate, OnDelete, OnUpdate } from '../Types/Database/Hooks';
|
|
import CreateBy from '../Types/Database/CreateBy';
|
|
import AccessTokenService from './AccessTokenService';
|
|
import Email from 'Common/Types/Email';
|
|
import UserService from './UserService';
|
|
import User from 'Model/Models/User';
|
|
import UpdateBy from '../Types/Database/UpdateBy';
|
|
import DeleteBy from '../Types/Database/DeleteBy';
|
|
import ObjectID from 'Common/Types/ObjectID';
|
|
import QueryHelper from '../Types/Database/QueryHelper';
|
|
import LIMIT_MAX from 'Common/Types/Database/LimitMax';
|
|
import ProjectService from './ProjectService';
|
|
import { IsBillingEnabled } from '../EnvironmentConfig';
|
|
import { AccountsRoute } from 'Common/ServiceRoute';
|
|
import DatabaseConfig from '../DatabaseConfig';
|
|
import BillingService from './BillingService';
|
|
import SubscriptionPlan, {
|
|
PlanSelect,
|
|
} from 'Common/Types/Billing/SubscriptionPlan';
|
|
import Project from 'Model/Models/Project';
|
|
import MailService from './MailService';
|
|
import EmailTemplateType from 'Common/Types/Email/EmailTemplateType';
|
|
import URL from 'Common/Types/API/URL';
|
|
import logger from '../Utils/Logger';
|
|
import BadDataException from 'Common/Types/Exception/BadDataException';
|
|
import PositiveNumber from 'Common/Types/PositiveNumber';
|
|
import TeamMember from 'Model/Models/TeamMember';
|
|
import UserNotificationRuleService from './UserNotificationRuleService';
|
|
import UserNotificationSettingService from './UserNotificationSettingService';
|
|
import Hostname from 'Common/Types/API/Hostname';
|
|
import Protocol from 'Common/Types/API/Protocol';
|
|
import Errors from '../Utils/Errors';
|
|
|
|
export class TeamMemberService extends DatabaseService<TeamMember> {
|
|
public constructor(postgresDatabase?: PostgresDatabase) {
|
|
super(TeamMember, postgresDatabase);
|
|
}
|
|
|
|
protected override async onBeforeCreate(
|
|
createBy: CreateBy<TeamMember>
|
|
): Promise<OnCreate<TeamMember>> {
|
|
// check if this project can have more members.
|
|
if (IsBillingEnabled && createBy.data.projectId) {
|
|
const project: Project | null = await ProjectService.findOneById({
|
|
id: createBy.data.projectId!,
|
|
select: {
|
|
seatLimit: true,
|
|
paymentProviderSubscriptionSeats: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (
|
|
project &&
|
|
project.seatLimit &&
|
|
project.paymentProviderSubscriptionSeats &&
|
|
project.paymentProviderSubscriptionSeats >= project.seatLimit
|
|
) {
|
|
throw new BadDataException(
|
|
Errors.TeamMemberService.LIMIT_REACHED
|
|
);
|
|
}
|
|
|
|
if (
|
|
createBy.props.currentPlan === PlanSelect.Free &&
|
|
project &&
|
|
project.paymentProviderSubscriptionSeats &&
|
|
project.paymentProviderSubscriptionSeats >= 1
|
|
) {
|
|
throw new BadDataException(
|
|
Errors.TeamMemberService.LIMIT_REACHED_FOR_FREE_PLAN
|
|
);
|
|
}
|
|
}
|
|
|
|
createBy.data.hasAcceptedInvitation = false;
|
|
|
|
if (createBy.miscDataProps && createBy.miscDataProps['email']) {
|
|
const email: Email = new Email(
|
|
createBy.miscDataProps['email'] as string
|
|
);
|
|
|
|
let user: User | null = await UserService.findByEmail(email, {
|
|
isRoot: true,
|
|
});
|
|
|
|
let isNewUser: boolean = false;
|
|
|
|
if (!user) {
|
|
isNewUser = true;
|
|
user = await UserService.createByEmail(email, {
|
|
isRoot: true,
|
|
});
|
|
}
|
|
|
|
createBy.data.userId = user.id!;
|
|
|
|
const project: Project | null = await ProjectService.findOneById({
|
|
id: createBy.data.projectId!,
|
|
select: {
|
|
name: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (project) {
|
|
const host: Hostname = await DatabaseConfig.getHost();
|
|
const httpProtocol: Protocol =
|
|
await DatabaseConfig.getHttpProtocol();
|
|
|
|
MailService.sendMail(
|
|
{
|
|
toEmail: email,
|
|
templateType: EmailTemplateType.InviteMember,
|
|
vars: {
|
|
signInLink: URL.fromString(
|
|
new URL(
|
|
httpProtocol,
|
|
host,
|
|
AccountsRoute
|
|
).toString()
|
|
).toString(),
|
|
registerLink: URL.fromString(
|
|
new URL(
|
|
httpProtocol,
|
|
host,
|
|
AccountsRoute
|
|
).toString()
|
|
)
|
|
.addRoute('/register')
|
|
.addQueryParam('email', email.toString(), true)
|
|
.toString(),
|
|
isNewUser: isNewUser.toString(),
|
|
projectName: project.name!,
|
|
homeUrl: new URL(httpProtocol, host).toString(),
|
|
},
|
|
subject: 'You have been invited to ' + project.name,
|
|
},
|
|
{
|
|
projectId: createBy.data.projectId!,
|
|
}
|
|
).catch((err: Error) => {
|
|
logger.error(err);
|
|
});
|
|
}
|
|
}
|
|
|
|
//check if this user is already invited.
|
|
|
|
const member: TeamMember | null = await this.findOneBy({
|
|
query: {
|
|
userId: createBy.data.userId!,
|
|
teamId:
|
|
createBy.data.teamId ||
|
|
new ObjectID(createBy.data.team!._id!),
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
},
|
|
});
|
|
|
|
if (member) {
|
|
throw new BadDataException(
|
|
Errors.TeamMemberService.ALREADY_INVITED
|
|
);
|
|
}
|
|
|
|
return { createBy, carryForward: null };
|
|
}
|
|
|
|
public async refreshTokens(
|
|
userId: ObjectID,
|
|
projectId: ObjectID
|
|
): Promise<void> {
|
|
/// Refresh tokens.
|
|
await AccessTokenService.refreshUserGlobalAccessPermission(userId);
|
|
|
|
await AccessTokenService.refreshUserTenantAccessPermission(
|
|
userId,
|
|
projectId
|
|
);
|
|
}
|
|
|
|
protected override async onCreateSuccess(
|
|
onCreate: OnCreate<TeamMember>,
|
|
createdItem: TeamMember
|
|
): Promise<TeamMember> {
|
|
await this.refreshTokens(
|
|
onCreate.createBy.data.userId!,
|
|
onCreate.createBy.data.projectId!
|
|
);
|
|
|
|
await this.updateSubscriptionSeatsByUniqueTeamMembersInProject(
|
|
onCreate.createBy.data.projectId!
|
|
);
|
|
|
|
return createdItem;
|
|
}
|
|
|
|
protected override async onUpdateSuccess(
|
|
onUpdate: OnUpdate<TeamMember>,
|
|
updatedItemIds: Array<ObjectID>
|
|
): Promise<OnUpdate<TeamMember>> {
|
|
const updateBy: UpdateBy<TeamMember> = onUpdate.updateBy;
|
|
const items: Array<TeamMember> = await this.findBy({
|
|
query: {
|
|
_id: QueryHelper.in(updatedItemIds),
|
|
},
|
|
select: {
|
|
userId: true,
|
|
user: {
|
|
email: true,
|
|
isEmailVerified: true,
|
|
},
|
|
projectId: true,
|
|
},
|
|
limit: LIMIT_MAX,
|
|
skip: 0,
|
|
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
for (const item of items) {
|
|
await this.refreshTokens(item.userId!, item.projectId!);
|
|
|
|
if (
|
|
updateBy.data.hasAcceptedInvitation &&
|
|
item.user?.isEmailVerified
|
|
) {
|
|
await UserNotificationSettingService.addDefaultNotificationSettingsForUser(
|
|
item.userId!,
|
|
item.projectId!
|
|
);
|
|
await UserNotificationRuleService.addDefaultNotificationRuleForUser(
|
|
item.projectId!,
|
|
item.userId!,
|
|
item.user?.email as Email
|
|
);
|
|
}
|
|
}
|
|
|
|
return { updateBy, carryForward: onUpdate.carryForward };
|
|
}
|
|
|
|
protected override async onBeforeDelete(
|
|
deleteBy: DeleteBy<TeamMember>
|
|
): Promise<OnDelete<TeamMember>> {
|
|
const members: Array<TeamMember> = await this.findBy({
|
|
query: deleteBy.query,
|
|
select: {
|
|
userId: true,
|
|
projectId: true,
|
|
teamId: true,
|
|
hasAcceptedInvitation: true,
|
|
team: {
|
|
_id: true,
|
|
shouldHaveAtLeastOneMember: true,
|
|
},
|
|
},
|
|
limit: LIMIT_MAX,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
// check if there's one member in the team.
|
|
for (const member of members) {
|
|
if (member.team?.shouldHaveAtLeastOneMember) {
|
|
if (!member.hasAcceptedInvitation) {
|
|
continue;
|
|
}
|
|
|
|
const membersInTeam: PositiveNumber = await this.countBy({
|
|
query: {
|
|
teamId: member.teamId!,
|
|
hasAcceptedInvitation: true,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_MAX,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (membersInTeam.toNumber() <= 1) {
|
|
throw new BadDataException(
|
|
Errors.TeamMemberService.ONE_MEMBER_REQUIRED
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
deleteBy: deleteBy,
|
|
carryForward: members,
|
|
};
|
|
}
|
|
|
|
protected override async onDeleteSuccess(
|
|
onDelete: OnDelete<TeamMember>
|
|
): Promise<OnDelete<TeamMember>> {
|
|
for (const item of onDelete.carryForward as Array<TeamMember>) {
|
|
await this.refreshTokens(item.userId!, item.projectId!);
|
|
await this.updateSubscriptionSeatsByUniqueTeamMembersInProject(
|
|
item.projectId!
|
|
);
|
|
await UserNotificationSettingService.removeDefaultNotificationSettingsForUser(
|
|
item.userId!,
|
|
item.projectId!
|
|
);
|
|
}
|
|
|
|
return onDelete;
|
|
}
|
|
|
|
public async getUniqueTeamMemberCountInProject(
|
|
projectId: ObjectID
|
|
): Promise<number> {
|
|
const members: Array<TeamMember> = await this.findBy({
|
|
query: {
|
|
projectId: projectId!,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
select: {
|
|
userId: true,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_MAX,
|
|
});
|
|
|
|
const memberIds: Array<string | undefined> = members
|
|
.map((member: TeamMember) => {
|
|
return member.userId?.toString();
|
|
})
|
|
.filter((memberId: string | undefined) => {
|
|
return Boolean(memberId);
|
|
});
|
|
|
|
return [...new Set(memberIds)].length; //get unique member ids.
|
|
}
|
|
|
|
public async getUsersInTeams(
|
|
teamIds: Array<ObjectID>
|
|
): Promise<Array<User>> {
|
|
const members: Array<TeamMember> = await this.findBy({
|
|
query: {
|
|
teamId: QueryHelper.in(teamIds),
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
user: {
|
|
_id: true,
|
|
email: true,
|
|
name: true,
|
|
},
|
|
},
|
|
|
|
skip: 0,
|
|
limit: LIMIT_MAX,
|
|
});
|
|
|
|
const uniqueUserIds: Set<string> = new Set<string>();
|
|
const uniqueMembers: TeamMember[] = members.filter(
|
|
(member: TeamMember) => {
|
|
const userId: string | undefined = member.user?._id?.toString();
|
|
if (userId && !uniqueUserIds.has(userId)) {
|
|
uniqueUserIds.add(userId);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
);
|
|
|
|
return uniqueMembers.map((member: TeamMember) => {
|
|
return member.user!;
|
|
});
|
|
}
|
|
|
|
public async getUsersInTeam(teamId: ObjectID): Promise<Array<User>> {
|
|
const members: Array<TeamMember> = await this.findBy({
|
|
query: {
|
|
teamId: teamId,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
user: {
|
|
_id: true,
|
|
email: true,
|
|
name: true,
|
|
},
|
|
},
|
|
|
|
skip: 0,
|
|
limit: LIMIT_MAX,
|
|
});
|
|
|
|
return members.map((member: TeamMember) => {
|
|
return member.user!;
|
|
});
|
|
}
|
|
|
|
public async updateSubscriptionSeatsByUniqueTeamMembersInProject(
|
|
projectId: ObjectID
|
|
): Promise<void> {
|
|
if (!IsBillingEnabled) {
|
|
return;
|
|
}
|
|
|
|
const numberOfMembers: number =
|
|
await this.getUniqueTeamMemberCountInProject(projectId);
|
|
const project: Project | null = await ProjectService.findOneById({
|
|
id: projectId,
|
|
select: {
|
|
paymentProviderSubscriptionId: true,
|
|
paymentProviderPlanId: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (
|
|
project &&
|
|
project.paymentProviderSubscriptionId &&
|
|
project?.paymentProviderPlanId
|
|
) {
|
|
const plan: SubscriptionPlan | undefined =
|
|
SubscriptionPlan.getSubscriptionPlanById(
|
|
project?.paymentProviderPlanId
|
|
);
|
|
|
|
if (!plan) {
|
|
return;
|
|
}
|
|
|
|
await BillingService.changeQuantity(
|
|
project.paymentProviderSubscriptionId,
|
|
numberOfMembers
|
|
);
|
|
|
|
await ProjectService.updateOneById({
|
|
id: projectId,
|
|
data: {
|
|
paymentProviderSubscriptionSeats: numberOfMembers,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export default new TeamMemberService();
|