mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 11:41:44 +10:00
fix: wip
This commit is contained in:
@ -1,101 +0,0 @@
|
||||
import { TeamMemberInviteStatus } from '@prisma/client';
|
||||
|
||||
import { updateSubscriptionItemQuantity } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { jobs } from '../../jobs/client';
|
||||
|
||||
export type AcceptTeamInvitationOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const acceptTeamInvitation = async ({ userId, teamId }: AcceptTeamInvitationOptions) => {
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const user = await tx.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const teamMemberInvite = await tx.teamMemberInvite.findFirstOrThrow({
|
||||
where: {
|
||||
teamId,
|
||||
email: user.email,
|
||||
status: {
|
||||
not: TeamMemberInviteStatus.DECLINED,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
include: {
|
||||
subscription: true,
|
||||
members: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (teamMemberInvite.status === TeamMemberInviteStatus.ACCEPTED) {
|
||||
const memberExists = await tx.teamMember.findFirst({
|
||||
where: {
|
||||
teamId: teamMemberInvite.teamId,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (memberExists) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { team } = teamMemberInvite;
|
||||
|
||||
const teamMember = await tx.teamMember.create({
|
||||
data: {
|
||||
teamId: teamMemberInvite.teamId,
|
||||
userId: user.id,
|
||||
role: teamMemberInvite.role,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.teamMemberInvite.update({
|
||||
where: {
|
||||
id: teamMemberInvite.id,
|
||||
},
|
||||
data: {
|
||||
status: TeamMemberInviteStatus.ACCEPTED,
|
||||
},
|
||||
});
|
||||
|
||||
if (IS_BILLING_ENABLED() && team.subscription) {
|
||||
const numberOfSeats = await tx.teamMember.count({
|
||||
where: {
|
||||
teamId: teamMemberInvite.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
await updateSubscriptionItemQuantity({
|
||||
priceId: team.subscription.priceId,
|
||||
subscriptionId: team.subscription.planId,
|
||||
quantity: numberOfSeats,
|
||||
});
|
||||
}
|
||||
|
||||
await jobs.triggerJob({
|
||||
name: 'send.team-member-joined.email',
|
||||
payload: {
|
||||
teamId: teamMember.teamId,
|
||||
memberId: teamMember.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
};
|
||||
@ -1,47 +0,0 @@
|
||||
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type CreateTeamBillingPortalOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const createTeamBillingPortal = async ({
|
||||
userId,
|
||||
teamId,
|
||||
}: CreateTeamBillingPortalOptions) => {
|
||||
if (!IS_BILLING_ENABLED()) {
|
||||
throw new Error('Billing is not enabled');
|
||||
}
|
||||
|
||||
const team = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
role: {
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_BILLING'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
subscription: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team.subscription) {
|
||||
throw new Error('Team has no subscription');
|
||||
}
|
||||
|
||||
if (!team.customerId) {
|
||||
throw new Error('Team has no customerId');
|
||||
}
|
||||
|
||||
return getPortalSession({
|
||||
customerId: team.customerId,
|
||||
});
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import type { Team, TeamGlobalSettings } from '@prisma/client';
|
||||
import type { OrganisationGlobalSettings, Team } from '@prisma/client';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
@ -15,9 +15,12 @@ import { createTokenVerification } from '@documenso/lib/utils/token-verification
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
||||
import { env } from '../../utils/env';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getTeamSettings } from './get-team-settings';
|
||||
|
||||
export type CreateTeamEmailVerificationOptions = {
|
||||
userId: number;
|
||||
@ -34,33 +37,27 @@ export const createTeamEmailVerification = async ({
|
||||
data,
|
||||
}: CreateTeamEmailVerificationOptions): Promise<void> => {
|
||||
try {
|
||||
const settings = await getTeamSettings({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const team = await prisma.team.findFirstOrThrow({
|
||||
where: buildTeamWhereQuery(teamId, userId, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM']),
|
||||
include: {
|
||||
teamEmail: true,
|
||||
emailVerification: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (team.teamEmail || team.emailVerification) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Team already has an email or existing email verification.',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const team = await tx.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
role: {
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
teamEmail: true,
|
||||
emailVerification: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (team.teamEmail || team.emailVerification) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Team already has an email or existing email verification.',
|
||||
});
|
||||
}
|
||||
|
||||
const existingTeamEmail = await tx.teamEmail.findFirst({
|
||||
where: {
|
||||
email: data.email,
|
||||
@ -85,7 +82,7 @@ export const createTeamEmailVerification = async ({
|
||||
},
|
||||
});
|
||||
|
||||
await sendTeamEmailVerificationEmail(data.email, token, team);
|
||||
await sendTeamEmailVerificationEmail(data.email, token, team, settings);
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
@ -119,9 +116,8 @@ export const createTeamEmailVerification = async ({
|
||||
export const sendTeamEmailVerificationEmail = async (
|
||||
email: string,
|
||||
token: string,
|
||||
team: Team & {
|
||||
teamGlobalSettings?: TeamGlobalSettings | null;
|
||||
},
|
||||
team: Team,
|
||||
settings: Omit<OrganisationGlobalSettings, 'id'>,
|
||||
) => {
|
||||
const assetBaseUrl = env('NEXT_PUBLIC_WEBAPP_URL') || 'http://localhost:3000';
|
||||
|
||||
@ -133,11 +129,10 @@ export const sendTeamEmailVerificationEmail = async (
|
||||
token,
|
||||
});
|
||||
|
||||
const branding = team.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
|
||||
: undefined;
|
||||
const branding = teamGlobalSettingsToBranding(settings, team.id);
|
||||
|
||||
const lang = team.teamGlobalSettings?.documentLanguage;
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const lang = settings.documentLanguage as SupportedLanguageCodes;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
|
||||
@ -1,190 +0,0 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import type { Team, TeamGlobalSettings } from '@prisma/client';
|
||||
import { TeamMemberInviteStatus } from '@prisma/client';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { TeamInviteEmailTemplate } from '@documenso/email/templates/team-invite';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { TCreateTeamMemberInvitesMutationSchema } from '@documenso/trpc/server/team-router/schema';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
|
||||
export type CreateTeamMemberInvitesOptions = {
|
||||
userId: number;
|
||||
userName: string;
|
||||
teamId: number;
|
||||
invitations: TCreateTeamMemberInvitesMutationSchema['invitations'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Invite team members via email to join a team.
|
||||
*/
|
||||
export const createTeamMemberInvites = async ({
|
||||
userId,
|
||||
userName,
|
||||
teamId,
|
||||
invitations,
|
||||
}: CreateTeamMemberInvitesOptions): Promise<void> => {
|
||||
const team = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
role: {
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
select: {
|
||||
role: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
invites: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
const teamMemberEmails = team.members.map((member) => member.user.email);
|
||||
const teamMemberInviteEmails = team.invites.map((invite) => invite.email);
|
||||
const currentTeamMember = team.members.find((member) => member.user.id === userId);
|
||||
|
||||
if (!currentTeamMember) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'User not part of team.',
|
||||
});
|
||||
}
|
||||
|
||||
const usersToInvite = invitations.filter((invitation) => {
|
||||
// Filter out users that are already members of the team.
|
||||
if (teamMemberEmails.includes(invitation.email)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter out users that have already been invited to the team.
|
||||
if (teamMemberInviteEmails.includes(invitation.email)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const unauthorizedRoleAccess = usersToInvite.some(
|
||||
({ role }) => !isTeamRoleWithinUserHierarchy(currentTeamMember.role, role),
|
||||
);
|
||||
|
||||
if (unauthorizedRoleAccess) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'User does not have permission to set high level roles',
|
||||
});
|
||||
}
|
||||
|
||||
const teamMemberInvites = usersToInvite.map(({ email, role }) => ({
|
||||
email,
|
||||
teamId,
|
||||
role,
|
||||
status: TeamMemberInviteStatus.PENDING,
|
||||
token: nanoid(32),
|
||||
}));
|
||||
|
||||
await prisma.teamMemberInvite.createMany({
|
||||
data: teamMemberInvites,
|
||||
});
|
||||
|
||||
const sendEmailResult = await Promise.allSettled(
|
||||
teamMemberInvites.map(async ({ email, token }) =>
|
||||
sendTeamMemberInviteEmail({
|
||||
email,
|
||||
token,
|
||||
team,
|
||||
senderName: userName,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const sendEmailResultErrorList = sendEmailResult.filter(
|
||||
(result): result is PromiseRejectedResult => result.status === 'rejected',
|
||||
);
|
||||
|
||||
if (sendEmailResultErrorList.length > 0) {
|
||||
console.error(JSON.stringify(sendEmailResultErrorList));
|
||||
|
||||
throw new AppError('EmailDeliveryFailed', {
|
||||
message: 'Failed to send invite emails to one or more users.',
|
||||
userMessage: `Failed to send invites to ${sendEmailResultErrorList.length}/${teamMemberInvites.length} users.`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
type SendTeamMemberInviteEmailOptions = {
|
||||
email: string;
|
||||
senderName: string;
|
||||
token: string;
|
||||
team: Team & {
|
||||
teamGlobalSettings?: TeamGlobalSettings | null;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Send an email to a user inviting them to join a team.
|
||||
*/
|
||||
export const sendTeamMemberInviteEmail = async ({
|
||||
email,
|
||||
senderName,
|
||||
token,
|
||||
team,
|
||||
}: SendTeamMemberInviteEmailOptions) => {
|
||||
const template = createElement(TeamInviteEmailTemplate, {
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
senderName,
|
||||
token,
|
||||
teamName: team.name,
|
||||
teamUrl: team.url,
|
||||
});
|
||||
|
||||
const branding = team.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: team.teamGlobalSettings?.documentLanguage, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: team.teamGlobalSettings?.documentLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(team.teamGlobalSettings?.documentLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: email,
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(msg`You have been invited to join ${team.name} on Documenso`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
};
|
||||
@ -1,8 +1,14 @@
|
||||
import { Prisma, TeamMemberRole } from '@prisma/client';
|
||||
import {
|
||||
OrganisationGroupType,
|
||||
OrganisationMemberRole,
|
||||
Prisma,
|
||||
TeamMemberRole,
|
||||
} from '@prisma/client';
|
||||
import type Stripe from 'stripe';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTeamCustomer } from '@documenso/ee/server-only/stripe/create-team-customer';
|
||||
import { createOrganisationCustomer } from '@documenso/ee/server-only/stripe/create-team-customer';
|
||||
import { getTeamRelatedPrices } from '@documenso/ee/server-only/stripe/get-team-related-prices';
|
||||
import { mapStripeSubscriptionToPrismaUpsertAction } from '@documenso/ee/server-only/stripe/webhook/on-subscription-updated';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
@ -10,6 +16,13 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import {
|
||||
LOWEST_ORGANISATION_ROLE,
|
||||
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP,
|
||||
} from '../../constants/organisations';
|
||||
import { TEAM_INTERNAL_GROUPS } from '../../constants/teams';
|
||||
import { buildOrganisationWhereQuery } from '../../utils/organisations';
|
||||
import { generateDefaultTeamSettings } from '../../utils/teams';
|
||||
import { stripe } from '../stripe';
|
||||
|
||||
export type CreateTeamOptions = {
|
||||
@ -29,6 +42,24 @@ export type CreateTeamOptions = {
|
||||
* Used as the URL path, example: https://documenso.com/t/{teamUrl}/settings
|
||||
*/
|
||||
teamUrl: string;
|
||||
|
||||
/**
|
||||
* ID of the organisation the team belongs to.
|
||||
*/
|
||||
organisationId: string;
|
||||
|
||||
/**
|
||||
* Whether to inherit all members from the organisation.
|
||||
*/
|
||||
inheritMembers: boolean;
|
||||
|
||||
/**
|
||||
* List of additional groups to attach to the team.
|
||||
*/
|
||||
groups?: {
|
||||
id: string;
|
||||
role: TeamMemberRole;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const ZCreateTeamResponseSchema = z.union([
|
||||
@ -50,16 +81,123 @@ export const createTeam = async ({
|
||||
userId,
|
||||
teamName,
|
||||
teamUrl,
|
||||
organisationId,
|
||||
inheritMembers,
|
||||
}: CreateTeamOptions): Promise<TCreateTeamResponse> => {
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery(
|
||||
organisationId,
|
||||
userId,
|
||||
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
),
|
||||
include: {
|
||||
groups: true, // Todo: (orgs)
|
||||
subscriptions: true,
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Organisation not found.',
|
||||
});
|
||||
}
|
||||
|
||||
// Inherit internal organisation groups to the team.
|
||||
// Organisation Admins/Mangers get assigned as team admins, members get assigned as team members.
|
||||
const internalOrganisationGroups = organisation.groups
|
||||
.filter((group) => {
|
||||
if (group.type !== OrganisationGroupType.INTERNAL_ORGANISATION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we're inheriting members, allow all internal organisation groups.
|
||||
if (inheritMembers) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise, only inherit organisation admins/managers.
|
||||
return (
|
||||
group.organisationRole === OrganisationMemberRole.ADMIN ||
|
||||
group.organisationRole === OrganisationMemberRole.MANAGER
|
||||
);
|
||||
})
|
||||
.map((group) =>
|
||||
match(group.organisationRole)
|
||||
.with(OrganisationMemberRole.ADMIN, OrganisationMemberRole.MANAGER, () => ({
|
||||
organisationGroupId: group.id,
|
||||
teamRole: TeamMemberRole.ADMIN,
|
||||
}))
|
||||
.with(OrganisationMemberRole.MEMBER, () => ({
|
||||
organisationGroupId: group.id,
|
||||
teamRole: TeamMemberRole.MEMBER,
|
||||
}))
|
||||
.exhaustive(),
|
||||
);
|
||||
|
||||
console.log({
|
||||
internalOrganisationGroups,
|
||||
});
|
||||
|
||||
if (Date.now() > 0) {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const teamSettings = await tx.teamGlobalSettings.create({
|
||||
data: generateDefaultTeamSettings(),
|
||||
});
|
||||
|
||||
const team = await tx.team.create({
|
||||
data: {
|
||||
name: teamName,
|
||||
url: teamUrl,
|
||||
organisationId,
|
||||
teamGlobalSettingsId: teamSettings.id,
|
||||
teamGroups: {
|
||||
createMany: {
|
||||
// Attach the internal organisation groups to the team.
|
||||
data: internalOrganisationGroups,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
teamGroups: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Create the internal team groups.
|
||||
await Promise.all(
|
||||
TEAM_INTERNAL_GROUPS.map(async (teamGroup) =>
|
||||
tx.organisationGroup.create({
|
||||
data: {
|
||||
type: teamGroup.type,
|
||||
organisationRole: LOWEST_ORGANISATION_ROLE,
|
||||
organisationId,
|
||||
teamGroups: {
|
||||
create: {
|
||||
teamId: team.id,
|
||||
teamRole: teamGroup.teamRole,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
paymentRequired: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (Date.now() > 0) {
|
||||
throw new Error('Todo: Orgs');
|
||||
}
|
||||
|
||||
let isPaymentRequired = IS_BILLING_ENABLED();
|
||||
let customerId: string | null = null;
|
||||
|
||||
@ -68,59 +206,46 @@ export const createTeam = async ({
|
||||
prices.map((price) => price.id),
|
||||
);
|
||||
|
||||
isPaymentRequired = !subscriptionsContainsActivePlan(user.subscriptions, teamRelatedPriceIds);
|
||||
isPaymentRequired = !subscriptionsContainsActivePlan(
|
||||
organisation.subscriptions,
|
||||
teamRelatedPriceIds, // Todo: (orgs)
|
||||
);
|
||||
|
||||
customerId = await createTeamCustomer({
|
||||
name: user.name ?? teamName,
|
||||
email: user.email,
|
||||
customerId = await createOrganisationCustomer({
|
||||
name: organisation.owner.name ?? teamName,
|
||||
email: organisation.owner.email,
|
||||
}).then((customer) => customer.id);
|
||||
|
||||
await prisma.organisation.update({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
data: {
|
||||
customerId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Create the team directly if no payment is required.
|
||||
if (!isPaymentRequired) {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const existingUserProfileWithUrl = await tx.user.findUnique({
|
||||
where: {
|
||||
url: teamUrl,
|
||||
await prisma.team.create({
|
||||
data: {
|
||||
name: teamName,
|
||||
url: teamUrl,
|
||||
organisationId,
|
||||
members: {
|
||||
create: [
|
||||
{
|
||||
userId,
|
||||
role: TeamMemberRole.ADMIN, // Todo: (orgs)
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
teamGlobalSettings: {
|
||||
create: {},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUserProfileWithUrl) {
|
||||
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
|
||||
message: 'URL already taken.',
|
||||
});
|
||||
}
|
||||
|
||||
const team = await tx.team.create({
|
||||
data: {
|
||||
name: teamName,
|
||||
url: teamUrl,
|
||||
ownerUserId: user.id,
|
||||
customerId,
|
||||
members: {
|
||||
create: [
|
||||
{
|
||||
userId: user.id,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await tx.teamGlobalSettings.upsert({
|
||||
where: {
|
||||
teamId: team.id,
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type DeclineTeamInvitationOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const declineTeamInvitation = async ({ userId, teamId }: DeclineTeamInvitationOptions) => {
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const user = await tx.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const teamMemberInvite = await tx.teamMemberInvite.findFirstOrThrow({
|
||||
where: {
|
||||
teamId,
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.teamMemberInvite.delete({
|
||||
where: {
|
||||
id: teamMemberInvite.id,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: notify the team owner
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
};
|
||||
@ -1,6 +1,8 @@
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type DeleteTeamEmailVerificationOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
@ -10,25 +12,13 @@ export const deleteTeamEmailVerification = async ({
|
||||
userId,
|
||||
teamId,
|
||||
}: DeleteTeamEmailVerificationOptions) => {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
role: {
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await prisma.team.findFirstOrThrow({
|
||||
where: buildTeamWhereQuery(teamId, userId, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM']),
|
||||
});
|
||||
|
||||
await tx.teamEmailVerification.delete({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
await prisma.teamEmailVerification.delete({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -13,6 +13,8 @@ import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { env } from '../../utils/env';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getTeamSettings } from './get-team-settings';
|
||||
|
||||
export type DeleteTeamEmailOptions = {
|
||||
userId: number;
|
||||
@ -26,47 +28,42 @@ export type DeleteTeamEmailOptions = {
|
||||
* The user must either be part of the team with the required permissions, or the owner of the email.
|
||||
*/
|
||||
export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamEmailOptions) => {
|
||||
const team = await prisma.$transaction(async (tx) => {
|
||||
const foundTeam = await tx.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
OR: [
|
||||
{
|
||||
teamEmail: {
|
||||
email: userEmail,
|
||||
},
|
||||
},
|
||||
{
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
role: {
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
include: {
|
||||
teamEmail: true,
|
||||
owner: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
const settings = await getTeamSettings({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const team = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
OR: [
|
||||
buildTeamWhereQuery(teamId, userId, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM']),
|
||||
{
|
||||
id: teamId,
|
||||
teamEmail: {
|
||||
email: userEmail,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
include: {
|
||||
teamEmail: true,
|
||||
organisation: {
|
||||
select: {
|
||||
owner: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await tx.teamEmail.delete({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
return foundTeam;
|
||||
await prisma.teamEmail.delete({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
@ -80,11 +77,8 @@ export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamE
|
||||
teamUrl: team.url,
|
||||
});
|
||||
|
||||
const branding = team.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const lang = team.teamGlobalSettings?.documentLanguage;
|
||||
const branding = teamGlobalSettingsToBranding(settings, team.id);
|
||||
const lang = settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
@ -95,8 +89,8 @@ export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamE
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: team.owner.email,
|
||||
name: team.owner.name ?? '',
|
||||
address: team.organisation.owner.email,
|
||||
name: team.organisation.owner.name ?? '',
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||
|
||||
export type DeleteTeamMemberInvitationsOptions = {
|
||||
/**
|
||||
* The ID of the user who is initiating this action.
|
||||
*/
|
||||
userId: number;
|
||||
|
||||
/**
|
||||
* The ID of the team to remove members from.
|
||||
*/
|
||||
teamId: number;
|
||||
|
||||
/**
|
||||
* The IDs of the invitations to remove.
|
||||
*/
|
||||
invitationIds: number[];
|
||||
};
|
||||
|
||||
export const deleteTeamMemberInvitations = async ({
|
||||
userId,
|
||||
teamId,
|
||||
invitationIds,
|
||||
}: DeleteTeamMemberInvitationsOptions) => {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.teamMember.findFirstOrThrow({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
role: {
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await tx.teamMemberInvite.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: invitationIds,
|
||||
},
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -22,6 +22,7 @@ export type DeleteTeamMembersOptions = {
|
||||
teamMemberIds: number[];
|
||||
};
|
||||
|
||||
// Todo: orgs (we curretnly have an implementation already, need to make it backwards compatible)
|
||||
export const deleteTeamMembers = async ({
|
||||
userId,
|
||||
teamId,
|
||||
@ -50,7 +51,6 @@ export const deleteTeamMembers = async ({
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
subscription: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||
|
||||
export type DeleteTeamTransferRequestOptions = {
|
||||
/**
|
||||
* The ID of the user deleting the transfer.
|
||||
*/
|
||||
userId: number;
|
||||
|
||||
/**
|
||||
* The ID of the team whose team transfer request should be deleted.
|
||||
*/
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const deleteTeamTransferRequest = async ({
|
||||
userId,
|
||||
teamId,
|
||||
}: DeleteTeamTransferRequestOptions) => {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
role: {
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['DELETE_TEAM_TRANSFER_REQUEST'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await tx.teamTransferVerification.delete({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -1,20 +1,22 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import type { Team, TeamGlobalSettings } from '@prisma/client';
|
||||
import type { OrganisationGlobalSettings } from '@prisma/client';
|
||||
import { OrganisationGroupType, type Team } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { TeamDeleteEmailTemplate } from '@documenso/email/templates/team-delete';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { jobs } from '../../jobs/client';
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getTeamSettings } from './get-team-settings';
|
||||
|
||||
export type DeleteTeamOptions = {
|
||||
userId: number;
|
||||
@ -22,65 +24,97 @@ export type DeleteTeamOptions = {
|
||||
};
|
||||
|
||||
export const deleteTeam = async ({ userId, teamId }: DeleteTeamOptions) => {
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const team = await tx.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
ownerUserId: userId,
|
||||
},
|
||||
// Todo: orgs double check this.
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery(teamId, userId, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['DELETE_TEAM']),
|
||||
include: {
|
||||
teamGroups: {
|
||||
include: {
|
||||
subscription: true,
|
||||
members: {
|
||||
organisationGroup: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
organisationGroupMembers: {
|
||||
include: {
|
||||
organisationMember: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (team.subscription) {
|
||||
await stripe.subscriptions
|
||||
.cancel(team.subscription.planId, {
|
||||
prorate: false,
|
||||
invoice_now: true,
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
throw AppError.parseError(err);
|
||||
});
|
||||
}
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You are not authorized to delete this team',
|
||||
});
|
||||
}
|
||||
|
||||
await jobs.triggerJob({
|
||||
name: 'send.team-deleted.email',
|
||||
payload: {
|
||||
team: {
|
||||
name: team.name,
|
||||
url: team.url,
|
||||
ownerUserId: team.ownerUserId,
|
||||
teamGlobalSettings: team.teamGlobalSettings,
|
||||
},
|
||||
members: team.members.map((member) => ({
|
||||
id: member.user.id,
|
||||
name: member.user.name || '',
|
||||
email: member.user.email,
|
||||
})),
|
||||
},
|
||||
});
|
||||
const settings = await getTeamSettings({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
// Todo: orgs handle any subs?
|
||||
// if (team.subscription) {
|
||||
// await stripe.subscriptions
|
||||
// .cancel(team.subscription.planId, {
|
||||
// prorate: false,
|
||||
// invoice_now: true,
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// console.error(err);
|
||||
// throw AppError.parseError(err);
|
||||
// });
|
||||
// }
|
||||
|
||||
await tx.team.delete({
|
||||
where: {
|
||||
id: teamId,
|
||||
ownerUserId: userId,
|
||||
},
|
||||
});
|
||||
|
||||
// Purge all internal organisation groups that have no teams.
|
||||
await tx.organisationGroup.deleteMany({
|
||||
where: {
|
||||
type: OrganisationGroupType.INTERNAL_TEAM,
|
||||
teamGroups: {
|
||||
none: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// const members = team.teamGroups.flatMap((group) =>
|
||||
// group.organisationGroup.organisationMembers.map((member) => ({
|
||||
// id: member.user.id,
|
||||
// name: member.user.name || '',
|
||||
// email: member.user.email,
|
||||
// })),
|
||||
// );
|
||||
|
||||
// await jobs.triggerJob({
|
||||
// name: 'send.team-deleted.email',
|
||||
// payload: {
|
||||
// team: {
|
||||
// name: team.name,
|
||||
// url: team.url,
|
||||
// teamGlobalSettings: team.teamGlobalSettings, // Todo: orgs
|
||||
// },
|
||||
// members,
|
||||
// },
|
||||
// });
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
@ -88,25 +122,24 @@ export const deleteTeam = async ({ userId, teamId }: DeleteTeamOptions) => {
|
||||
|
||||
type SendTeamDeleteEmailOptions = {
|
||||
email: string;
|
||||
team: Pick<Team, 'url' | 'name'> & {
|
||||
teamGlobalSettings?: TeamGlobalSettings | null;
|
||||
};
|
||||
isOwner: boolean;
|
||||
team: Pick<Team, 'id' | 'url' | 'name'>;
|
||||
settings: Omit<OrganisationGlobalSettings, 'id'>;
|
||||
};
|
||||
|
||||
export const sendTeamDeleteEmail = async ({ email, isOwner, team }: SendTeamDeleteEmailOptions) => {
|
||||
export const sendTeamDeleteEmail = async ({
|
||||
email,
|
||||
team,
|
||||
settings,
|
||||
}: SendTeamDeleteEmailOptions) => {
|
||||
const template = createElement(TeamDeleteEmailTemplate, {
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
teamUrl: team.url,
|
||||
isOwner,
|
||||
});
|
||||
|
||||
const branding = team.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
|
||||
: undefined;
|
||||
const branding = teamGlobalSettingsToBranding(settings, team.id);
|
||||
|
||||
const lang = team.teamGlobalSettings?.documentLanguage;
|
||||
const lang = settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
|
||||
@ -8,7 +8,7 @@ export interface FindTeamInvoicesOptions {
|
||||
teamId: number;
|
||||
}
|
||||
|
||||
export const findTeamInvoices = async ({ userId, teamId }: FindTeamInvoicesOptions) => {
|
||||
export const findOrganisationInvoices = async ({ userId, teamId }: FindTeamInvoicesOptions) => {
|
||||
const team = await prisma.team.findUniqueOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
@ -1,105 +0,0 @@
|
||||
import type { TeamMemberInvite } from '@prisma/client';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { P, match } from 'ts-pattern';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberInviteSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamMemberInviteSchema';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||
import { type FindResultResponse, ZFindResultResponse } from '../../types/search-params';
|
||||
|
||||
export interface FindTeamMemberInvitesOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
orderBy?: {
|
||||
column: keyof TeamMemberInvite;
|
||||
direction: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
|
||||
export const ZFindTeamMemberInvitesResponseSchema = ZFindResultResponse.extend({
|
||||
data: TeamMemberInviteSchema.pick({
|
||||
id: true,
|
||||
teamId: true,
|
||||
email: true,
|
||||
role: true,
|
||||
createdAt: true,
|
||||
}).array(),
|
||||
});
|
||||
|
||||
export type TFindTeamMemberInvitesResponse = z.infer<typeof ZFindTeamMemberInvitesResponseSchema>;
|
||||
|
||||
export const findTeamMemberInvites = async ({
|
||||
userId,
|
||||
teamId,
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
orderBy,
|
||||
}: FindTeamMemberInvitesOptions): Promise<TFindTeamMemberInvitesResponse> => {
|
||||
const orderByColumn = orderBy?.column ?? 'email';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
|
||||
// Check that the user belongs to the team they are trying to find invites in.
|
||||
const userTeam = await prisma.team.findUniqueOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
role: {
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const termFilters: Prisma.TeamMemberInviteWhereInput | undefined = match(query)
|
||||
.with(P.string.minLength(1), () => ({
|
||||
email: {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
}))
|
||||
.otherwise(() => undefined);
|
||||
|
||||
const whereClause: Prisma.TeamMemberInviteWhereInput = {
|
||||
...termFilters,
|
||||
teamId: userTeam.id,
|
||||
};
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.teamMemberInvite.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
[orderByColumn]: orderByDirection,
|
||||
},
|
||||
// Exclude token attribute.
|
||||
select: {
|
||||
id: true,
|
||||
teamId: true,
|
||||
email: true,
|
||||
role: true,
|
||||
createdAt: true,
|
||||
},
|
||||
}),
|
||||
prisma.teamMemberInvite.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
@ -1,14 +1,13 @@
|
||||
import type { TeamMember } from '@prisma/client';
|
||||
import type { OrganisationMember } from '@prisma/client';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { P, match } from 'ts-pattern';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamMemberSchema';
|
||||
import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
import { ZFindResultResponse } from '../../types/search-params';
|
||||
import { getHighestOrganisationRoleInGroup } from '../../utils/organisations';
|
||||
import { getHighestTeamRoleInGroup } from '../../utils/teams';
|
||||
|
||||
export interface FindTeamMembersOptions {
|
||||
userId: number;
|
||||
@ -17,22 +16,11 @@ export interface FindTeamMembersOptions {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
orderBy?: {
|
||||
column: keyof TeamMember | 'name';
|
||||
column: keyof OrganisationMember | 'name';
|
||||
direction: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
|
||||
export const ZFindTeamMembersResponseSchema = ZFindResultResponse.extend({
|
||||
data: TeamMemberSchema.extend({
|
||||
user: UserSchema.pick({
|
||||
name: true,
|
||||
email: true,
|
||||
}),
|
||||
}).array(),
|
||||
});
|
||||
|
||||
export type TFindTeamMembersResponse = z.infer<typeof ZFindTeamMembersResponseSchema>;
|
||||
|
||||
export const findTeamMembers = async ({
|
||||
userId,
|
||||
teamId,
|
||||
@ -40,39 +28,69 @@ export const findTeamMembers = async ({
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
orderBy,
|
||||
}: FindTeamMembersOptions): Promise<TFindTeamMembersResponse> => {
|
||||
}: FindTeamMembersOptions) => {
|
||||
const orderByColumn = orderBy?.column ?? 'name';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
|
||||
// Check that the user belongs to the team they are trying to find members in.
|
||||
const userTeam = await prisma.team.findUniqueOrThrow({
|
||||
const userTeam = await prisma.organisationMember.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
userId,
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
userId,
|
||||
group: {
|
||||
teamGroups: {
|
||||
some: {
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const termFilters: Prisma.TeamMemberWhereInput | undefined = match(query)
|
||||
if (!userTeam) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
const termFilters: Prisma.OrganisationMemberWhereInput | undefined = match(query)
|
||||
.with(P.string.minLength(1), () => ({
|
||||
user: {
|
||||
name: {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
OR: [
|
||||
{
|
||||
name: {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
{
|
||||
email: {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}))
|
||||
.otherwise(() => undefined);
|
||||
|
||||
const whereClause: Prisma.TeamMemberWhereInput = {
|
||||
const whereClause: Prisma.OrganisationMemberWhereInput = {
|
||||
...termFilters,
|
||||
teamId: userTeam.id,
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
group: {
|
||||
teamGroups: {
|
||||
some: {
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let orderByClause: Prisma.TeamMemberOrderByWithRelationInput = {
|
||||
let orderByClause: Prisma.OrganisationMemberOrderByWithRelationInput = {
|
||||
[orderByColumn]: orderByDirection,
|
||||
};
|
||||
|
||||
@ -86,7 +104,7 @@ export const findTeamMembers = async ({
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.teamMember.findMany({
|
||||
prisma.organisationMember.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
@ -94,22 +112,51 @@ export const findTeamMembers = async ({
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
avatarImageId: true,
|
||||
},
|
||||
},
|
||||
organisationGroupMembers: {
|
||||
include: {
|
||||
group: {
|
||||
include: {
|
||||
teamGroups: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.teamMember.count({
|
||||
prisma.organisationMember.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
// same as get-team-members.
|
||||
const mappedData = data.map((member) => ({
|
||||
id: member.id,
|
||||
userId: member.userId,
|
||||
createdAt: member.createdAt,
|
||||
email: member.user.email,
|
||||
name: member.user.name,
|
||||
avatarImageId: member.user.avatarImageId,
|
||||
// Todo: orgs test this
|
||||
teamRole: getHighestTeamRoleInGroup(
|
||||
member.organisationGroupMembers.flatMap(({ group }) => group.teamGroups),
|
||||
),
|
||||
teamRoleGroupType: member.organisationGroupMembers[0].group.type,
|
||||
organisationRole: getHighestOrganisationRoleInGroup(
|
||||
member.organisationGroupMembers.flatMap(({ group }) => group),
|
||||
),
|
||||
}));
|
||||
|
||||
return {
|
||||
data,
|
||||
data: mappedData,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
} satisfies FindResultResponse<typeof mappedData>;
|
||||
};
|
||||
|
||||
@ -7,6 +7,7 @@ import type { FindResultResponse } from '../../types/search-params';
|
||||
|
||||
export interface FindTeamsOptions {
|
||||
userId: number;
|
||||
organisationId: string;
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
@ -18,6 +19,7 @@ export interface FindTeamsOptions {
|
||||
|
||||
export const findTeams = async ({
|
||||
userId,
|
||||
organisationId,
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
@ -27,9 +29,20 @@ export const findTeams = async ({
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
|
||||
const whereClause: Prisma.TeamWhereInput = {
|
||||
members: {
|
||||
organisation: {
|
||||
id: organisationId,
|
||||
},
|
||||
teamGroups: {
|
||||
some: {
|
||||
userId,
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -50,9 +63,27 @@ export const findTeams = async ({
|
||||
[orderByColumn]: orderByDirection,
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
teamGroups: {
|
||||
include: {
|
||||
organisationGroup: {
|
||||
include: {
|
||||
organisationGroupMembers: {
|
||||
include: {
|
||||
organisationMember: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -62,10 +93,9 @@ export const findTeams = async ({
|
||||
}),
|
||||
]);
|
||||
|
||||
// Todo: Orgs nested group membesr thing
|
||||
const maskedData = data.map((team) => ({
|
||||
...team,
|
||||
currentTeamMember: team.members[0],
|
||||
members: undefined,
|
||||
}));
|
||||
|
||||
return {
|
||||
|
||||
128
packages/lib/server-only/team/get-member-roles.ts
Normal file
128
packages/lib/server-only/team/get-member-roles.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { getHighestOrganisationRoleInGroup } from '../../utils/organisations';
|
||||
import { getHighestTeamRoleInGroup } from '../../utils/teams';
|
||||
|
||||
type GetMemberRolesOptions = {
|
||||
teamId: number;
|
||||
reference:
|
||||
| {
|
||||
type: 'User';
|
||||
id: number;
|
||||
}
|
||||
| {
|
||||
type: 'Member';
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the highest Organisation and Team role of a given member or user of a team
|
||||
*/
|
||||
export const getMemberRoles = async ({ teamId, reference }: GetMemberRolesOptions) => {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
include: {
|
||||
teamGroups: {
|
||||
where: {
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember:
|
||||
reference.type === 'User'
|
||||
? {
|
||||
userId: reference.id,
|
||||
}
|
||||
: {
|
||||
id: reference.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organisationGroup: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Roles not found',
|
||||
});
|
||||
}
|
||||
|
||||
console.log({
|
||||
groups: team.teamGroups,
|
||||
roles: {
|
||||
organisationRole: getHighestOrganisationRoleInGroup(
|
||||
team.teamGroups.flatMap((group) => group.organisationGroup),
|
||||
),
|
||||
teamRole: getHighestTeamRoleInGroup(team.teamGroups),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
organisationRole: getHighestOrganisationRoleInGroup(
|
||||
team.teamGroups.flatMap((group) => group.organisationGroup),
|
||||
),
|
||||
teamRole: getHighestTeamRoleInGroup(team.teamGroups),
|
||||
};
|
||||
};
|
||||
|
||||
type GetMemberOrganisationRoleOptions = {
|
||||
organisationId: string;
|
||||
reference:
|
||||
| {
|
||||
type: 'User';
|
||||
id: number;
|
||||
}
|
||||
| {
|
||||
type: 'Member';
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the highest Organisation of a given organisation member
|
||||
*/
|
||||
export const getMemberOrganisationRole = async ({
|
||||
organisationId,
|
||||
reference,
|
||||
}: GetMemberOrganisationRoleOptions) => {
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
include: {
|
||||
groups: {
|
||||
where: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember:
|
||||
reference.type === 'User'
|
||||
? {
|
||||
userId: reference.id,
|
||||
}
|
||||
: {
|
||||
id: reference.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Roles not found',
|
||||
});
|
||||
}
|
||||
|
||||
return getHighestOrganisationRoleInGroup(organisation.groups);
|
||||
};
|
||||
@ -1,40 +0,0 @@
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberInviteSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamMemberInviteSchema';
|
||||
import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
|
||||
export type GetTeamInvitationsOptions = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
export const ZGetTeamInvitationsResponseSchema = TeamMemberInviteSchema.extend({
|
||||
team: TeamSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
avatarImageId: true,
|
||||
}),
|
||||
}).array();
|
||||
|
||||
export type TGetTeamInvitationsResponse = z.infer<typeof ZGetTeamInvitationsResponseSchema>;
|
||||
|
||||
export const getTeamInvitations = async ({
|
||||
email,
|
||||
}: GetTeamInvitationsOptions): Promise<TGetTeamInvitationsResponse> => {
|
||||
return await prisma.teamMemberInvite.findMany({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
avatarImageId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,38 +1,32 @@
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamMemberSchema';
|
||||
import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
||||
import type { TGetTeamMembersResponse } from '@documenso/trpc/server/team-router/get-team-members.types';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { getHighestOrganisationRoleInGroup } from '../../utils/organisations';
|
||||
import { getHighestTeamRoleInGroup } from '../../utils/teams';
|
||||
|
||||
export type GetTeamMembersOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const ZGetTeamMembersResponseSchema = TeamMemberSchema.extend({
|
||||
user: UserSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
}),
|
||||
}).array();
|
||||
|
||||
export type TGetTeamMembersResponseSchema = z.infer<typeof ZGetTeamMembersResponseSchema>;
|
||||
|
||||
/**
|
||||
* Get all team members for a given team.
|
||||
*/
|
||||
export const getTeamMembers = async ({
|
||||
userId,
|
||||
teamId,
|
||||
}: GetTeamMembersOptions): Promise<TGetTeamMembersResponseSchema> => {
|
||||
return await prisma.teamMember.findMany({
|
||||
}: GetTeamMembersOptions): Promise<TGetTeamMembersResponse> => {
|
||||
const teamMembers = await prisma.organisationMember.findMany({
|
||||
where: {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId: userId,
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
group: {
|
||||
teamGroups: {
|
||||
some: {
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -43,8 +37,41 @@ export const getTeamMembers = async ({
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
avatarImageId: true,
|
||||
},
|
||||
},
|
||||
organisationGroupMembers: {
|
||||
include: {
|
||||
group: {
|
||||
include: {
|
||||
teamGroups: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const isAuthorized = teamMembers.some((member) => member.userId === userId);
|
||||
|
||||
// Checks that the user is part of the organisation/team.
|
||||
if (!isAuthorized) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
return teamMembers.map((member) => {
|
||||
const memberGroups = member.organisationGroupMembers.map((group) => group.group);
|
||||
|
||||
return {
|
||||
id: member.id,
|
||||
userId: member.userId,
|
||||
createdAt: member.createdAt,
|
||||
email: member.user.email,
|
||||
name: member.user.name,
|
||||
avatarImageId: member.user.avatarImageId,
|
||||
// Todo: orgs test this
|
||||
teamRole: getHighestTeamRoleInGroup(memberGroups.flatMap((group) => group.teamGroups)),
|
||||
organisationRole: getHighestOrganisationRoleInGroup(memberGroups),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@ -3,6 +3,7 @@ import type { TeamProfile } from '@prisma/client';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { updateTeamPublicProfile } from './update-team-public-profile';
|
||||
|
||||
export type GetTeamPublicProfileOptions = {
|
||||
@ -20,14 +21,7 @@ export const getTeamPublicProfile = async ({
|
||||
teamId,
|
||||
}: GetTeamPublicProfileOptions): Promise<GetTeamPublicProfileResponse> => {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: buildTeamWhereQuery(teamId, userId),
|
||||
include: {
|
||||
profile: true,
|
||||
},
|
||||
|
||||
37
packages/lib/server-only/team/get-team-settings.ts
Normal file
37
packages/lib/server-only/team/get-team-settings.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery, extractDerivedTeamSettings } from '../../utils/teams';
|
||||
|
||||
export type GetTeamSettingsOptions = {
|
||||
userId?: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* You must provide userId if you want to validate whether the user can access the team settings.
|
||||
*/
|
||||
export const getTeamSettings = async ({ userId, teamId }: GetTeamSettingsOptions) => {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: userId !== undefined ? buildTeamWhereQuery(teamId, userId) : { id: teamId },
|
||||
include: {
|
||||
organisation: {
|
||||
include: {
|
||||
organisationGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found',
|
||||
});
|
||||
}
|
||||
|
||||
const organisationSettings = team.organisation.organisationGlobalSettings;
|
||||
const teamSettings = team.teamGlobalSettings;
|
||||
|
||||
return extractDerivedTeamSettings(organisationSettings, teamSettings);
|
||||
};
|
||||
@ -1,71 +1,61 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import type { z } from 'zod';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamEmailSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamEmailSchema';
|
||||
import { TeamGlobalSettingsSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamGlobalSettingsSchema';
|
||||
import { TeamMemberSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamMemberSchema';
|
||||
import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery, getHighestTeamRoleInGroup } from '../../utils/teams';
|
||||
|
||||
export type GetTeamByIdOptions = {
|
||||
userId?: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const ZGetTeamByIdResponseSchema = TeamSchema.extend({
|
||||
teamEmail: TeamEmailSchema.nullable(),
|
||||
teamGlobalSettings: TeamGlobalSettingsSchema.nullable(),
|
||||
currentTeamMember: TeamMemberSchema.pick({
|
||||
role: true,
|
||||
}).nullable(),
|
||||
currentTeamRole: z.nativeEnum(TeamMemberRole),
|
||||
});
|
||||
|
||||
export type TGetTeamByIdResponse = z.infer<typeof ZGetTeamByIdResponseSchema>;
|
||||
|
||||
/**
|
||||
* Get a team given a teamId.
|
||||
*
|
||||
* Provide an optional userId to check that the user is a member of the team.
|
||||
*/
|
||||
export const getTeamById = async ({
|
||||
userId,
|
||||
teamId,
|
||||
}: GetTeamByIdOptions): Promise<TGetTeamByIdResponse> => {
|
||||
const whereFilter: Prisma.TeamWhereUniqueInput = {
|
||||
id: teamId,
|
||||
};
|
||||
|
||||
if (userId !== undefined) {
|
||||
whereFilter['members'] = {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const result = await prisma.team.findUniqueOrThrow({
|
||||
where: whereFilter,
|
||||
// Todo: orgs test
|
||||
const result = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery(teamId, userId),
|
||||
include: {
|
||||
teamEmail: true,
|
||||
teamGlobalSettings: true,
|
||||
members: {
|
||||
teamGroups: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { members, ...team } = result;
|
||||
if (!result) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found',
|
||||
});
|
||||
}
|
||||
|
||||
const { teamGroups, ...team } = result;
|
||||
|
||||
return {
|
||||
...team,
|
||||
currentTeamMember: userId !== undefined ? members[0] : null,
|
||||
currentTeamRole: getHighestTeamRoleInGroup(teamGroups),
|
||||
};
|
||||
};
|
||||
|
||||
@ -80,20 +70,23 @@ export type TGetTeamByUrlResponse = Awaited<ReturnType<typeof getTeamByUrl>>;
|
||||
* Get a team given a team URL.
|
||||
*/
|
||||
export const getTeamByUrl = async ({ userId, teamUrl }: GetTeamByUrlOptions) => {
|
||||
const whereFilter: Prisma.TeamWhereUniqueInput = {
|
||||
url: teamUrl,
|
||||
};
|
||||
|
||||
if (userId !== undefined) {
|
||||
whereFilter['members'] = {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const result = await prisma.team.findFirst({
|
||||
where: whereFilter,
|
||||
where: {
|
||||
url: teamUrl,
|
||||
teamGroups: {
|
||||
some: {
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
teamEmail: true,
|
||||
emailVerification: {
|
||||
@ -103,21 +96,17 @@ export const getTeamByUrl = async ({ userId, teamUrl }: GetTeamByUrlOptions) =>
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
transferVerification: {
|
||||
select: {
|
||||
expiresAt: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
subscription: true,
|
||||
teamGlobalSettings: true,
|
||||
members: {
|
||||
teamGroups: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -127,10 +116,10 @@ export const getTeamByUrl = async ({ userId, teamUrl }: GetTeamByUrlOptions) =>
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const { members, ...team } = result;
|
||||
const { teamGroups, ...team } = result;
|
||||
|
||||
return {
|
||||
...team,
|
||||
currentTeamMember: members[0],
|
||||
currentTeamRole: getHighestTeamRoleInGroup(teamGroups),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,53 +1,84 @@
|
||||
import type { z } from 'zod';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { SubscriptionSchema } from '@documenso/prisma/generated/zod/modelSchema/SubscriptionSchema';
|
||||
import { TeamMemberSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamMemberSchema';
|
||||
import { TeamMemberRole } from '@documenso/prisma/generated/types';
|
||||
import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
|
||||
import { getHighestTeamRoleInGroup } from '../../utils/teams';
|
||||
|
||||
export type GetTeamsOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const ZGetTeamsResponseSchema = TeamSchema.extend({
|
||||
currentTeamMember: TeamMemberSchema.pick({
|
||||
role: true,
|
||||
}),
|
||||
subscription: SubscriptionSchema.pick({
|
||||
status: true,
|
||||
}).nullable(),
|
||||
teamRole: z.nativeEnum(TeamMemberRole),
|
||||
}).array();
|
||||
|
||||
export type TGetTeamsResponse = z.infer<typeof ZGetTeamsResponseSchema>;
|
||||
|
||||
export const getTeams = async ({ userId }: GetTeamsOptions): Promise<TGetTeamsResponse> => {
|
||||
const teams = await prisma.team.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
export const getTeams = async ({ userId, teamId }: GetTeamsOptions) => {
|
||||
let whereQuery: Prisma.TeamWhereInput = {
|
||||
teamGroups: {
|
||||
some: {
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
subscription: {
|
||||
select: {
|
||||
status: true,
|
||||
};
|
||||
|
||||
if (teamId) {
|
||||
whereQuery = {
|
||||
teamGroups: {
|
||||
some: {
|
||||
teamId,
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
members: {
|
||||
};
|
||||
}
|
||||
|
||||
const teams = await prisma.team.findMany({
|
||||
where: whereQuery,
|
||||
include: {
|
||||
teamGroups: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return teams.map(({ members, ...team }) => ({
|
||||
...team,
|
||||
currentTeamMember: members[0],
|
||||
}));
|
||||
return teams.map((team) => {
|
||||
const teamRole = getHighestTeamRoleInGroup(team.teamGroups);
|
||||
|
||||
return {
|
||||
...team,
|
||||
teamRole,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
import { updateSubscriptionItemQuantity } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { jobs } from '../../jobs/client';
|
||||
|
||||
export type LeaveTeamOptions = {
|
||||
/**
|
||||
* The ID of the user who is leaving the team.
|
||||
*/
|
||||
userId: number;
|
||||
|
||||
/**
|
||||
* The ID of the team the user is leaving.
|
||||
*/
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const leaveTeam = async ({ userId, teamId }: LeaveTeamOptions): Promise<void> => {
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const team = await tx.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
ownerUserId: {
|
||||
not: userId,
|
||||
},
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
subscription: true,
|
||||
},
|
||||
});
|
||||
|
||||
const leavingUser = await tx.user.findUniqueOrThrow({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
await tx.teamMember.delete({
|
||||
where: {
|
||||
userId_teamId: {
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
team: {
|
||||
ownerUserId: {
|
||||
not: userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (IS_BILLING_ENABLED() && team.subscription) {
|
||||
const numberOfSeats = await tx.teamMember.count({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
await updateSubscriptionItemQuantity({
|
||||
priceId: team.subscription.priceId,
|
||||
subscriptionId: team.subscription.planId,
|
||||
quantity: numberOfSeats,
|
||||
});
|
||||
}
|
||||
|
||||
await jobs.triggerJob({
|
||||
name: 'send.team-member-left.email',
|
||||
payload: {
|
||||
teamId,
|
||||
memberUserId: leavingUser.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
};
|
||||
@ -1,122 +0,0 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { TeamTransferRequestTemplate } from '@documenso/email/templates/team-transfer-request';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
import { createTokenVerification } from '@documenso/lib/utils/token-verification';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
|
||||
export type RequestTeamOwnershipTransferOptions = {
|
||||
/**
|
||||
* The ID of the user initiating the transfer.
|
||||
*/
|
||||
userId: number;
|
||||
|
||||
/**
|
||||
* The name of the user initiating the transfer.
|
||||
*/
|
||||
userName: string;
|
||||
|
||||
/**
|
||||
* The ID of the team whose ownership is being transferred.
|
||||
*/
|
||||
teamId: number;
|
||||
|
||||
/**
|
||||
* The user ID of the new owner.
|
||||
*/
|
||||
newOwnerUserId: number;
|
||||
|
||||
/**
|
||||
* Whether to clear any current payment methods attached to the team.
|
||||
*/
|
||||
clearPaymentMethods: boolean;
|
||||
};
|
||||
|
||||
export const requestTeamOwnershipTransfer = async ({
|
||||
userId,
|
||||
userName,
|
||||
teamId,
|
||||
newOwnerUserId,
|
||||
}: RequestTeamOwnershipTransferOptions): Promise<void> => {
|
||||
// Todo: Clear payment methods disabled for now.
|
||||
const clearPaymentMethods = false;
|
||||
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const team = await tx.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
ownerUserId: userId,
|
||||
members: {
|
||||
some: {
|
||||
userId: newOwnerUserId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newOwnerUser = await tx.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: newOwnerUserId,
|
||||
},
|
||||
});
|
||||
|
||||
const { token, expiresAt } = createTokenVerification({ minute: 10 });
|
||||
|
||||
const teamVerificationPayload = {
|
||||
teamId,
|
||||
token,
|
||||
expiresAt,
|
||||
userId: newOwnerUserId,
|
||||
name: newOwnerUser.name ?? '',
|
||||
email: newOwnerUser.email,
|
||||
clearPaymentMethods,
|
||||
};
|
||||
|
||||
await tx.teamTransferVerification.upsert({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
create: teamVerificationPayload,
|
||||
update: teamVerificationPayload,
|
||||
});
|
||||
|
||||
const template = createElement(TeamTransferRequestTemplate, {
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
senderName: userName,
|
||||
teamName: team.name,
|
||||
teamUrl: team.url,
|
||||
token,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template),
|
||||
renderEmailWithI18N(template, { plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance();
|
||||
|
||||
await mailer.sendMail({
|
||||
to: newOwnerUser.email,
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(
|
||||
msg`You have been requested to take ownership of team ${team.name} on Documenso`,
|
||||
),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
};
|
||||
@ -3,7 +3,9 @@ import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { createTokenVerification } from '@documenso/lib/utils/token-verification';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { sendTeamEmailVerificationEmail } from './create-team-email-verification';
|
||||
import { getTeamSettings } from './get-team-settings';
|
||||
|
||||
export type ResendTeamMemberInvitationOptions = {
|
||||
userId: number;
|
||||
@ -17,32 +19,26 @@ export const resendTeamEmailVerification = async ({
|
||||
userId,
|
||||
teamId,
|
||||
}: ResendTeamMemberInvitationOptions): Promise<void> => {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery(teamId, userId, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM']),
|
||||
include: {
|
||||
emailVerification: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError('TeamNotFound', {
|
||||
message: 'User is not a member of the team.',
|
||||
});
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const team = await tx.team.findUniqueOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
role: {
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
emailVerification: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError('TeamNotFound', {
|
||||
message: 'User is not a member of the team.',
|
||||
});
|
||||
}
|
||||
|
||||
const { emailVerification } = team;
|
||||
|
||||
if (!emailVerification) {
|
||||
@ -63,7 +59,7 @@ export const resendTeamEmailVerification = async ({
|
||||
},
|
||||
});
|
||||
|
||||
await sendTeamEmailVerificationEmail(emailVerification.email, token, team);
|
||||
await sendTeamEmailVerificationEmail(emailVerification.email, token, team, settings);
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { sendTeamMemberInviteEmail } from './create-team-member-invites';
|
||||
|
||||
export type ResendTeamMemberInvitationOptions = {
|
||||
/**
|
||||
* The ID of the user who is initiating this action.
|
||||
*/
|
||||
userId: number;
|
||||
|
||||
/**
|
||||
* The name of the user who is initiating this action.
|
||||
*/
|
||||
userName: string;
|
||||
|
||||
/**
|
||||
* The ID of the team.
|
||||
*/
|
||||
teamId: number;
|
||||
|
||||
/**
|
||||
* The IDs of the invitations to resend.
|
||||
*/
|
||||
invitationId: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resend an email for a given team member invite.
|
||||
*/
|
||||
export const resendTeamMemberInvitation = async ({
|
||||
userId,
|
||||
userName,
|
||||
teamId,
|
||||
invitationId,
|
||||
}: ResendTeamMemberInvitationOptions): Promise<void> => {
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const team = await tx.team.findUniqueOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
role: {
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError('TeamNotFound', { message: 'User is not a valid member of the team.' });
|
||||
}
|
||||
|
||||
const teamMemberInvite = await tx.teamMemberInvite.findUniqueOrThrow({
|
||||
where: {
|
||||
id: invitationId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamMemberInvite) {
|
||||
throw new AppError('InviteNotFound', { message: 'No invite exists for this user.' });
|
||||
}
|
||||
|
||||
await sendTeamMemberInviteEmail({
|
||||
email: teamMemberInvite.email,
|
||||
token: teamMemberInvite.token,
|
||||
senderName: userName,
|
||||
team,
|
||||
});
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
};
|
||||
@ -1,104 +0,0 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import type Stripe from 'stripe';
|
||||
|
||||
import { transferTeamSubscription } from '@documenso/ee/server-only/stripe/transfer-team-subscription';
|
||||
import { mapStripeSubscriptionToPrismaUpsertAction } from '@documenso/ee/server-only/stripe/webhook/on-subscription-updated';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type TransferTeamOwnershipOptions = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
export const transferTeamOwnership = async ({ token }: TransferTeamOwnershipOptions) => {
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const teamTransferVerification = await tx.teamTransferVerification.findFirstOrThrow({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
include: {
|
||||
subscription: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { team, userId: newOwnerUserId } = teamTransferVerification;
|
||||
|
||||
await Promise.all([
|
||||
tx.teamTransferVerification.updateMany({
|
||||
where: {
|
||||
teamId: team.id,
|
||||
},
|
||||
data: {
|
||||
completed: true,
|
||||
},
|
||||
}),
|
||||
tx.teamTransferVerification.deleteMany({
|
||||
where: {
|
||||
teamId: team.id,
|
||||
expiresAt: {
|
||||
lt: new Date(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const newOwnerUser = await tx.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: newOwnerUserId,
|
||||
teamMembers: {
|
||||
some: {
|
||||
teamId: team.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
subscriptions: true,
|
||||
},
|
||||
});
|
||||
|
||||
let teamSubscription: Stripe.Subscription | null = null;
|
||||
|
||||
if (IS_BILLING_ENABLED()) {
|
||||
teamSubscription = await transferTeamSubscription({
|
||||
user: newOwnerUser,
|
||||
team,
|
||||
clearPaymentMethods: teamTransferVerification.clearPaymentMethods,
|
||||
});
|
||||
}
|
||||
|
||||
if (teamSubscription) {
|
||||
await tx.subscription.upsert(
|
||||
mapStripeSubscriptionToPrismaUpsertAction(teamSubscription, undefined, team.id),
|
||||
);
|
||||
}
|
||||
|
||||
await tx.team.update({
|
||||
where: {
|
||||
id: team.id,
|
||||
},
|
||||
data: {
|
||||
ownerUserId: newOwnerUserId,
|
||||
members: {
|
||||
update: {
|
||||
where: {
|
||||
userId_teamId: {
|
||||
teamId: team.id,
|
||||
userId: newOwnerUserId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
role: TeamMemberRole.ADMIN,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
};
|
||||
@ -1,61 +0,0 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamGlobalSettingsSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamGlobalSettingsSchema';
|
||||
|
||||
export type UpdateTeamBrandingSettingsOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
|
||||
settings: {
|
||||
brandingEnabled: boolean;
|
||||
brandingLogo: string;
|
||||
brandingUrl: string;
|
||||
brandingCompanyDetails: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const ZUpdateTeamBrandingSettingsResponseSchema = TeamGlobalSettingsSchema;
|
||||
|
||||
export type TUpdateTeamBrandingSettingsResponse = z.infer<
|
||||
typeof ZUpdateTeamBrandingSettingsResponseSchema
|
||||
>;
|
||||
|
||||
export const updateTeamBrandingSettings = async ({
|
||||
userId,
|
||||
teamId,
|
||||
settings,
|
||||
}: UpdateTeamBrandingSettingsOptions): Promise<TUpdateTeamBrandingSettingsResponse> => {
|
||||
const { brandingEnabled, brandingLogo, brandingUrl, brandingCompanyDetails } = settings;
|
||||
|
||||
const member = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!member || member.role !== TeamMemberRole.ADMIN) {
|
||||
throw new Error('You do not have permission to update this team.');
|
||||
}
|
||||
|
||||
return await prisma.teamGlobalSettings.upsert({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
create: {
|
||||
teamId,
|
||||
brandingEnabled,
|
||||
brandingLogo,
|
||||
brandingUrl,
|
||||
brandingCompanyDetails,
|
||||
},
|
||||
update: {
|
||||
brandingEnabled,
|
||||
brandingLogo,
|
||||
brandingUrl,
|
||||
brandingCompanyDetails,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,6 +1,8 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type UpdateTeamEmailOptions = {
|
||||
userId: number;
|
||||
@ -15,32 +17,30 @@ export const updateTeamEmail = async ({
|
||||
teamId,
|
||||
data,
|
||||
}: UpdateTeamEmailOptions): Promise<void> => {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
role: {
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
},
|
||||
},
|
||||
},
|
||||
teamEmail: {
|
||||
isNot: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery(teamId, userId, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM']),
|
||||
include: {
|
||||
teamEmail: true,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.teamEmail.update({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
data: {
|
||||
// Note: Never allow the email to be updated without re-verifying via email.
|
||||
name: data.name,
|
||||
},
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!team.teamEmail) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team email does not exist',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.teamEmail.update({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
data: {
|
||||
// Note: Never allow the email to be updated without re-verifying via email.
|
||||
name: data.name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
// Todo: orgs
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type UpdatePublicProfileOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
@ -15,14 +17,7 @@ export const updateTeamPublicProfile = async ({
|
||||
data,
|
||||
}: UpdatePublicProfileOptions) => {
|
||||
return await prisma.team.update({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: buildTeamWhereQuery(teamId, userId),
|
||||
data: {
|
||||
profile: {
|
||||
upsert: {
|
||||
|
||||
@ -5,6 +5,8 @@ import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type UpdateTeamOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
@ -23,24 +25,20 @@ export const updateTeam = async ({ userId, teamId, data }: UpdateTeamOptions): P
|
||||
},
|
||||
});
|
||||
|
||||
if (foundPendingTeamWithUrl) {
|
||||
const foundOrganisationWithUrl = await tx.organisation.findFirst({
|
||||
where: {
|
||||
url: data.url,
|
||||
},
|
||||
});
|
||||
|
||||
if (foundPendingTeamWithUrl || foundOrganisationWithUrl) {
|
||||
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
|
||||
message: 'Team URL already exists.',
|
||||
});
|
||||
}
|
||||
|
||||
const team = await tx.team.update({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
role: {
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: buildTeamWhereQuery(teamId, userId, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM']),
|
||||
data: {
|
||||
url: data.url,
|
||||
name: data.name,
|
||||
|
||||
Reference in New Issue
Block a user