mirror of
https://github.com/documenso/documenso.git
synced 2026-07-26 09:54:51 +10:00
chore: merge main, resolve biome formatting conflicts
Merge origin/main into feat/external-2fa-codes. Resolve formatting conflicts caused by biome rollout; preserve both feature streams: PR's external 2FA token + signing-session 2FA proof additions plus main's RateLimit/RecipientExpired/signingReminders/date-auto-insert. In complete-document-with-token.ts, drop the duplicate early field-fetching block introduced when main moved that logic later with date auto-insert support; keep the EXTERNAL_TWO_FACTOR_AUTH check using derivedRecipientActionAuth.
This commit is contained in:
@@ -1,23 +1,13 @@
|
||||
import {
|
||||
ALLOWED_TEAM_GROUP_TYPES,
|
||||
TEAM_MEMBER_ROLE_PERMISSIONS_MAP,
|
||||
} from '@documenso/lib/constants/teams';
|
||||
import { ALLOWED_TEAM_GROUP_TYPES, TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getMemberRoles } from '@documenso/lib/server-only/team/get-member-roles';
|
||||
import { generateDatabaseId } from '@documenso/lib/universal/id';
|
||||
import { buildTeamWhereQuery, isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
OrganisationGroupType,
|
||||
OrganisationMemberRole,
|
||||
TeamMemberRole,
|
||||
} from '@documenso/prisma/generated/types';
|
||||
import { OrganisationGroupType, OrganisationMemberRole, TeamMemberRole } from '@documenso/prisma/generated/types';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZCreateTeamGroupsRequestSchema,
|
||||
ZCreateTeamGroupsResponseSchema,
|
||||
} from './create-team-groups.types';
|
||||
import { ZCreateTeamGroupsRequestSchema, ZCreateTeamGroupsResponseSchema } from './create-team-groups.types';
|
||||
|
||||
export const createTeamGroupsRoute = authenticatedProcedure
|
||||
// .meta(createTeamGroupsMeta)
|
||||
@@ -65,10 +55,10 @@ export const createTeamGroupsRoute = authenticatedProcedure
|
||||
},
|
||||
});
|
||||
|
||||
// Hard validation — these failures indicate programming or authorisation
|
||||
// errors and should reject the whole request.
|
||||
const isValid = groups.every((group) => {
|
||||
const organisationGroup = team.organisation.groups.find(
|
||||
({ id }) => id === group.organisationGroupId,
|
||||
);
|
||||
const organisationGroup = team.organisation.groups.find(({ id }) => id === group.organisationGroupId);
|
||||
|
||||
// Only allow specific organisation groups to be used as a reference for team groups.
|
||||
if (!organisationGroup?.type || !ALLOWED_TEAM_GROUP_TYPES.includes(organisationGroup.type)) {
|
||||
@@ -84,11 +74,6 @@ export const createTeamGroupsRoute = authenticatedProcedure
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that the group is not already added to the team.
|
||||
if (organisationGroup.teamGroups.some((teamGroup) => teamGroup.teamId === teamId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that the user has permission to add the group to the team.
|
||||
if (!isTeamRoleWithinUserHierarchy(currentUserTeamRole, group.teamRole)) {
|
||||
return false;
|
||||
@@ -103,8 +88,21 @@ export const createTeamGroupsRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
// Silently drop groups already attached to the team. Makes the create
|
||||
// idempotent for the common race where a group was added between the
|
||||
// picker fetch and the submit.
|
||||
const filteredGroups = groups.filter((group) => {
|
||||
const organisationGroup = team.organisation.groups.find(({ id }) => id === group.organisationGroupId);
|
||||
|
||||
return !organisationGroup?.teamGroups.some((teamGroup) => teamGroup.teamId === teamId);
|
||||
});
|
||||
|
||||
if (filteredGroups.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.teamGroup.createMany({
|
||||
data: groups.map((group) => ({
|
||||
data: filteredGroups.map((group) => ({
|
||||
id: generateDatabaseId('team_group'),
|
||||
teamId,
|
||||
organisationGroupId: group.organisationGroupId,
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { OrganisationGroupType, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getMemberRoles } from '@documenso/lib/server-only/team/get-member-roles';
|
||||
import { generateDatabaseId } from '@documenso/lib/universal/id';
|
||||
import { buildTeamWhereQuery, isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { OrganisationGroupType, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZCreateTeamMembersRequestSchema,
|
||||
ZCreateTeamMembersResponseSchema,
|
||||
} from './create-team-members.types';
|
||||
import { ZCreateTeamMembersRequestSchema, ZCreateTeamMembersResponseSchema } from './create-team-members.types';
|
||||
|
||||
export const createTeamMembersRoute = authenticatedProcedure
|
||||
.input(ZCreateTeamMembersRequestSchema)
|
||||
@@ -44,11 +40,7 @@ type CreateTeamMembersOptions = {
|
||||
}[];
|
||||
};
|
||||
|
||||
export const createTeamMembers = async ({
|
||||
userId,
|
||||
teamId,
|
||||
membersToCreate,
|
||||
}: CreateTeamMembersOptions) => {
|
||||
export const createTeamMembers = async ({ userId, teamId, membersToCreate }: CreateTeamMembersOptions) => {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({
|
||||
teamId,
|
||||
@@ -136,25 +128,58 @@ export const createTeamMembers = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
!membersToCreate.every((member) =>
|
||||
isTeamRoleWithinUserHierarchy(currentUserTeamRole, member.teamRole),
|
||||
)
|
||||
) {
|
||||
if (!membersToCreate.every((member) => isTeamRoleWithinUserHierarchy(currentUserTeamRole, member.teamRole))) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Cannot add a member with a role higher than your own',
|
||||
});
|
||||
}
|
||||
|
||||
const teamRoleGroupId = (role: TeamMemberRole) =>
|
||||
match(role)
|
||||
.with(TeamMemberRole.MEMBER, () => teamMemberGroup.organisationGroupId)
|
||||
.with(TeamMemberRole.MANAGER, () => teamManagerGroup.organisationGroupId)
|
||||
.with(TeamMemberRole.ADMIN, () => teamAdminGroup.organisationGroupId)
|
||||
.exhaustive();
|
||||
|
||||
// Silently drop additions that would duplicate an existing membership in
|
||||
// the same internal-team role group (the only case that would hit the
|
||||
// (organisationMemberId, groupId) unique constraint). Members who are
|
||||
// implicitly part of the team via an INTERNAL_ORGANISATION group are NOT
|
||||
// dropped, so this still allows assigning an explicit team role on top
|
||||
// of the inherited org-level membership.
|
||||
const existingTeamGroupMemberships = await prisma.organisationGroupMember.findMany({
|
||||
where: {
|
||||
organisationMemberId: {
|
||||
in: membersToCreate.map((member) => member.organisationMemberId),
|
||||
},
|
||||
groupId: {
|
||||
in: [
|
||||
teamMemberGroup.organisationGroupId,
|
||||
teamManagerGroup.organisationGroupId,
|
||||
teamAdminGroup.organisationGroupId,
|
||||
],
|
||||
},
|
||||
},
|
||||
select: { organisationMemberId: true, groupId: true },
|
||||
});
|
||||
|
||||
const existingPairs = new Set(
|
||||
existingTeamGroupMemberships.map(({ organisationMemberId, groupId }) => `${organisationMemberId}:${groupId}`),
|
||||
);
|
||||
|
||||
const filteredMembersToCreate = membersToCreate.filter(
|
||||
(member) => !existingPairs.has(`${member.organisationMemberId}:${teamRoleGroupId(member.teamRole)}`),
|
||||
);
|
||||
|
||||
if (filteredMembersToCreate.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.organisationGroupMember.createMany({
|
||||
data: membersToCreate.map((member) => ({
|
||||
data: filteredMembersToCreate.map((member) => ({
|
||||
id: generateDatabaseId('group_member'),
|
||||
organisationMemberId: member.organisationMemberId,
|
||||
groupId: match(member.teamRole)
|
||||
.with(TeamMemberRole.MEMBER, () => teamMemberGroup.organisationGroupId)
|
||||
.with(TeamMemberRole.MANAGER, () => teamManagerGroup.organisationGroupId)
|
||||
.with(TeamMemberRole.ADMIN, () => teamAdminGroup.organisationGroupId)
|
||||
.exhaustive(),
|
||||
groupId: teamRoleGroupId(member.teamRole),
|
||||
})),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { TeamMemberRole } from '@documenso/prisma/generated/types';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZCreateTeamMembersRequestSchema = z.object({
|
||||
teamId: z.number(),
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZTeamUrlSchema } from './schema';
|
||||
import { ZTeamNameSchema } from './schema';
|
||||
import { ZTeamNameSchema, ZTeamUrlSchema } from './schema';
|
||||
|
||||
// export const createTeamMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
|
||||
@@ -6,10 +6,7 @@ import { prisma } from '@documenso/prisma';
|
||||
import { OrganisationGroupType, OrganisationMemberRole } from '@documenso/prisma/generated/types';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZDeleteTeamGroupRequestSchema,
|
||||
ZDeleteTeamGroupResponseSchema,
|
||||
} from './delete-team-group.types';
|
||||
import { ZDeleteTeamGroupRequestSchema, ZDeleteTeamGroupResponseSchema } from './delete-team-group.types';
|
||||
|
||||
export const deleteTeamGroupRoute = authenticatedProcedure
|
||||
// .meta(deleteTeamGroupMeta)
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { OrganisationGroupType } from '@prisma/client';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getMemberRoles } from '@documenso/lib/server-only/team/get-member-roles';
|
||||
import { buildTeamWhereQuery, isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { OrganisationGroupType } from '@prisma/client';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZDeleteTeamMemberRequestSchema,
|
||||
ZDeleteTeamMemberResponseSchema,
|
||||
} from './delete-team-member.types';
|
||||
import { ZDeleteTeamMemberRequestSchema, ZDeleteTeamMemberResponseSchema } from './delete-team-member.types';
|
||||
|
||||
export const deleteTeamMemberRoute = authenticatedProcedure
|
||||
// .meta(deleteTeamMemberMeta)
|
||||
@@ -34,6 +30,11 @@ export const deleteTeamMemberRoute = authenticatedProcedure
|
||||
roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
}),
|
||||
include: {
|
||||
organisation: {
|
||||
select: {
|
||||
ownerUserId: true,
|
||||
},
|
||||
},
|
||||
teamGroups: {
|
||||
where: {
|
||||
organisationGroup: {
|
||||
@@ -106,12 +107,38 @@ export const deleteTeamMemberRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.organisationGroupMember.delete({
|
||||
where: {
|
||||
organisationMemberId_groupId: {
|
||||
organisationMemberId: memberId,
|
||||
groupId: teamGroupToRemoveMemberFrom.organisationGroupId,
|
||||
const removedMember = teamGroupToRemoveMemberFrom.organisationGroup.organisationGroupMembers.find(
|
||||
(ogm) => ogm.organisationMember.id === memberId,
|
||||
);
|
||||
|
||||
if (!removedMember) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Member not found in this team',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Removing a user from a single team drops their INTERNAL_TEAM
|
||||
// OrganisationGroupMember link, but Envelope rows they authored in this
|
||||
// team still point at their userId. Reassign to the org owner so those
|
||||
// envelopes remain reachable after the member loses team access.
|
||||
await tx.envelope.updateMany({
|
||||
where: {
|
||||
userId: removedMember.organisationMember.userId,
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
userId: team.organisation.ownerUserId,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.organisationGroupMember.delete({
|
||||
where: {
|
||||
organisationMemberId_groupId: {
|
||||
organisationMemberId: memberId,
|
||||
groupId: teamGroupToRemoveMemberFrom.organisationGroupId,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { orphanEnvelopes } from '@documenso/lib/server-only/envelope/orphan-envelopes';
|
||||
import { transferTeamEnvelopes } from '@documenso/lib/server-only/envelope/transfer-team-envelopes';
|
||||
import { deleteTeam } from '@documenso/lib/server-only/team/delete-team';
|
||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import { ZDeleteTeamRequestSchema, ZDeleteTeamResponseSchema } from './delete-team.types';
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import type { FindResultResponse } from '@documenso/lib/types/search-params';
|
||||
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { unique } from 'remeda';
|
||||
|
||||
import type { FindResultResponse } from '@documenso/lib/types/search-params';
|
||||
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZFindTeamGroupsRequestSchema,
|
||||
ZFindTeamGroupsResponseSchema,
|
||||
} from './find-team-groups.types';
|
||||
import { ZFindTeamGroupsRequestSchema, ZFindTeamGroupsResponseSchema } from './find-team-groups.types';
|
||||
|
||||
export const findTeamGroupsRoute = authenticatedProcedure
|
||||
// .meta(getTeamGroupsMeta)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { TeamGroupSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamGroupSchema';
|
||||
import { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const getTeamGroupsMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { findTeamMembers } from '@documenso/lib/server-only/team/find-team-members';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZFindTeamMembersRequestSchema,
|
||||
ZFindTeamMembersResponseSchema,
|
||||
} from './find-team-members.types';
|
||||
import { ZFindTeamMembersRequestSchema, ZFindTeamMembersResponseSchema } from './find-team-members.types';
|
||||
|
||||
export const findTeamMembersRoute = authenticatedProcedure
|
||||
.input(ZFindTeamMembersRequestSchema)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { OrganisationMemberRole, TeamMemberRole } from '@documenso/prisma/generated/types';
|
||||
import OrganisationMemberSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationMemberSchema';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZFindTeamMembersRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
teamId: z.number(),
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const getTeamsMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { getTeamMembers } from '@documenso/lib/server-only/team/get-team-members';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetTeamMembersRequestSchema,
|
||||
ZGetTeamMembersResponseSchema,
|
||||
} from './get-team-members.types';
|
||||
import { ZGetTeamMembersRequestSchema, ZGetTeamMembersResponseSchema } from './get-team-members.types';
|
||||
|
||||
export const getTeamMembersRoute = authenticatedProcedure
|
||||
// .meta(getTeamMembersMeta)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { OrganisationMemberRole, TeamMemberRole } from '@documenso/prisma/generated/types';
|
||||
import OrganisationMemberSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationMemberSchema';
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const getTeamMembersMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { TeamMemberRole } from '@documenso/prisma/generated/types';
|
||||
import OrganisationGlobalSettingsSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationGlobalSettingsSchema';
|
||||
import TeamGlobalSettingsSchema from '@documenso/prisma/generated/zod/modelSchema/TeamGlobalSettingsSchema';
|
||||
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const getTeamMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
|
||||
@@ -58,37 +58,33 @@ export const teamRouter = router({
|
||||
get: authenticatedProcedure.query(async ({ ctx }) => {
|
||||
return await getTeamEmailByEmail({ email: ctx.user.email });
|
||||
}),
|
||||
update: authenticatedProcedure
|
||||
.input(ZUpdateTeamEmailMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
teamId: input.teamId,
|
||||
},
|
||||
});
|
||||
update: authenticatedProcedure.input(ZUpdateTeamEmailMutationSchema).mutation(async ({ input, ctx }) => {
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
teamId: input.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
return await updateTeamEmail({
|
||||
userId: ctx.user.id,
|
||||
...input,
|
||||
});
|
||||
}),
|
||||
delete: authenticatedProcedure
|
||||
.input(ZDeleteTeamEmailMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId } = input;
|
||||
return await updateTeamEmail({
|
||||
userId: ctx.user.id,
|
||||
...input,
|
||||
});
|
||||
}),
|
||||
delete: authenticatedProcedure.input(ZDeleteTeamEmailMutationSchema).mutation(async ({ input, ctx }) => {
|
||||
const { teamId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
return await deleteTeamEmail({
|
||||
userId: ctx.user.id,
|
||||
userEmail: ctx.user.email,
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
teamId,
|
||||
});
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return await deleteTeamEmail({
|
||||
userId: ctx.user.id,
|
||||
userEmail: ctx.user.email,
|
||||
teamId,
|
||||
});
|
||||
}),
|
||||
verification: {
|
||||
send: authenticatedProcedure
|
||||
.input(ZCreateTeamEmailVerificationMutationSchema)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { URL_PATTERN, ZNameSchema } from '@documenso/lib/constants/auth';
|
||||
import { PROTECTED_TEAM_URLS } from '@documenso/lib/constants/teams';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PROTECTED_TEAM_URLS } from '@documenso/lib/constants/teams';
|
||||
|
||||
/**
|
||||
* Restrict team URLs schema.
|
||||
*
|
||||
@@ -26,10 +27,7 @@ export const ZTeamUrlSchema = z
|
||||
.toLowerCase()
|
||||
.regex(/^[a-z0-9].*[^_-]$/, 'Team URL cannot start or end with dashes or underscores.')
|
||||
.regex(/^(?!.*[-_]{2})/, 'Team URL cannot contain consecutive dashes or underscores.')
|
||||
.regex(
|
||||
/^[a-z0-9]+(?:[-_][a-z0-9]+)*$/,
|
||||
'Team URL can only contain letters, numbers, dashes and underscores.',
|
||||
)
|
||||
.regex(/^[a-z0-9]+(?:[-_][a-z0-9]+)*$/, 'Team URL can only contain letters, numbers, dashes and underscores.')
|
||||
.refine((value) => !PROTECTED_TEAM_URLS.includes(value), {
|
||||
message: 'This URL is already in use.',
|
||||
});
|
||||
@@ -38,12 +36,15 @@ export const ZTeamNameSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3, { message: 'Team name must be at least 3 characters long.' })
|
||||
.max(30, { message: 'Team name must not exceed 30 characters.' });
|
||||
.max(30, { message: 'Team name must not exceed 30 characters.' })
|
||||
.refine((value) => !URL_PATTERN.test(value), {
|
||||
message: 'Team name cannot contain URLs.',
|
||||
});
|
||||
|
||||
export const ZCreateTeamEmailVerificationMutationSchema = z.object({
|
||||
teamId: z.number(),
|
||||
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
|
||||
email: z.string().trim().email().toLowerCase().min(1, 'Please enter a valid email.'),
|
||||
name: ZNameSchema,
|
||||
email: zEmail().trim().toLowerCase().min(1, 'Please enter a valid email.'),
|
||||
});
|
||||
|
||||
export const ZDeleteTeamEmailMutationSchema = z.object({
|
||||
@@ -61,7 +62,7 @@ export const ZGetTeamMembersQuerySchema = z.object({
|
||||
export const ZUpdateTeamEmailMutationSchema = z.object({
|
||||
teamId: z.number(),
|
||||
data: z.object({
|
||||
name: z.string().trim().min(1),
|
||||
name: ZNameSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -77,13 +78,9 @@ export const ZResendTeamEmailVerificationMutationSchema = z.object({
|
||||
teamId: z.number(),
|
||||
});
|
||||
|
||||
export type TCreateTeamEmailVerificationMutationSchema = z.infer<
|
||||
typeof ZCreateTeamEmailVerificationMutationSchema
|
||||
>;
|
||||
export type TCreateTeamEmailVerificationMutationSchema = z.infer<typeof ZCreateTeamEmailVerificationMutationSchema>;
|
||||
|
||||
export type TDeleteTeamEmailMutationSchema = z.infer<typeof ZDeleteTeamEmailMutationSchema>;
|
||||
export type TGetTeamMembersQuerySchema = z.infer<typeof ZGetTeamMembersQuerySchema>;
|
||||
export type TUpdateTeamEmailMutationSchema = z.infer<typeof ZUpdateTeamEmailMutationSchema>;
|
||||
export type TResendTeamEmailVerificationMutationSchema = z.infer<
|
||||
typeof ZResendTeamEmailVerificationMutationSchema
|
||||
>;
|
||||
export type TResendTeamEmailVerificationMutationSchema = z.infer<typeof ZResendTeamEmailVerificationMutationSchema>;
|
||||
|
||||
@@ -6,10 +6,7 @@ import { prisma } from '@documenso/prisma';
|
||||
import { OrganisationGroupType } from '@documenso/prisma/generated/types';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZUpdateTeamGroupRequestSchema,
|
||||
ZUpdateTeamGroupResponseSchema,
|
||||
} from './update-team-group.types';
|
||||
import { ZUpdateTeamGroupRequestSchema, ZUpdateTeamGroupResponseSchema } from './update-team-group.types';
|
||||
|
||||
export const updateTeamGroupRoute = authenticatedProcedure
|
||||
// .meta(updateTeamGroupMeta)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getMemberRoles } from '@documenso/lib/server-only/team/get-member-roles';
|
||||
@@ -7,12 +5,10 @@ import { generateDatabaseId } from '@documenso/lib/universal/id';
|
||||
import { buildTeamWhereQuery, isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { OrganisationGroupType, TeamMemberRole } from '@documenso/prisma/generated/types';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZUpdateTeamMemberRequestSchema,
|
||||
ZUpdateTeamMemberResponseSchema,
|
||||
} from './update-team-member.types';
|
||||
import { ZUpdateTeamMemberRequestSchema, ZUpdateTeamMemberResponseSchema } from './update-team-member.types';
|
||||
|
||||
export const updateTeamMemberRoute = authenticatedProcedure
|
||||
// .meta(updateTeamMemberMeta)
|
||||
@@ -78,9 +74,7 @@ export const updateTeamMemberRoute = authenticatedProcedure
|
||||
(group) =>
|
||||
group.organisationGroup.type === OrganisationGroupType.INTERNAL_TEAM &&
|
||||
group.teamId === teamId &&
|
||||
group.organisationGroup.organisationGroupMembers.some(
|
||||
(member) => member.organisationMemberId === memberId,
|
||||
),
|
||||
group.organisationGroup.organisationGroupMembers.some((member) => member.organisationMemberId === memberId),
|
||||
);
|
||||
|
||||
const teamMemberGroup = team.teamGroups.find(
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { OrganisationType } from '@prisma/client';
|
||||
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { normalizeBrandingColors } from '@documenso/lib/utils/normalize-branding-colors';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { type SanitizeBrandingCssWarning, sanitizeBrandingCss } from '@documenso/lib/utils/sanitize-branding-css';
|
||||
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { OrganisationType, Prisma } from '@prisma/client';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZUpdateTeamSettingsRequestSchema,
|
||||
ZUpdateTeamSettingsResponseSchema,
|
||||
} from './update-team-settings.types';
|
||||
import { ZUpdateTeamSettingsRequestSchema, ZUpdateTeamSettingsResponseSchema } from './update-team-settings.types';
|
||||
|
||||
export const updateTeamSettingsRoute = authenticatedProcedure
|
||||
.input(ZUpdateTeamSettingsRequestSchema)
|
||||
@@ -40,12 +37,16 @@ export const updateTeamSettingsRoute = authenticatedProcedure
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
delegateDocumentOwnership,
|
||||
envelopeExpirationPeriod,
|
||||
reminderSettings,
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled,
|
||||
brandingLogo,
|
||||
brandingUrl,
|
||||
brandingCompanyDetails,
|
||||
brandingColors,
|
||||
brandingCss,
|
||||
|
||||
// Email related settings.
|
||||
emailId,
|
||||
@@ -66,11 +67,7 @@ export const updateTeamSettingsRoute = authenticatedProcedure
|
||||
}
|
||||
|
||||
// Signatures will only be inherited if all are NULL.
|
||||
if (
|
||||
typedSignatureEnabled === false &&
|
||||
uploadSignatureEnabled === false &&
|
||||
drawSignatureEnabled === false
|
||||
) {
|
||||
if (typedSignatureEnabled === false && uploadSignatureEnabled === false && drawSignatureEnabled === false) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'At least one signature type must be enabled',
|
||||
});
|
||||
@@ -123,8 +120,7 @@ export const updateTeamSettingsRoute = authenticatedProcedure
|
||||
});
|
||||
|
||||
const isPersonalOrganisation = organisation?.type === OrganisationType.PERSONAL;
|
||||
const currentIncludeSenderDetails =
|
||||
organisation?.organisationGlobalSettings.includeSenderDetails;
|
||||
const currentIncludeSenderDetails = organisation?.organisationGlobalSettings.includeSenderDetails;
|
||||
|
||||
const isChangingIncludeSenderDetails =
|
||||
includeSenderDetails !== undefined && includeSenderDetails !== currentIncludeSenderDetails;
|
||||
@@ -135,6 +131,27 @@ export const updateTeamSettingsRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
// Sanitize custom branding CSS at write time. `null` means inherit-from-org
|
||||
// for teams, so only run the sanitiser when an explicit string is provided.
|
||||
// An empty string after sanitisation is collapsed to `null` so the team
|
||||
// row inherits rather than persisting an empty override.
|
||||
let cssWarnings: SanitizeBrandingCssWarning[] | undefined;
|
||||
let sanitizedBrandingCss: string | null | undefined;
|
||||
|
||||
if (brandingCss === null) {
|
||||
sanitizedBrandingCss = null;
|
||||
} else if (typeof brandingCss === 'string') {
|
||||
const result = sanitizeBrandingCss(brandingCss);
|
||||
sanitizedBrandingCss = result.css.trim() === '' ? null : result.css;
|
||||
cssWarnings = result.warnings;
|
||||
}
|
||||
|
||||
// Strip empty-string colour values; collapse to `null` when the payload
|
||||
// contains no overrides. For teams this matters because brandingEnabled
|
||||
// = null inherits from the org — leaving `{}` here would persist a real
|
||||
// override of nothing once a team toggles brandingEnabled = true.
|
||||
const normalizedBrandingColors = normalizeBrandingColors(brandingColors);
|
||||
|
||||
await prisma.team.update({
|
||||
where: {
|
||||
id: teamId,
|
||||
@@ -154,19 +171,22 @@ export const updateTeamSettingsRoute = authenticatedProcedure
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
delegateDocumentOwnership,
|
||||
envelopeExpirationPeriod: envelopeExpirationPeriod === null ? Prisma.DbNull : envelopeExpirationPeriod,
|
||||
reminderSettings: reminderSettings === null ? Prisma.DbNull : reminderSettings,
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled,
|
||||
brandingLogo,
|
||||
brandingUrl,
|
||||
brandingCompanyDetails,
|
||||
brandingColors: normalizedBrandingColors === null ? Prisma.DbNull : normalizedBrandingColors,
|
||||
brandingCss: sanitizedBrandingCss,
|
||||
|
||||
// Email related settings.
|
||||
emailId,
|
||||
emailReplyTo,
|
||||
// emailReplyToName,
|
||||
emailDocumentSettings:
|
||||
emailDocumentSettings === null ? Prisma.DbNull : emailDocumentSettings,
|
||||
emailDocumentSettings: emailDocumentSettings === null ? Prisma.DbNull : emailDocumentSettings,
|
||||
defaultRecipients: defaultRecipients === null ? Prisma.DbNull : defaultRecipients,
|
||||
|
||||
// AI features settings.
|
||||
@@ -175,4 +195,8 @@ export const updateTeamSettingsRoute = authenticatedProcedure
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
cssWarnings: cssWarnings && cssWarnings.length > 0 ? cssWarnings : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { BRANDING_CSS_MAX_LENGTH } from '@documenso/lib/constants/branding';
|
||||
import { ZEnvelopeExpirationPeriod } from '@documenso/lib/constants/envelope-expiration';
|
||||
import { ZEnvelopeReminderSettings } from '@documenso/lib/constants/envelope-reminder';
|
||||
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
|
||||
import { ZCssVarsSchema } from '@documenso/lib/types/css-vars';
|
||||
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
} from '@documenso/lib/types/document-meta';
|
||||
import { ZDocumentMetaDateFormatSchema, ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import { ZSanitizeBrandingCssWarningSchema } from '@documenso/lib/utils/sanitize-branding-css';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Null = Inherit from organisation.
|
||||
@@ -28,16 +30,20 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({
|
||||
uploadSignatureEnabled: z.boolean().nullish(),
|
||||
drawSignatureEnabled: z.boolean().nullish(),
|
||||
delegateDocumentOwnership: z.boolean().nullish(),
|
||||
envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullish(),
|
||||
reminderSettings: ZEnvelopeReminderSettings.nullish(),
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled: z.boolean().nullish(),
|
||||
brandingLogo: z.string().nullish(),
|
||||
brandingUrl: z.string().nullish(),
|
||||
brandingCompanyDetails: z.string().nullish(),
|
||||
brandingColors: ZCssVarsSchema.nullish(),
|
||||
brandingCss: z.string().max(BRANDING_CSS_MAX_LENGTH).nullish(),
|
||||
|
||||
// Email related settings.
|
||||
emailId: z.string().nullish(),
|
||||
emailReplyTo: z.string().email().nullish(),
|
||||
emailReplyTo: zEmail().nullish(),
|
||||
// emailReplyToName: z.string().nullish(),
|
||||
emailDocumentSettings: ZDocumentEmailSettingsSchema.nullish(),
|
||||
|
||||
@@ -48,4 +54,6 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZUpdateTeamSettingsResponseSchema = z.void();
|
||||
export const ZUpdateTeamSettingsResponseSchema = z.object({
|
||||
cssWarnings: z.array(ZSanitizeBrandingCssWarningSchema).optional(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user